feat: 修复ai历史记录等问题

master^2
Lxy 1 week ago
parent a65cca62f8
commit 70865d0958

@ -870,7 +870,7 @@ def get_ai_analysis(symbol: str, force_refresh: bool = False, db: Session = Depe
@router.get("/ai-analysis/{symbol}/history") @router.get("/ai-analysis/{symbol}/history")
def get_ai_analysis_history(symbol: str, limit: int = 10, analysis_db: Session = Depends(get_analysis_db)): def get_ai_analysis_history(symbol: str, limit: int = 20, analysis_db: Session = Depends(get_analysis_db)):
"""获取AI分析历史记录""" """获取AI分析历史记录"""
try: try:
records = analysis_db.query(AIAnalysisCache).filter( records = analysis_db.query(AIAnalysisCache).filter(
@ -885,9 +885,7 @@ def get_ai_analysis_history(symbol: str, limit: int = 10, analysis_db: Session =
"id": r.id, "id": r.id,
"symbol": r.symbol, "symbol": r.symbol,
"analysis_time": r.created_at.isoformat(), "analysis_time": r.created_at.isoformat(),
"summary": r.analysis_data.get("summary", ""), "analysis_data": r.analysis_data
"trading_suggestion": r.analysis_data.get("trading_suggestion", {}),
"confidence": r.analysis_data.get("trading_suggestion", {}).get("confidence", 0)
} for r in records] } for r in records]
} }
except Exception as e: except Exception as e:
@ -896,3 +894,34 @@ def get_ai_analysis_history(symbol: str, limit: int = 10, analysis_db: Session =
"success": False, "success": False,
"error": f"获取历史记录失败: {str(e)}" "error": f"获取历史记录失败: {str(e)}"
} }
@router.get("/ai-analysis/history/{record_id}")
def get_ai_analysis_detail(record_id: int, analysis_db: Session = Depends(get_analysis_db)):
"""获取单条AI分析记录详情"""
try:
record = analysis_db.query(AIAnalysisCache).filter(
AIAnalysisCache.id == record_id
).first()
if not record:
return {
"success": False,
"error": "记录不存在"
}
return {
"success": True,
"data": {
"id": record.id,
"symbol": record.symbol,
"analysis_time": record.created_at.isoformat(),
"analysis_data": record.analysis_data
}
}
except Exception as e:
logger.error(f"获取AI分析详情失败: {e}")
return {
"success": False,
"error": f"获取记录详情失败: {str(e)}"
}

@ -1597,6 +1597,328 @@ body.theme-minimal .sort-select select:hover {
border-color: var(--text-muted); border-color: var(--text-muted);
} }
/* AI历史记录详情弹窗样式 */
.ai-history-detail {
padding: 8px 0;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.detail-header h4 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.detail-header h4 i {
color: var(--cyan);
}
.detail-time {
font-size: 12px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 6px;
}
.detail-summary {
background: rgba(6, 182, 212, 0.05);
border-left: 3px solid var(--cyan);
padding: 12px 16px;
margin-bottom: 16px;
border-radius: 4px;
display: flex;
gap: 10px;
align-items: flex-start;
}
.detail-summary i {
color: var(--cyan);
font-size: 14px;
margin-top: 2px;
}
.detail-summary p {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
flex: 1;
}
.detail-suggestion {
margin-bottom: 16px;
}
.suggestion-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 8px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.suggestion-card.long {
border-color: var(--green);
background: rgba(16, 185, 129, 0.05);
}
.suggestion-card.short {
border-color: var(--red);
background: rgba(239, 68, 68, 0.05);
}
.suggestion-card.neutral {
border-color: var(--amber);
background: rgba(245, 158, 11, 0.05);
}
.suggestion-card i {
font-size: 24px;
}
.suggestion-card.long i {
color: var(--green);
}
.suggestion-card.short i {
color: var(--red);
}
.suggestion-card.neutral i {
color: var(--amber);
}
.suggestion-text {
font-size: 20px;
font-weight: 700;
flex: 1;
}
.suggestion-card.long .suggestion-text {
color: var(--green);
}
.suggestion-card.short .suggestion-text {
color: var(--red);
}
.suggestion-card.neutral .suggestion-text {
color: var(--amber);
}
.confidence-text {
font-size: 14px;
color: var(--text-muted);
}
.detail-metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.metric-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
text-align: center;
}
.metric-label {
display: block;
font-size: 11px;
color: var(--text-muted);
margin-bottom: 6px;
}
.metric-value {
display: block;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.metric-value.down {
color: var(--red);
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h5 {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.detail-section h5 i {
color: var(--cyan);
font-size: 13px;
}
.four-d-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.four-d-table th {
background: var(--bg-secondary);
padding: 8px 10px;
text-align: left;
color: var(--text-muted);
font-weight: 600;
border-bottom: 1px solid var(--border-color);
}
.four-d-table td {
padding: 10px;
border-bottom: 1px solid rgba(56, 189, 248, 0.05);
color: var(--text-secondary);
}
.four-d-table tr:hover td {
background: rgba(6, 182, 212, 0.03);
}
.kdj-diagnosis-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.kdj-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.kdj-label {
font-size: 11px;
color: var(--text-muted);
}
.kdj-value {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.pivot-points-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.pivot-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
text-align: center;
display: flex;
flex-direction: column;
gap: 4px;
}
.pivot-item span:first-child {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
}
.pivot-item strong {
font-size: 14px;
color: var(--text-primary);
}
.pivot-item.resistance span:first-child {
color: var(--red);
}
.pivot-item.resistance strong {
color: var(--red);
}
.pivot-item.support span:first-child {
color: var(--green);
}
.pivot-item.support strong {
color: var(--green);
}
.pivot-item.center {
border-color: var(--purple);
background: rgba(139, 92, 246, 0.05);
}
.pivot-item.center span:first-child {
color: var(--purple);
}
.pivot-item.center strong {
color: var(--purple);
}
.warning-list {
list-style: none;
padding: 0;
}
.warning-list li {
background: rgba(245, 158, 11, 0.05);
border-left: 3px solid var(--amber);
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 4px;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.warning-list li:last-child {
margin-bottom: 0;
}
@media (max-width: 768px) {
.detail-metrics {
grid-template-columns: repeat(2, 1fr);
}
.pivot-points-grid {
grid-template-columns: repeat(3, 1fr);
}
.kdj-diagnosis-grid {
grid-template-columns: 1fr;
}
}
/* ============================================ /* ============================================
AI AI
============================================ */ ============================================ */

@ -520,7 +520,7 @@ function updateDetailView(data) {
async function loadHistoryList(symbol) { async function loadHistoryList(symbol) {
try { try {
const response = await fetch(`${API_BASE}/analysis/history/${symbol}?limit=10`); const response = await fetch(`${API_BASE}/ai-analysis/${symbol}/history?limit=20`);
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
renderHistoryList(data.data); renderHistoryList(data.data);
@ -537,13 +537,18 @@ async function loadAIAnalysis() {
const content = document.getElementById('ai-analysis-content'); const content = document.getElementById('ai-analysis-content');
try { try {
console.log(`加载合约 ${currentSymbol} 的AI分析...`);
const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}`); const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}`);
const data = await response.json(); const data = await response.json();
console.log(`合约 ${currentSymbol} AI分析响应:`, data);
if (data.success && data.data) { if (data.success && data.data) {
console.log(`合约 ${currentSymbol} 分析数据 - symbol:`, data.data.symbol);
currentAIAnalysis = data.data; currentAIAnalysis = data.data;
displayAIAnalysisResult(data.data); displayAIAnalysisResult(data.data);
} else { } else {
console.log(`合约 ${currentSymbol} 无分析结果`);
content.innerHTML = ` content.innerHTML = `
<div class="ai-analysis-placeholder"> <div class="ai-analysis-placeholder">
<i class="fas fa-brain"></i> <i class="fas fa-brain"></i>
@ -552,7 +557,7 @@ async function loadAIAnalysis() {
`; `;
} }
} catch (error) { } catch (error) {
console.error('加载AI分析失败:', error); console.error(`加载合约 ${currentSymbol} AI分析失败:`, error);
content.innerHTML = ` content.innerHTML = `
<div class="ai-analysis-placeholder"> <div class="ai-analysis-placeholder">
<i class="fas fa-brain"></i> <i class="fas fa-brain"></i>
@ -569,28 +574,34 @@ function renderHistoryList(records) {
return; return;
} }
container.innerHTML = records.map(record => ` console.log('渲染历史记录,记录数量:', records.length);
<div class="history-item" onclick="showHistoryModal(${JSON.stringify(record).replace(/"/g, '&quot;')})"> console.log('历史记录合约分布:', records.map(r => r.symbol));
<div class="history-item-left">
<span class="history-time">${record.analysis_time ? record.analysis_time.replace('T', ' ').substring(0, 16) : '--'}</span> container.innerHTML = records.map(record => {
<span class="history-suggestion ${record.suggestion_type || 'neutral'}">${record.suggestion || '--'}</span> const analysisData = record.analysis_data || {};
<span class="history-score">评分: ${record.trend_score || '--'}</span> const suggestion = analysisData.trading_suggestion || {};
</div> const timeStr = record.analysis_time ? record.analysis_time.replace('T', ' ').substring(0, 16) : '--';
<div class="history-item-right"> const summary = analysisData.summary || '--';
<div class="history-metric"> const direction = suggestion.direction || '--';
<span class="history-metric-label">MACD</span> const confidence = suggestion.confidence || 0;
<span class="history-metric-value">${record.macd_signal || '--'}</span>
console.log(`历史记录 ID:${record.id} 合约:${record.symbol}`);
return `
<div class="history-item" onclick="showAIHistoryDetail(${record.id})">
<div class="history-item-left">
<span class="history-time">${timeStr}</span>
<span class="history-suggestion">${summary.substring(0, 30)}${summary.length > 30 ? '...' : ''}</span>
<span class="history-score">方向: ${direction} | 置信度: ${confidence}%</span>
</div> </div>
<div class="history-metric"> <div class="history-item-right">
<span class="history-metric-label">RSI</span> <button class="history-detail-btn" onclick="event.stopPropagation(); showAIHistoryDetail(${record.id})">
<span class="history-metric-value">${record.rsi_value || '--'}</span> <i class="fas fa-chevron-right"></i>
</button>
</div> </div>
<button class="history-detail-btn" onclick="event.stopPropagation(); showHistoryModal(${JSON.stringify(record).replace(/"/g, '&quot;')})">
<i class="fas fa-chevron-right"></i>
</button>
</div> </div>
</div> `;
`).join(''); }).join('');
} }
function showSuggestionModal(data) { function showSuggestionModal(data) {
@ -717,10 +728,13 @@ function showHistoryModal(record) {
} }
async function loadKlineData(symbol, period) { async function loadKlineData(symbol, period) {
if (!symbol) return;
try { try {
const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`); const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`);
const data = await response.json(); const data = await response.json();
if (data.success) {
if (data.success && data.data) {
renderKlineChart(data.data); renderKlineChart(data.data);
} }
} catch (error) { } catch (error) {
@ -728,6 +742,161 @@ async function loadKlineData(symbol, period) {
} }
} }
async function showAIHistoryDetail(recordId) {
try {
const response = await fetch(`${API_BASE}/ai-analysis/history/${recordId}`);
const data = await response.json();
if (data.success && data.data) {
const record = data.data;
const result = record.analysis_data;
const timestamp = new Date(record.analysis_time).toLocaleString('zh-CN');
// 构建弹窗内容
const modalBody = document.getElementById('ai-analysis-modal-body');
const direction = result.trading_suggestion?.direction || '观望';
const directionClass = direction === '做多' ? 'long' : direction === '做空' ? 'short' : 'neutral';
const directionIcon = direction === '做多' ? 'fa-arrow-up' : direction === '做空' ? 'fa-arrow-down' : 'fa-arrows-left-right';
const confidence = result.trading_suggestion?.confidence || 0;
modalBody.innerHTML = `
<div class="ai-history-detail">
<div class="detail-header">
<h4><i class="fas fa-file-alt"></i> ${record.symbol} AI</h4>
<span class="detail-time"><i class="fas fa-clock"></i> ${timestamp}</span>
</div>
<div class="detail-summary">
<i class="fas fa-quote-left"></i>
<p>${result.summary || '暂无总结'}</p>
</div>
<!-- AI交易建议卡片已隐藏 -->
<!-- <div class="detail-suggestion">
<div class="suggestion-card ${directionClass}">
<i class="fas ${directionIcon}"></i>
<span class="suggestion-text">${direction}</span>
<span class="confidence-text">置信度: ${confidence}%</span>
</div>
</div> -->
${result.four_dimensional ? `
<div class="detail-section">
<h5><i class="fas fa-brain"></i> AI</h5>
<table class="four-d-table">
<thead>
<tr>
<th>周期</th>
<th>MACD趋势</th>
<th>成交量</th>
<th>KDJ状态</th>
<th>结论</th>
</tr>
</thead>
<tbody>
${Object.entries(result.four_dimensional).map(([period, d]) => `
<tr>
<td><strong>${period}</strong></td>
<td>${d.macd?.trend || '--'}</td>
<td>${d.volume?.status || '--'}</td>
<td>${d.kdj?.status || '--'}</td>
<td>${d.conclusion || '--'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : ''}
<div class="detail-metrics">
<div class="metric-card">
<span class="metric-label">入场区间</span>
<span class="metric-value">${result.trading_suggestion?.entry_range?.min || '--'}-${result.trading_suggestion?.entry_range?.max || '--'}</span>
</div>
<div class="metric-card">
<span class="metric-label">止损位</span>
<span class="metric-value down">${result.trading_suggestion?.stop_loss || '--'}</span>
</div>
<div class="metric-card">
<span class="metric-label">建议仓位</span>
<span class="metric-value">${result.trading_suggestion?.position_size || '--'}</span>
</div>
<div class="metric-card">
<span class="metric-label">纪律评分</span>
<span class="metric-value">${result.discipline_score?.total || '--'}/${result.discipline_score?.max || '11'}</span>
</div>
</div>
${result.kdj_diagnosis ? `
<div class="detail-section">
<h5><i class="fas fa-stethoscope"></i> KDJ</h5>
<div class="kdj-diagnosis-grid">
<div class="kdj-item">
<span class="kdj-label">当前状态</span>
<span class="kdj-value">${result.kdj_diagnosis.current_status || '--'}</span>
</div>
<div class="kdj-item">
<span class="kdj-label">背离</span>
<span class="kdj-value">${result.kdj_diagnosis.divergence || '--'}</span>
</div>
<div class="kdj-item">
<span class="kdj-label">钝化</span>
<span class="kdj-value">${result.kdj_diagnosis.paralysis || '--'}</span>
</div>
<div class="kdj-item" style="grid-column: 1 / -1;">
<span class="kdj-label">建议</span>
<span class="kdj-value">${result.kdj_diagnosis.recommendation || '--'}</span>
</div>
</div>
</div>
` : ''}
${result.pivot_points ? `
<div class="detail-section">
<h5><i class="fas fa-crosshairs"></i> </h5>
<div class="pivot-points-grid">
<div class="pivot-item resistance">
<span>R2</span><strong>${result.pivot_points.r2 || '--'}</strong>
</div>
<div class="pivot-item resistance">
<span>R1</span><strong>${result.pivot_points.r1 || '--'}</strong>
</div>
<div class="pivot-item center">
<span>PP</span><strong>${result.pivot_points.pp || '--'}</strong>
</div>
<div class="pivot-item support">
<span>S1</span><strong>${result.pivot_points.s1 || '--'}</strong>
</div>
<div class="pivot-item support">
<span>S2</span><strong>${result.pivot_points.s2 || '--'}</strong>
</div>
</div>
</div>
` : ''}
${result.risk_warnings && result.risk_warnings.length > 0 ? `
<div class="detail-section">
<h5><i class="fas fa-exclamation-triangle"></i> </h5>
<ul class="warning-list">
${result.risk_warnings.map(w => `<li>${w}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
// 显示弹窗
document.getElementById('ai-analysis-modal').classList.add('active');
} else {
showToast('error', '加载失败', data.error || '记录不存在');
}
} catch (error) {
console.error('加载历史记录详情失败:', error);
showToast('error', '加载失败', '网络错误,请稍后重试');
}
}
function renderKlineChart(data) { function renderKlineChart(data) {
if (klineChart) { if (klineChart) {
klineChart.dispose(); klineChart.dispose();

@ -0,0 +1,12 @@
import sqlite3
conn = sqlite3.connect('data/futures_analysis.db')
cursor = conn.execute('SELECT id, symbol, created_at FROM ai_analysis_cache ORDER BY created_at DESC LIMIT 10')
print('AI分析缓存记录:')
print('ID | 合约 | 创建时间')
print('-' * 60)
for row in cursor:
print(f'{row[0]} | {row[1]} | {row[2]}')
conn.close()
Loading…
Cancel
Save