const API_BASE = '/api/v1/futures'; let klineChart = null; let currentSymbol = null; let currentPeriod = '15'; let allFuturesData = []; let watchedSymbols = []; let currentDetailData = null; document.addEventListener('DOMContentLoaded', function() { addScoreGradient(); updateTime(); setInterval(updateTime, 1000); initEventListeners(); loadWatchedSymbols(); loadFuturesList(); }); function addScoreGradient() { const svg = document.querySelector('.score-ring svg'); if (svg && !svg.querySelector('defs')) { const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); gradient.setAttribute('id', 'scoreGradient'); gradient.setAttribute('x1', '0%'); gradient.setAttribute('y1', '0%'); gradient.setAttribute('x2', '100%'); gradient.setAttribute('y2', '0%'); const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); stop1.setAttribute('offset', '0%'); stop1.setAttribute('stop-color', '#ef4444'); const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); stop2.setAttribute('offset', '50%'); stop2.setAttribute('stop-color', '#f59e0b'); const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); stop3.setAttribute('offset', '100%'); stop3.setAttribute('stop-color', '#10b981'); gradient.appendChild(stop1); gradient.appendChild(stop2); gradient.appendChild(stop3); defs.appendChild(gradient); svg.insertBefore(defs, svg.firstChild); } } function updateTime() { const now = new Date(); const timeStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0'); document.getElementById('current-time').textContent = timeStr; } function initEventListeners() { const backBtn = document.getElementById('back-btn'); if (backBtn) backBtn.addEventListener('click', showListView); const themeToggle = document.getElementById('theme-toggle'); if (themeToggle) themeToggle.addEventListener('click', toggleTheme); const refreshAllBtn = document.getElementById('refresh-all-btn'); if (refreshAllBtn) refreshAllBtn.addEventListener('click', refreshAllSymbols); const aiAnalyzeAllBtn = document.getElementById('ai-analyze-all-btn'); if (aiAnalyzeAllBtn) aiAnalyzeAllBtn.addEventListener('click', analyzeAllSymbols); const refreshSymbolBtn = document.getElementById('refresh-symbol-btn'); if (refreshSymbolBtn) refreshSymbolBtn.addEventListener('click', function() { if (currentSymbol) { refreshSingleSymbol(currentSymbol); } }); const savedTheme = localStorage.getItem('futures-theme'); if (savedTheme === 'dark') { document.body.classList.remove('theme-minimal'); updateThemeIcon(false); } else { document.body.classList.add('theme-minimal'); updateThemeIcon(true); } document.querySelectorAll('.period-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); currentPeriod = this.dataset.period; loadKlineData(currentSymbol, currentPeriod); }); }); const searchInput = document.getElementById('search-input'); if (searchInput) searchInput.addEventListener('input', function() { filterFuturesList(this.value); }); document.querySelectorAll('.pill').forEach(tab => { if (tab.dataset.category) { tab.addEventListener('click', function() { document.querySelectorAll('.pill').forEach(t => t.classList.remove('active')); this.classList.add('active'); filterByCategory(this.dataset.category); }); } }); const sortSelect = document.getElementById('sort-select'); if (sortSelect) sortSelect.addEventListener('change', function() { sortFuturesList(this.value); }); document.querySelectorAll('.modal-overlay').forEach(modal => { modal.addEventListener('click', function(e) { if (e.target === this) { this.classList.remove('active'); } }); }); // 导航项点击事件 document.querySelectorAll('.nav-item[data-page]').forEach(navItem => { navItem.addEventListener('click', function(e) { e.preventDefault(); const page = this.dataset.page; document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active')); this.classList.add('active'); if (page === 'review') { showReviewView(); } else if (page === 'trade-review') { showTradeReviewView(); } else if (page === 'watched') { showWatchedView(); } else { showListView(); } }); }); } function closeModal(modalId) { document.getElementById(modalId).classList.remove('active'); } function showListView() { document.getElementById('list-view').classList.add('active'); document.getElementById('detail-view').classList.remove('active'); document.getElementById('review-view').classList.remove('active'); document.getElementById('watched-view').classList.remove('active'); document.getElementById('trade-review-view').classList.remove('active'); if (klineChart) { klineChart.dispose(); klineChart = null; } } function showWatchedView() { document.getElementById('list-view').classList.remove('active'); document.getElementById('detail-view').classList.remove('active'); document.getElementById('review-view').classList.remove('active'); document.getElementById('watched-view').classList.add('active'); document.getElementById('trade-review-view').classList.remove('active'); if (klineChart) { klineChart.dispose(); klineChart = null; } renderWatchedList(); } function showReviewView() { document.getElementById('list-view').classList.remove('active'); document.getElementById('detail-view').classList.remove('active'); document.getElementById('review-view').classList.add('active'); document.getElementById('watched-view').classList.remove('active'); document.getElementById('trade-review-view').classList.remove('active'); if (klineChart) { klineChart.dispose(); klineChart = null; } // 初始化复盘计划功能 initReviewPlan(); } function showTradeReviewView() { document.getElementById('list-view').classList.remove('active'); document.getElementById('detail-view').classList.remove('active'); document.getElementById('review-view').classList.remove('active'); document.getElementById('watched-view').classList.remove('active'); document.getElementById('trade-review-view').classList.add('active'); if (klineChart) { klineChart.dispose(); klineChart = null; } initTradeReview(); } async function showDetailView(symbol) { currentSymbol = symbol; document.getElementById('list-view').classList.remove('active'); document.getElementById('detail-view').classList.add('active'); // 1. 加载行情数据 loadFuturesDetail(symbol); loadKlineData(symbol, currentPeriod); // 2. 加载历史记录 await loadHistoryListForAnalysis(symbol); } async function loadHistoryListForAnalysis(symbol) { console.log(`[AI分析] 开始加载 ${symbol} 的历史分析记录...`); try { const response = await fetch(`${API_BASE}/ai-analysis/${symbol}/history?limit=20`); const data = await response.json(); console.log(`[AI分析] API响应:`, data); if (data.success) { renderHistoryList(data.data); // 3. 查找今天的最新分析记录 const today = new Date(); const todayStr = today.toISOString().split('T')[0]; console.log(`[AI分析] 查找日期: ${todayStr}`); console.log(`[AI分析] 历史记录数量: ${data.data?.length || 0}`); let todayRecord = null; if (data.data && data.data.length > 0) { for (const record of data.data) { const recordDate = new Date(record.analysis_time); const recordDateStr = recordDate.toISOString().split('T')[0]; console.log(`[AI分析] 检查记录: ${record.analysis_time} -> ${recordDateStr}`); if (recordDateStr === todayStr) { todayRecord = record; console.log(`[AI分析] ✓ 找到今天的记录!`); break; } } } // 4. 根据是否有今天的记录进行不同处理 if (todayRecord) { console.log(`[AI分析] 显示AI分析结果...`); currentAIAnalysis = { id: todayRecord.id, symbol: todayRecord.symbol, analysis_time: todayRecord.analysis_time, result: todayRecord.analysis_data }; displayAIAnalysisResult(currentAIAnalysis); syncAIToPanels(todayRecord.analysis_data); } else { console.log(`[AI分析] 没有找到今天的分析记录,显示占位符`); showAIAnalysisPlaceholder(); } } else { console.log(`[AI分析] API返回失败,显示占位符`); showAIAnalysisPlaceholder(); } } catch (error) { console.error('[AI分析] 加载历史记录失败:', error); showAIAnalysisPlaceholder(); } } function showAIAnalysisPlaceholder() { console.log('[AI分析] 显示占位符...'); const content = document.getElementById('ai-analysis-content'); console.log('[AI分析] ai-analysis-content元素:', content); if (!content) { console.error('[AI分析] 错误: 找不到ai-analysis-content元素!'); return; } content.innerHTML = `

点击"智能分析"按钮获取AI分析结果

`; console.log('[AI分析] 占位符已显示'); } async function loadWatchedSymbols() { try { const response = await fetch(`${API_BASE}/watched`); const data = await response.json(); if (data.success) { watchedSymbols = data.data.map(s => s.symbol); } } catch (error) { console.error('加载自选列表失败:', error); watchedSymbols = []; } } async function toggleWatch(symbol, name, event) { event.stopPropagation(); const isWatched = watchedSymbols.includes(symbol); try { if (isWatched) { const response = await fetch(`${API_BASE}/watched/${symbol}`, { method: 'DELETE' }); const data = await response.json(); if (data.success) { watchedSymbols = watchedSymbols.filter(s => s !== symbol); showToast('success', '已取消自选', `${symbol} 已从自选列表移除`); } } else { const response = await fetch(`${API_BASE}/watched`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol, name }) }); const data = await response.json(); if (data.success) { watchedSymbols.push(symbol); showToast('success', '已添加自选', `${name}(${symbol}) 已添加到自选列表`); } } // 重新渲染当前视图 const activePill = document.querySelector('.pill.active'); const category = activePill ? activePill.dataset.category : 'all'; // 如果当前视图不是自选页面,重新渲染主网格 if (category !== 'watched') { renderFuturesGrid(getCurrentFilteredData()); } } catch (error) { console.error('切换自选失败:', error); showToast('error', '操作失败', '网络错误,请稍后重试'); } } function toggleFav(symbol, name, event) { event.stopPropagation(); // 调用原有的toggleWatch函数处理API请求和数据更新 toggleWatch(symbol, name, event); // 如果当前在自选页面,刷新自选列表 const watchedView = document.getElementById('watched-view'); if (watchedView && watchedView.classList.contains('active')) { setTimeout(() => renderWatchedList(), 200); } } function getCurrentFilteredData() { const activePill = document.querySelector('.pill.active'); const category = activePill ? activePill.dataset.category : 'all'; return filterDataByCategory(allFuturesData, category); } function filterDataByCategory(data, category) { if (category === 'all') return data; const categoryMap = { 'energy': ['SC', 'FU', 'LU', 'BU', 'RU', 'NR'], 'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'AO', 'SI', 'LC', 'PS'], 'agriculture': ['M', 'RM', 'C', 'CS', 'A', 'B', 'Y', 'P', 'OI', 'CF', 'SR', 'AP', 'LH'], 'finance': ['IF', 'IC', 'IH', 'IM', 'T', 'TF', 'TS', 'TL'] }; const symbols = categoryMap[category] || []; return data.filter(item => { const symbolBase = item.symbol.replace(/[0-9]/g, '').toUpperCase(); return symbols.includes(symbolBase); }); } async function loadFuturesList() { try { console.log('正在加载品种列表...'); const response = await fetch(`${API_BASE}/list`); const data = await response.json(); console.log('品种列表响应:', data); if (data.success) { allFuturesData = data.data.map(item => ({ ...item, hasAIAnalysis: false // 默认没有AI分析数据 })); console.log('加载的品种数据:', allFuturesData.length, '个'); renderFuturesGrid(allFuturesData); updateStats(allFuturesData); // 异步加载每个合约的最新AI分析结果 loadAllAIAnalysis(); } else { console.error('加载品种列表失败:', data); } } catch (error) { console.error('加载品种列表失败:', error); loadFuturesFromConfig(); } } async function loadAllAIAnalysis() { console.log('开始加载所有合约的AI分析结果...'); // 获取今天的日期字符串用于比较 const today = new Date(); const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD // 分批加载,避免并发请求过多 const batchSize = 5; for (let i = 0; i < allFuturesData.length; i += batchSize) { const batch = allFuturesData.slice(i, i + batchSize); const promises = batch.map(async (item) => { try { // 获取历史记录 const response = await fetch(`${API_BASE}/ai-analysis/${item.symbol}/history?limit=1`); const data = await response.json(); if (data.success && data.data && data.data.length > 0) { const latestRecord = data.data[0]; // 最新的一条记录 const analysisTime = latestRecord.analysis_time; // 判断是否是今天的记录 const recordDate = new Date(analysisTime); const recordDateStr = recordDate.toISOString().split('T')[0]; if (recordDateStr === todayStr) { // 是今天的记录,加载数据 const result = latestRecord.analysis_data; const analysisItem = allFuturesData.find(d => d.symbol === item.symbol); if (analysisItem) { analysisItem.hasAIAnalysis = true; analysisItem.aiResult = result; analysisItem.analysisTime = analysisTime; // 更新操作建议 if (result.trading_suggestion?.direction) { analysisItem.suggestion = result.trading_suggestion.direction; analysisItem.suggestionType = result.trading_suggestion.direction === '做多' ? 'up' : result.trading_suggestion.direction === '做空' ? 'down' : 'neutral'; } // 更新压力支撑位 if (result.pivot_points) { if (result.pivot_points.r1) analysisItem.resistance = result.pivot_points.r1; if (result.pivot_points.s1) analysisItem.support = result.pivot_points.s1; } // 更新多周期趋势 if (result.four_dimensional) { const periodMap = { '60min': '60', '30min': '30', '15min': '15', '5min': '5' }; analysisItem.periods = {}; Object.entries(result.four_dimensional).forEach(([period, pdata]) => { const periodNum = periodMap[period]; if (periodNum) { const trend = pdata.conclusion || pdata.macd?.trend || 'neutral'; analysisItem.periods[periodNum] = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral'; } }); } // 更新趋势评分(使用AI置信度) if (result.trading_suggestion?.confidence) { analysisItem.trendScore = result.trading_suggestion.confidence; } // 更新成功率(根据判断方向设置一致概率) const direction = analysisItem.suggestionType; if (direction === 'up') { analysisItem.successRate = 85; // 做多成功率 } else if (direction === 'down') { analysisItem.successRate = 82; // 做空成功率 } else { analysisItem.successRate = 60; // 观望成功率 } } } else { console.log(`${item.symbol} 的分析记录不是今天的 (${recordDateStr}),不加载`); } } else { console.log(`${item.symbol} 没有AI分析记录`); } } catch (error) { console.error(`加载 ${item.symbol} AI分析失败:`, error); } }); await Promise.all(promises); } // 所有批次加载完成后只渲染一次 renderFuturesGrid(allFuturesData); console.log('所有合约AI分析结果加载完成'); } async function loadFuturesFromConfig() { try { const response = await fetch('/api/v1/config'); const config = await response.json(); const futuresConfig = config.futures || {}; allFuturesData = Object.entries(futuresConfig).map(([name, symbol]) => ({ symbol: symbol, name: name, price: 0, change: 0, changePct: 0, suggestion: '等待数据', suggestionType: 'neutral', periods: { '5': 'neutral', '15': 'neutral', '30': 'neutral', '60': 'neutral' }, successRate: 0, trendScore: 0, resistance: 0, support: 0, open: 0, high: 0, low: 0, volume: 0 })); renderFuturesGrid(allFuturesData); updateStats(allFuturesData); } catch (error) { console.error('加载配置失败:', error); const mockData = generateMockFuturesData(); allFuturesData = mockData; renderFuturesGrid(mockData); updateStats(mockData); } } function generateMockFuturesData() { return [ { symbol: 'SC2606', name: '原油', price: 528.6, change: 12.1, changePct: 2.35, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'neutral' }, successRate: 72, trendScore: 85, resistance: 535, support: 518, open: 516.5, high: 530, low: 515, volume: 78000 }, { symbol: 'AU2606', name: '黄金', price: 685.2, change: 12.45, changePct: 1.85, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, successRate: 78, trendScore: 92, resistance: 692, support: 678, open: 672.75, high: 688, low: 670, volume: 128000 }, { symbol: 'AG2606', name: '白银', price: 8250, change: 165, changePct: 2.04, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, successRate: 75, trendScore: 88, resistance: 8350, support: 8100, open: 8085, high: 8280, low: 8050, volume: 95000 }, { symbol: 'CU2606', name: '沪铜', price: 80610, change: 112, changePct: 0.14, suggestion: '观望等待', suggestionType: 'neutral', periods: { '5': 'neutral', '15': 'up', '30': 'neutral', '60': 'up' }, successRate: 58, trendScore: 65, resistance: 81200, support: 79800, open: 80498, high: 80850, low: 80200, volume: 42000 }, { symbol: 'M2609', name: '豆粕', price: 2985, change: -51, changePct: -1.68, suggestion: '逢高做空', suggestionType: 'down', periods: { '5': 'down', '15': 'down', '30': 'down', '60': 'neutral' }, successRate: 65, trendScore: 35, resistance: 3050, support: 2920, open: 3036, high: 3040, low: 2980, volume: 185000 } ]; } function renderFuturesGrid(data) { const grid = document.getElementById('futures-grid'); console.log('渲染品种网格,数据量:', data.length); if (data.length === 0) { grid.innerHTML = '
暂无数据
'; console.log('显示暂无数据'); return; } grid.innerHTML = data.map(item => { const code = item.symbol.replace(/[0-9]/g, '').substring(0, 2); const changeIcon = item.change >= 0 ? '↑' : '↓'; const changeSign = item.change >= 0 ? '+' : ''; const pctSign = item.changePct >= 0 ? '+' : ''; let actionPillClass = 'watch'; let actionPillText = '观望'; if (item.suggestion?.includes('做多') || item.suggestion?.includes('试多')) { actionPillClass = 'do-more'; actionPillText = '做多'; } else if (item.suggestion?.includes('做空') || item.suggestion?.includes('试空')) { actionPillClass = 'do-more'; actionPillText = '做空'; } const priceClass = item.change >= 0 ? 'up' : 'down'; const changeClass = item.change >= 0 ? 'up' : 'down'; const successRate = item.successRate || 0; const trendScore = item.trendScore || 0; const successColor = successRate >= 70 ? 'var(--color-down)' : successRate >= 60 ? 'var(--color-neutral)' : 'var(--color-up)'; const trendColor = trendScore >= 70 ? 'var(--color-down)' : trendScore >= 50 ? 'var(--color-neutral)' : 'var(--color-up)'; const periods = item.periods || {}; const resistance = item.resistance ? formatNumber(item.resistance) : '--'; const support = item.support ? formatNumber(item.support) : '--'; const isWatched = watchedSymbols.includes(item.symbol); const watchIcon = isWatched ? 'fa-star' : 'fa-star-o'; const watchClass = isWatched ? 'active' : ''; return `
${code}
${item.name}
${item.symbol}
¥${formatNumber(item.price)}
${changeIcon} ${changeSign}${formatNumber(item.change)} (${pctSign}${item.changePct.toFixed(2)}%)
${actionPillText}
成功率 ${successRate}%
趋势评分 ${trendScore}
5M
15M
30M
1H
`}).join(''); } // ==================== 交易反思子系统 ==================== let trReflCurrentDate = null; let trReflData = null; let trReflTags = []; let trReflExperiences = []; let trReflTagState = new Set(); let trExpPage = 1; let trExpPageSize = 12; let trExpTotal = 0; let trReflCurrentPairId = null; let trReflCurrentReflectionId = null; let trReflCurrentAIResult = null; let trReflAIHistory = []; let trReflCurrentAnalysis = null; function trSwitchSubtab(tab) { document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => { btn.classList.toggle('active', btn.dataset.subtab === tab); }); document.querySelectorAll('#trade-review-view > .tr-tab-panel').forEach(panel => { panel.classList.toggle('active', panel.id === 'tr-tab-' + tab); }); if (tab === 'experience') trExpLoad(); if (tab === 'data') trLoadDataOverview(); } function trSwitchToReflection(date) { trSwitchSubtab('reflection'); trReflSetDate(date); } function trReflInit() { const dateInput = document.getElementById('tr-refl-current-date'); if (!dateInput) return; // 默认日期取统计分析的最新交易日或今天 const today = new Date().toISOString().slice(0, 10); trReflSetDate(today); document.getElementById('tr-refl-prev').addEventListener('click', () => trReflChangeDate(-1)); document.getElementById('tr-refl-next').addEventListener('click', () => trReflChangeDate(1)); document.getElementById('tr-refl-edit-daily').addEventListener('click', trReflOpenDailyModal); document.getElementById('tr-refl-edit-form').addEventListener('submit', e => { e.preventDefault(); trReflSaveEdit(); }); document.getElementById('tr-refl-daily-form').addEventListener('submit', e => { e.preventDefault(); trReflSaveDaily(); }); const saveAndAiBtn = document.getElementById('tr-refl-save-and-ai'); if (saveAndAiBtn) { saveAndAiBtn.addEventListener('click', e => { e.preventDefault(); trReflSaveAndAnalyze(); }); } const manualPairBtn = document.getElementById('tr-refl-manual-pair-btn'); if (manualPairBtn) manualPairBtn.addEventListener('click', trReflManualPair); const selectAll = document.getElementById('tr-refl-unpaired-select-all'); if (selectAll) { selectAll.addEventListener('change', function() { document.querySelectorAll('.tr-refl-unpaired-check').forEach(cb => cb.checked = this.checked); }); } document.querySelectorAll('#tr-refl-discipline-score .tr-form-star, #tr-refl-daily-discipline-score .tr-form-star').forEach(star => { star.addEventListener('click', () => { const container = star.parentElement; const score = parseInt(star.dataset.score); container.dataset.value = score; trReflUpdateStars(container, score); }); }); document.getElementById('tr-exp-search').addEventListener('input', () => { trExpPage = 1; trExpLoad(); }); document.getElementById('tr-exp-tag-filter').addEventListener('change', () => { trExpPage = 1; trExpLoad(); }); document.getElementById('tr-exp-type-filter').addEventListener('change', () => { trExpPage = 1; trExpLoad(); }); document.getElementById('tr-exp-prev').addEventListener('click', () => { if (trExpPage > 1) { trExpPage--; trExpLoad(); } }); document.getElementById('tr-exp-next').addEventListener('click', () => { if (trExpPage * trExpPageSize < trExpTotal) { trExpPage++; trExpLoad(); } }); document.querySelectorAll('.tr-exp-viewtab').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.tr-exp-viewtab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); trExpRender(); }); }); trReflLoadTags(); } function trReflSetDate(date) { trReflCurrentDate = date; document.getElementById('tr-refl-current-date').textContent = date; trReflLoadData(); } function trReflChangeDate(days) { const d = new Date(trReflCurrentDate); d.setDate(d.getDate() + days); trReflSetDate(d.toISOString().slice(0, 10)); } async function trReflLoadData() { if (!trReflCurrentDate) return; try { const res = await fetch(`${TR_API_BASE}/daily-trades/${trReflCurrentDate}`); const json = await res.json(); trReflData = json.success ? json.data : null; if (!trReflData) trReflData = { pairs: [], unpaired: [], daily_reflection: null, summary: {} }; trReflRender(); } catch (e) { console.error('加载反思数据失败', e); trReflData = { pairs: [], unpaired: [], daily_reflection: null, summary: {} }; trReflRender(); } } function trReflRender() { const data = trReflData || { pairs: [], unpaired: [], daily_reflection: null, summary: {} }; const summary = data.summary || {}; const daily = data.daily_reflection || {}; const pnl = summary.total_pnl || 0; const pnlEl = document.getElementById('tr-refl-stat-pnl'); pnlEl.textContent = (pnl >= 0 ? '+' : '') + pnl.toFixed(2); pnlEl.className = 'tr-refl-statvalue ' + (pnl >= 0 ? 'profit' : 'loss'); document.getElementById('tr-refl-stat-trades').textContent = summary.total_trades || 0; document.getElementById('tr-refl-stat-winrate').textContent = (summary.win_rate || 0) + '%'; const reflectedEl = document.getElementById('tr-refl-stat-reflected'); const reflected = summary.reflected_count || 0; const totalPairs = (data.pairs || []).length; if (totalPairs === 0) { reflectedEl.innerHTML = '无交易'; } else if (reflected >= totalPairs) { reflectedEl.innerHTML = '✓ 已完成'; } else { reflectedEl.innerHTML = `${reflected}/${totalPairs}`; } document.getElementById('tr-refl-emotion').textContent = trReflTextMap.emotion[daily.emotion_state] || daily.emotion_state || '未填写'; document.getElementById('tr-refl-emotion').className = 'tr-refl-daily-value ' + (daily.emotion_state ? '' : 'empty'); document.getElementById('tr-refl-discipline').textContent = daily.discipline_score ? daily.discipline_score + '/5' : '未填写'; document.getElementById('tr-refl-discipline').className = 'tr-refl-daily-value ' + (daily.discipline_score ? '' : 'empty'); document.getElementById('tr-refl-overall').textContent = trReflTextMap.overall[daily.overall_rating] || daily.overall_rating || '未填写'; document.getElementById('tr-refl-overall').className = 'tr-refl-daily-value ' + (daily.overall_rating ? '' : 'empty'); document.getElementById('tr-refl-market').textContent = trReflTextMap.market[daily.market_judgment] || daily.market_judgment || '未填写'; document.getElementById('tr-refl-market').className = 'tr-refl-daily-value ' + (daily.market_judgment ? '' : 'empty'); const pairsContainer = document.getElementById('tr-refl-pairs'); const pairs = data.pairs || []; if (!pairs.length) { pairsContainer.innerHTML = '
📝
当日无交易配对
'; } else { pairsContainer.innerHTML = pairs.map(p => trReflRenderPairCard(p)).join(''); } const unpairedBody = document.getElementById('tr-refl-unpaired-body'); const unpairedEmpty = document.getElementById('tr-refl-unpaired-empty'); const selectAll = document.getElementById('tr-refl-unpaired-select-all'); const unpaired = data.unpaired || []; if (!unpaired.length) { unpairedBody.innerHTML = ''; unpairedEmpty.style.display = 'flex'; if (selectAll) selectAll.checked = false; } else { unpairedEmpty.style.display = 'none'; unpairedBody.innerHTML = unpaired.map(u => { const pnl = u.net_pnl || u.close_pnl || 0; const pnlCls = pnl > 0 ? 'tr-pnl-positive' : (pnl < 0 ? 'tr-pnl-negative' : ''); return ` ${u.trade_time || '-'} ${u.variety}${u.symbol_name ? '(' + u.symbol_name + ')' : ''} ${u.offset || '-'} ${u.direction || '-'} ${(u.price || 0).toFixed(2)} ${u.volume || 0} ${pnl !== 0 ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) : '-'} `; }).join(''); } } async function trReflManualPair() { const checked = Array.from(document.querySelectorAll('.tr-refl-unpaired-check:checked')); if (!checked.length) { showToast('warning', '请选择记录', '请至少选择一条未配对交易记录'); return; } const openRecords = checked.filter(cb => cb.dataset.offset === '开'); const closeRecords = checked.filter(cb => cb.dataset.offset === '平'); if (!openRecords.length) { showToast('warning', '缺少开仓记录', '手动配对需要至少一条开仓记录(开)'); return; } if (!closeRecords.length) { showToast('warning', '缺少平仓记录', '手动配对需要至少一条平仓记录(平)'); return; } const symbols = new Set(checked.map(cb => cb.dataset.symbol)); if (symbols.size > 1) { showToast('warning', '品种不一致', '请选择同一品种的开仓和平仓记录'); return; } const openDirection = openRecords[0].dataset.direction; const closeDirection = closeRecords[0].dataset.direction; // 多头:开=买,平=卖;空头:开=卖,平=买 const isLong = openDirection === '买' && closeDirection === '卖'; const isShort = openDirection === '卖' && closeDirection === '买'; if (!isLong && !isShort) { showToast('warning', '方向不匹配', '多头配对:开仓为买、平仓为卖;空头配对:开仓为卖、平仓为买'); return; } const openRecordIds = openRecords.map(cb => parseInt(cb.dataset.id)); const closeRecordIds = closeRecords.map(cb => parseInt(cb.dataset.id)); try { const res = await fetch(`${TR_API_BASE}/trade-pairs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ open_record_ids: openRecordIds, close_record_ids: closeRecordIds }), }); const json = await res.json(); if (json.success) { showToast('success', '配对成功', '交易配对已创建'); trReflLoadData(); } else { showToast('error', '配对失败', json.message || '请重试'); } } catch (e) { showToast('error', '配对失败', e.message); } } function trReflRenderPairCard(p) { const pnl = p.net_pnl || 0; const pnlCls = pnl >= 0 ? 'profit' : 'loss'; const dirCls = p.direction === '多' ? 'long' : 'short'; const dirText = p.direction === '多' ? '做多' : '做空'; const tags = (p.tags || []).map(t => `${typeof t === 'string' ? t : t.name}`).join(''); const hasReflection = p.reflection_status === 'done'; return `
${p.symbol_name || p.variety || p.symbol || '-'}
${dirText}
${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}
开仓:${p.open_date || '-'} ${p.open_time || ''} @ ${(p.open_price || 0).toFixed(2)}
平仓:${p.close_date || '-'} ${p.close_time || ''} @ ${(p.close_price || 0).toFixed(2)}
${tags}
`; } async function trReflLoadTags() { try { const res = await fetch(`${TR_API_BASE}/tags`); const json = await res.json(); if (json.success) trReflTags = json.data || []; } catch (e) { console.error('加载标签失败', e); } } // ===== 反思编辑 Modal ===== async function trReflOpenEditModal(pairId) { const data = trReflData || { pairs: [] }; const pair = data.pairs.find(p => p.id === pairId); if (!pair) return; trReflCurrentPairId = pairId; trReflCurrentReflectionId = null; document.getElementById('tr-refl-edit-pair-id').value = pairId; let reflection = {}; let selectedTagNames = new Set(); try { const [reflRes, tagsRes] = await Promise.all([ fetch(`${TR_API_BASE}/reflections?trade_pair_id=${pairId}`), fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`), ]); const reflJson = await reflRes.json(); const tagsJson = await tagsRes.json(); if (reflJson.success && reflJson.data && reflJson.data.length) { reflection = reflJson.data[0]; trReflCurrentReflectionId = reflection.id || null; } if (tagsJson.success && tagsJson.data) { selectedTagNames = new Set(tagsJson.data.map(t => typeof t === 'string' ? t : t.name)); } } catch (e) { console.error('加载反思/标签失败', e); } document.getElementById('tr-refl-entry-reason').value = reflection.entry_reason || ''; document.getElementById('tr-refl-exit-reason').value = reflection.exit_reason || ''; document.getElementById('tr-refl-entry-timing').value = reflection.entry_timing || ''; document.getElementById('tr-refl-exit-timing').value = reflection.exit_timing || ''; document.getElementById('tr-refl-position').value = reflection.position_management || ''; document.getElementById('tr-refl-free').value = reflection.free_reflection || ''; const score = reflection.discipline_score || 0; const container = document.getElementById('tr-refl-discipline-score'); container.dataset.value = score; trReflUpdateStars(container, score); // 渲染标签选择 const tagPool = document.getElementById('tr-refl-edit-tags'); tagPool.innerHTML = trReflTags.map(t => { const name = typeof t === 'string' ? t : t.name; const active = selectedTagNames.has(name) ? 'selected' : ''; return `${name}`; }).join(''); document.getElementById('tr-refl-edit-modal').classList.add('show'); } function trReflCloseEditModal() { document.getElementById('tr-refl-edit-modal').classList.remove('show'); trReflCurrentPairId = null; trReflCurrentReflectionId = null; } async function trReflSaveEdit(andAnalyze = false) { const pairId = parseInt(document.getElementById('tr-refl-edit-pair-id').value); if (!pairId) return; const body = { trade_pair_id: pairId, trade_date: trReflCurrentDate, entry_reason: document.getElementById('tr-refl-entry-reason').value, exit_reason: document.getElementById('tr-refl-exit-reason').value, entry_timing: document.getElementById('tr-refl-entry-timing').value, exit_timing: document.getElementById('tr-refl-exit-timing').value, position_management: document.getElementById('tr-refl-position').value, discipline_score: parseInt(document.getElementById('tr-refl-discipline-score').dataset.value || '0'), free_reflection: document.getElementById('tr-refl-free').value, }; try { const url = trReflCurrentReflectionId ? `${TR_API_BASE}/reflections/${trReflCurrentReflectionId}` : `${TR_API_BASE}/reflections`; const method = trReflCurrentReflectionId ? 'PUT' : 'POST'; const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); if (!json.success) { showToast('error', '保存失败', json.message || '请重试'); return; } // 同步标签(编辑 Modal 中的标签选择) const selectedTagNames = Array.from(document.querySelectorAll('#tr-refl-edit-tags .tr-tag-chip.selected')).map(el => el.textContent); try { await trReflSyncPairTags(pairId, selectedTagNames); } catch (e) { console.error('标签同步失败', e); } if (andAnalyze) { try { const reRes = await fetch(`${TR_API_BASE}/reanalyze`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ trade_pair_id: pairId }), }); const reJson = await reRes.json(); if (reJson.success && reJson.data) { // 重新分析接口不返回 id,需要再取一次 latest 获取完整记录 const latestRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`); const latestJson = await latestRes.json(); if (latestJson.success && latestJson.data) { trReflCurrentAIResult = latestJson.data; } else { trReflCurrentAIResult = reJson.data; } trReflCloseEditModal(); trReflOpenAIModal(pairId, false); return; } showToast('error', 'AI分析失败', reJson.message || '请重试'); return; } catch (e) { showToast('error', 'AI分析失败', e.message); return; } } showToast('success', '保存成功', '反思已保存'); trReflCloseEditModal(); trReflLoadData(); } catch (e) { showToast('error', '保存失败', e.message); } } async function trReflSaveAndAnalyze() { await trReflSaveEdit(true); } async function trReflSyncPairTags(pairId, selectedNames) { const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`); const json = await res.json(); const currentTags = (json.success ? json.data : []) || []; const currentNames = new Set(currentTags.map(t => typeof t === 'string' ? t : t.name)); // 新增标签 for (const name of selectedNames) { if (currentNames.has(name)) continue; const tag = trReflTags.find(t => (typeof t === 'string' ? t : t.name) === name); if (!tag || typeof tag === 'string') continue; await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags?tag_id=${tag.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); } // 移除标签 for (const tag of currentTags) { const name = typeof tag === 'string' ? tag : tag.name; if (selectedNames.includes(name)) continue; const tagId = typeof tag === 'string' ? null : tag.id; if (!tagId) continue; await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags/${tagId}`, { method: 'DELETE' }); } } // ===== 当日反思 Modal ===== function trReflOpenDailyModal() { const daily = (trReflData && trReflData.daily_reflection) || {}; document.getElementById('tr-refl-daily-emotion').value = daily.emotion_state || ''; document.getElementById('tr-refl-daily-overall').value = daily.overall_rating || ''; document.getElementById('tr-refl-daily-market').value = daily.market_judgment || ''; document.getElementById('tr-refl-daily-summary').value = daily.summary || ''; document.getElementById('tr-refl-daily-improvement').value = daily.improvements || ''; const score = daily.discipline_score || 0; const container = document.getElementById('tr-refl-daily-discipline-score'); container.dataset.value = score; trReflUpdateStars(container, score); document.getElementById('tr-refl-daily-modal').classList.add('show'); } function trReflCloseDailyModal() { document.getElementById('tr-refl-daily-modal').classList.remove('show'); } async function trReflSaveDaily() { const body = { reflection_date: trReflCurrentDate, emotion_state: document.getElementById('tr-refl-daily-emotion').value, overall_rating: document.getElementById('tr-refl-daily-overall').value, market_judgment: document.getElementById('tr-refl-daily-market').value, discipline_score: parseInt(document.getElementById('tr-refl-daily-discipline-score').dataset.value || '0'), summary: document.getElementById('tr-refl-daily-summary').value, improvements: document.getElementById('tr-refl-daily-improvement').value, }; try { const res = await fetch(`${TR_API_BASE}/daily-reflections`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); if (json.success) { showToast('success', '保存成功', '当日反思已保存'); trReflCloseDailyModal(); trReflLoadData(); } else { showToast('error', '保存失败', json.message || '请重试'); } } catch (e) { showToast('error', '保存失败', e.message); } } // ===== 标签管理 Modal ===== async function trReflOpenTagsModal(pairId) { const data = trReflData || { pairs: [] }; const pair = data.pairs.find(p => p.id === pairId); if (!pair) return; trReflCurrentPairId = pairId; trReflTagState = new Set(); try { const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`); const json = await res.json(); if (json.success && json.data) { trReflTagState = new Set(json.data.map(t => typeof t === 'string' ? t : t.name)); } } catch (e) { console.error('加载交易标签失败', e); } document.getElementById('tr-refl-tags-pair-id').value = pairId; trReflRenderTagPreset(); document.getElementById('tr-refl-tags-modal').classList.add('show'); } function trReflRenderTagPreset() { const container = document.getElementById('tr-refl-tags-preset'); const grouped = trReflGroupTags(trReflTags); container.innerHTML = Object.entries(grouped).map(([category, tags]) => `
${category}
${tags.map(t => { const name = typeof t === 'string' ? t : t.name; const selected = trReflTagState.has(name) ? 'selected' : ''; return `${name}`; }).join('')}
`).join(''); } function trReflGroupTags(tags) { const groups = {}; tags.forEach(t => { const category = (typeof t === 'object' && t.category) ? t.category : '常用'; if (!groups[category]) groups[category] = []; groups[category].push(t); }); return groups; } function trReflToggleTag(tag) { if (trReflTagState.has(tag)) trReflTagState.delete(tag); else trReflTagState.add(tag); trReflRenderTagPreset(); } function trReflAddCustomTag() { const input = document.getElementById('tr-refl-tag-input'); const val = input.value.trim(); if (!val) return; trReflTagState.add(val); input.value = ''; trReflRenderTagPreset(); } async function trReflResolveTagId(name) { const existing = trReflTags.find(t => (typeof t === 'string' ? t : t.name) === name); if (existing && typeof existing === 'object' && existing.id) return existing.id; // 对自定义标签创建后再使用 try { const res = await fetch(`${TR_API_BASE}/tags`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, category: 'custom' }), }); const json = await res.json(); if (json.success && json.data && json.data.id) { trReflTags.push({ id: json.data.id, name, category: 'custom', is_preset: false }); return json.data.id; } } catch (e) { console.error('创建标签失败', e); } return null; } async function trReflSaveTags() { const pairId = parseInt(document.getElementById('tr-refl-tags-pair-id').value); if (!pairId) return; const selectedNames = Array.from(trReflTagState); try { const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`); const json = await res.json(); const currentTags = (json.success ? json.data : []) || []; const currentMap = new Map(currentTags.map(t => [ typeof t === 'string' ? t : t.name, typeof t === 'string' ? null : t.id, ])); // 新增标签 for (const name of selectedNames) { if (currentMap.has(name)) continue; const tagId = await trReflResolveTagId(name); if (!tagId) continue; await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags?tag_id=${tagId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); } // 移除标签 for (const [name, tagId] of currentMap) { if (selectedNames.includes(name) || !tagId) continue; await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags/${tagId}`, { method: 'DELETE' }); } showToast('success', '保存成功', '标签已更新'); trReflCloseTagsModal(); trReflLoadData(); } catch (e) { showToast('error', '保存失败', e.message); } } function trReflCloseTagsModal() { document.getElementById('tr-refl-tags-modal').classList.remove('show'); trReflCurrentPairId = null; } // ===== AI 分析结果 Modal ===== async function trReflOpenAIModal(pairId, fetchLatest = true) { const data = trReflData || { pairs: [] }; const pair = data.pairs.find(p => p.id === pairId); if (!pair) return; trReflCurrentPairId = pairId; trReflAIHistory = []; trReflCurrentAnalysis = null; // 加载历史版本列表 try { const historyRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/history`); const historyJson = await historyRes.json(); if (historyJson.success && historyJson.data) { trReflAIHistory = Array.isArray(historyJson.data) ? historyJson.data : (historyJson.data.items || []); } } catch (e) { console.error('获取AI分析历史失败', e); } if (fetchLatest) { trReflCurrentAIResult = null; try { const res = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`); const json = await res.json(); if (json.success && json.data) { trReflCurrentAIResult = json.data; } } catch (e) { console.error('获取最新AI分析失败', e); } // 没有最新结果时,自动基于反思重新分析 if (!trReflCurrentAIResult) { try { const reRes = await fetch(`${TR_API_BASE}/reanalyze`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ trade_pair_id: pairId }), }); const reJson = await reRes.json(); if (reJson.success && reJson.data) { const latestRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`); const latestJson = await latestRes.json(); if (latestJson.success && latestJson.data) { trReflCurrentAIResult = latestJson.data; } else { trReflCurrentAIResult = reJson.data; } } } catch (e) { console.error('AI重新分析失败', e); } } // 如果历史列表为空但拿到最新结果,补充进历史列表 if (!trReflAIHistory.length && trReflCurrentAIResult) { trReflAIHistory = [trReflCurrentAIResult]; } } // 优先从历史列表中选中与当前结果匹配的版本,否则默认第一个 let selected = trReflAIHistory[0] || trReflCurrentAIResult || null; if (trReflCurrentAIResult && trReflCurrentAIResult.id && trReflAIHistory.length) { const matched = trReflAIHistory.find(a => a.id === trReflCurrentAIResult.id); if (matched) selected = matched; } trReflCurrentAnalysis = selected; trReflRenderAIVersionSelect(); trReflDisplayAIAnalysis(trReflCurrentAnalysis); document.getElementById('tr-refl-ai-pair-id').value = pairId; document.getElementById('tr-refl-ai-modal').classList.add('show'); } function trReflRenderAIVersionSelect() { const select = document.getElementById('tr-refl-ai-version'); if (!select) return; if (!trReflAIHistory.length) { select.innerHTML = ''; select.disabled = true; return; } select.disabled = false; select.innerHTML = trReflAIHistory.map((a, idx) => { const version = a.version || (trReflAIHistory.length - idx); const created = a.created_at ? a.created_at.slice(0, 19).replace('T', ' ') : ''; const selected = (trReflCurrentAnalysis && trReflCurrentAnalysis.id === a.id) ? 'selected' : ''; return ``; }).join(''); select.onchange = trReflSwitchAIVersion; } function trReflSwitchAIVersion() { const select = document.getElementById('tr-refl-ai-version'); if (!select) return; const value = select.value; const analysis = trReflAIHistory.find(a => String(a.id) === value); if (!analysis) return; trReflCurrentAnalysis = analysis; trReflDisplayAIAnalysis(analysis); } function trReflDisplayAIAnalysis(analysis) { const result = analysis || {}; document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果(请先保存反思)'; document.getElementById('tr-refl-ai-strengths').innerHTML = (result.strengths || []).map(s => `
  • ${s}
  • `).join('') || '
  • -
  • '; document.getElementById('tr-refl-ai-weaknesses').innerHTML = (result.weaknesses || []).map(s => `
  • ${s}
  • `).join('') || '
  • -
  • '; document.getElementById('tr-refl-ai-suggestion').textContent = result.experience_suggestion || result.suggestion || '暂无建议'; } async function trReflSaveExperience() { const pairId = parseInt(document.getElementById('tr-refl-ai-pair-id').value); const data = trReflData || { pairs: [] }; const pair = data.pairs.find(p => p.id === pairId); const result = trReflCurrentAnalysis || trReflCurrentAIResult; if (!pair || !result) return; const analysisId = result.id; if (!analysisId) { showToast('error', '保存失败', '缺少分析记录ID'); return; } const body = { title: `${pair.symbol_name || pair.variety || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 经验`, content: result.overall_evaluation || result.analysis || result.experience_suggestion || '', tags: ['AI分析'], }; try { const res = await fetch(`${TR_API_BASE}/ai-analysis/${analysisId}/save-experience`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const json = await res.json(); if (json.success) { showToast('success', '保存成功', '已保存到经验库'); trReflCloseAIModal(); } else { showToast('error', '保存失败', json.message || '请重试'); } } catch (e) { showToast('error', '保存失败', e.message); } } function trReflCloseAIModal() { document.getElementById('tr-refl-ai-modal').classList.remove('show'); trReflCurrentPairId = null; trReflCurrentAIResult = null; trReflCurrentAnalysis = null; trReflAIHistory = []; } // ===== 经验库 ===== async function trExpLoad() { try { const params = new URLSearchParams(); params.set('page', trExpPage); params.set('page_size', trExpPageSize); const keyword = document.getElementById('tr-exp-search').value.trim(); const tag = document.getElementById('tr-exp-tag-filter').value; const type = document.getElementById('tr-exp-type-filter').value; if (keyword) params.set('keyword', keyword); if (tag) params.set('tag', tag); if (type) params.set('type', type); const res = await fetch(`${TR_API_BASE}/experiences?${params.toString()}`); const json = await res.json(); if (json.success && json.data) { trReflExperiences = json.data.items || json.data || []; trExpTotal = json.data.total || trReflExperiences.length; } else { trReflExperiences = []; trExpTotal = 0; } trExpPopulateTagFilter(); trExpRender(); } catch (e) { console.error('加载经验库失败', e); trReflExperiences = []; trExpTotal = 0; trExpRender(); } } function trExpPopulateTagFilter() { const select = document.getElementById('tr-exp-tag-filter'); const allTags = new Set(); trReflExperiences.forEach(e => (e.tags || []).forEach(t => allTags.add(typeof t === 'string' ? t : t.name))); const current = select.value; select.innerHTML = '' + Array.from(allTags).sort().map(t => ``).join(''); select.value = current; } function trExpRender() { const container = document.getElementById('tr-exp-container'); const empty = document.getElementById('tr-exp-empty'); const pagination = document.getElementById('tr-exp-pagination'); const pageInfo = document.getElementById('tr-exp-pageinfo'); const view = document.querySelector('.tr-exp-viewtab.active').dataset.view; const list = trReflExperiences; if (!list.length) { container.innerHTML = ''; container.className = 'tr-exp-grid'; empty.style.display = 'flex'; if (pagination) pagination.style.display = 'none'; return; } empty.style.display = 'none'; const totalPages = Math.max(1, Math.ceil(trExpTotal / trExpPageSize)); if (pagination) { pagination.style.display = 'flex'; pageInfo.textContent = `第 ${trExpPage} / ${totalPages} 页,共 ${trExpTotal} 条`; document.getElementById('tr-exp-prev').disabled = trExpPage <= 1; document.getElementById('tr-exp-next').disabled = trExpPage >= totalPages; } if (view === 'timeline') { container.className = ''; const sorted = [...list].sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); container.innerHTML = `
    ${sorted.map(e => trExpRenderCard(e, true)).join('')}
    `; } else if (view === 'category') { container.className = ''; const grouped = {}; list.forEach(e => { const k = e.type || '其他'; if (!grouped[k]) grouped[k] = []; grouped[k].push(e); }); const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' }; container.innerHTML = Object.entries(grouped).map(([k, items]) => `
    ${typeNames[k] || k}
    ${items.map(e => trExpRenderCard(e)).join('')}
    `).join(''); } else { container.className = 'tr-exp-grid'; container.innerHTML = list.map(e => trExpRenderCard(e)).join(''); } } function trExpRenderCard(e, timeline = false) { const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' }; const tags = (e.tags || []).map(t => `${typeof t === 'string' ? t : t.name}`).join(''); const date = e.created_at ? e.created_at.slice(0, 10) : ''; return `
    ${e.title || '未命名经验'}
    ${typeNames[e.type] || e.type || '其他'} ${date ? '· ' + date : ''}
    ${(e.content || '').slice(0, 120)}${(e.content || '').length > 120 ? '...' : ''}
    ${tags}
    `; } function trExpOpenDetailModal(id) { const e = trReflExperiences.find(item => item.id === id); if (!e) return; const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' }; const tags = (e.tags || []).map(t => `${typeof t === 'string' ? t : t.name}`).join(''); const created = e.created_at ? e.created_at.slice(0, 19).replace('T', ' ') : '-'; const sourceDate = e.source_trade_date || (e.source_pair ? e.source_pair.trade_date : ''); document.getElementById('tr-exp-detail-title').textContent = e.title || '经验详情'; document.getElementById('tr-exp-detail-meta').textContent = `${typeNames[e.type] || e.type || '其他'} · 创建于 ${created}${sourceDate ? ' · 来源交易日期 ' + sourceDate : ''}`; document.getElementById('tr-exp-detail-content').textContent = e.content || ''; document.getElementById('tr-exp-detail-tags').innerHTML = tags || '无标签'; const sourceEl = document.getElementById('tr-exp-detail-source'); if (e.source_pair_id) { sourceEl.innerHTML = `查看原交易`; } else { sourceEl.innerHTML = ''; } document.getElementById('tr-exp-detail-modal').classList.add('show'); } function trExpCloseDetailModal() { document.getElementById('tr-exp-detail-modal').classList.remove('show'); } function trExpViewSourcePair(pairId) { console.log('查看原交易配对:', pairId); alert(`查看原交易配对:${pairId}`); } // ===== 星级评分辅助 ===== function trReflUpdateStars(container, score) { container.querySelectorAll('.tr-form-star').forEach(s => { s.classList.toggle('active', parseInt(s.dataset.score) <= score); }); } // ===== 文本映射 ===== const trReflTextMap = { emotion: { calm: '平静', confident: '自信', anxious: '焦虑', greedy: '贪婪', fearful: '恐惧', impulsive: '冲动', tired: '疲惫' }, overall: { excellent: '优秀', good: '良好', average: '一般', poor: '较差', bad: '糟糕' }, market: { accurate: '准确', basically: '基本准确', deviated: '有所偏差', wrong: '错误' }, timing: { excellent: '优秀', good: '良好', average: '一般', poor: '较差', bad: '糟糕' } }; // ==================== 交易复盘功能 ==================== const TR_API_BASE = '/api/v1/trade-review'; let trInitialized = false; let trAllPairs = []; let trVarietyKlineChart = null; let trVarietyCurrentPeriod = 'daily'; let trVarietyCurrentCode = null; // 图表实例 let trEquityChart = null; let trBubbleChart = null; let trDailyVarChart = null; let trDetailChart = null; let trCurrentPair = null; let trCurrentPeriod = 'daily'; function initTradeReview() { if (trInitialized) return; trInitialized = true; // 导入按钮 document.getElementById('tr-btn-import').addEventListener('click', () => { document.getElementById('tr-file-input').click(); }); document.getElementById('tr-file-input').addEventListener('change', trHandleFileImport); // 批量导入按钮 document.getElementById('tr-btn-batch-import').addEventListener('click', () => { document.getElementById('tr-batch-file-input').click(); }); document.getElementById('tr-batch-file-input').addEventListener('change', trHandleBatchImport); // 查询按钮 document.getElementById('tr-btn-query').addEventListener('click', () => { trLoadAllData(); }); // 删除当日按钮 document.getElementById('tr-btn-delete-date').addEventListener('click', trDeleteByDate); // 批次管理 document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal); // 子 Tab 切换 document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => { btn.addEventListener('click', () => trSwitchSubtab(btn.dataset.subtab)); }); // 初始加载 trInitDefaultDate(); // 初始化交易反思子系统 trReflInit(); } async function trInitDefaultDate() { try { const res = await fetch(`${TR_API_BASE}/latest-trade-date`); const json = await res.json(); if (json.success && json.data.trade_date) { const dateStr = json.data.trade_date; document.getElementById('tr-start-date').value = dateStr; document.getElementById('tr-end-date').value = dateStr; } } catch (e) { console.error('获取最后交易日失败', e); } // 初始化快捷日期按钮 trInitQuickDates(); trLoadAllData(); } function trInitQuickDates() { document.querySelectorAll('#tr-quick-dates .tr-quick-date').forEach(btn => { btn.addEventListener('click', () => trSetDateRange(btn.dataset.range)); }); } function trSetDateRange(range) { // 更新按钮状态 document.querySelectorAll('#tr-quick-dates .tr-quick-date').forEach(btn => { btn.classList.toggle('active', btn.dataset.range === range); }); const today = new Date(); const end = today.toISOString().split('T')[0]; let start = end; switch (range) { case 'today': start = end; break; case 'week': const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); start = weekAgo.toISOString().split('T')[0]; break; case 'half-month': const halfMonthAgo = new Date(today); halfMonthAgo.setDate(halfMonthAgo.getDate() - 15); start = halfMonthAgo.toISOString().split('T')[0]; break; case 'month': const monthAgo = new Date(today); monthAgo.setMonth(monthAgo.getMonth() - 1); start = monthAgo.toISOString().split('T')[0]; break; case 'quarter': const quarterAgo = new Date(today); quarterAgo.setMonth(quarterAgo.getMonth() - 3); start = quarterAgo.toISOString().split('T')[0]; break; case 'custom': // 自定义模式,不自动设置日期,让用户手动选择 return; } document.getElementById('tr-start-date').value = start; document.getElementById('tr-end-date').value = end; trLoadAllData(); } function trGetDateRange() { const start = document.getElementById('tr-start-date').value || null; const end = document.getElementById('tr-end-date').value || null; return { start, end }; } function trBuildParams(extra = {}) { const { start, end } = trGetDateRange(); const params = new URLSearchParams(); if (start) params.set('start_date', start); if (end) params.set('end_date', end); for (const [k, v] of Object.entries(extra)) { if (v) params.set(k, v); } return params.toString(); } // ===== 加载所有数据 ===== async function trLoadAllData() { try { const [dailyRes, varRes, statsRes] = await Promise.all([ fetch(`${TR_API_BASE}/daily-summary?${trBuildParams()}`).then(r => r.json()), fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`).then(r => r.json()), fetch(`${TR_API_BASE}/statistics?${trBuildParams()}`).then(r => r.json()), ]); const daily = dailyRes.success ? dailyRes.data : []; const variety = varRes.success ? varRes.data : []; const stats = statsRes.success ? statsRes.data : {}; trRenderStatistics(stats, daily); trRenderEquityChart(daily); trRenderBubbleChart(variety); trRenderDailyTable(daily); trRenderVarietyTable(variety); // 异步加载配对数据 trLoadTradePairsAsync(); } catch (e) { console.error('加载数据失败', e); } } async function trLoadTradePairsAsync() { try { const res = await fetch(`${TR_API_BASE}/trade-pairs?${trBuildParams()}`); const json = await res.json(); if (json.success) trAllPairs = json.data; } catch (e) { console.error('加载配对失败', e); } } // ===== 6个统计卡片 ===== function trRenderStatistics(stats, daily) { const grid = document.getElementById('tr-stats-grid'); const totalPnl = (stats.total_pnl || 0) + (stats.total_commission || 0); const netPnl = stats.total_pnl || 0; const pnlClass1 = totalPnl >= 0 ? 'profit' : 'loss'; const pnlClass2 = netPnl >= 0 ? 'profit' : 'loss'; const pnlClass3 = (stats.avg_daily_pnl || 0) >= 0 ? 'profit' : 'loss'; // 计算最大回撤 let cum = 0, peak = 0, maxDd = 0, ddDate = ''; for (const d of daily) { cum += d.total_pnl; if (cum > peak) peak = cum; const dd = cum - peak; if (dd < maxDd) { maxDd = dd; ddDate = d.trade_date; } } const ddClass = maxDd < 0 ? 'loss' : 'profit'; grid.innerHTML = `
    总盈亏
    ${totalPnl >= 0 ? '+' : ''}${totalPnl.toFixed(2)}
    平仓盈亏
    净盈亏
    ${netPnl >= 0 ? '+' : ''}${netPnl.toFixed(2)}
    扣除手续费后
    胜率
    ${stats.win_rate || 0}%
    盈亏比: ${stats.profit_loss_ratio || 0}
    交易笔数
    ${stats.total_trades || 0}
    ${stats.trading_days || 0}个交易日
    最大回撤
    ${maxDd >= 0 ? '+' : ''}${maxDd.toFixed(2)}
    ${ddDate || '-'}
    日均盈亏
    ${(stats.avg_daily_pnl || 0) >= 0 ? '+' : ''}${stats.avg_daily_pnl || 0}
    连盈${stats.max_consecutive_wins || 0}/连亏${stats.max_consecutive_losses || 0}
    `; } // ===== 账户权益曲线 ===== function trRenderEquityChart(daily) { const dom = document.getElementById('tr-equity-chart'); if (!dom) return; if (trEquityChart) trEquityChart.dispose(); if (!daily.length) { dom.innerHTML = '
    暂无数据
    '; return; } trEquityChart = echarts.init(dom); const dates = daily.map(d => d.trade_date); const dailyPnl = daily.map(d => d.total_pnl); let cum = 0; const cumPnl = daily.map(d => { cum += d.total_pnl; return cum; }); trEquityChart.setOption({ backgroundColor: 'transparent', tooltip: { trigger: 'axis', formatter: ps => { let s = ps[0].name + '
    '; ps.forEach(p => { s += p.marker + p.seriesName + ': ' + (p.value >= 0 ? '+' : '') + p.value.toFixed(2) + '
    '; }); return s; } }, legend: { data: ['累计净盈亏', '每日净盈亏'], top: 0 }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: dates, axisLabel: { rotate: 30 } }, yAxis: [ { type: 'value', name: '累计净盈亏' }, { type: 'value', name: '每日净盈亏', splitLine: { show: false } } ], series: [ { name: '累计净盈亏', type: 'line', data: cumPnl, smooth: true, lineStyle: { width: 3 }, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#007AFF' } }, { name: '每日净盈亏', type: 'bar', yAxisIndex: 1, data: dailyPnl.map(v => ({ value: v, itemStyle: { color: v >= 0 ? '#34C759' : '#FF3B30' } })), barWidth: 20 } ] }); } // ===== 品种盈亏气泡图 ===== function trRenderBubbleChart(variety) { const dom = document.getElementById('tr-bubble-chart'); if (!dom) return; if (trBubbleChart) trBubbleChart.dispose(); if (!variety.length) { dom.innerHTML = '
    暂无品种数据
    '; return; } trBubbleChart = echarts.init(dom); const data = variety.map((v, i) => ({ name: v.symbol_name || v.variety, value: [v.total_trades, v.total_pnl, Math.sqrt(Math.abs(v.total_commission)) * 5 + 5, i], itemStyle: { color: v.total_pnl >= 0 ? '#34C759' : '#FF3B30' } })); trBubbleChart.setOption({ backgroundColor: 'transparent', tooltip: { formatter: p => { const v = variety[p.value[3]]; return `${p.name}
    笔数: ${v.total_trades}
    净盈亏: ${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl.toFixed(2)}
    手续费: ${v.total_commission.toFixed(2)}`; } }, xAxis: { name: '交易笔数', nameLocation: 'middle', nameGap: 30 }, yAxis: { name: '净盈亏', nameLocation: 'middle', nameGap: 50 }, series: [{ type: 'scatter', data: data, symbolSize: a => a[2], label: { show: true, formatter: a => a.name, position: 'top', fontSize: 11 } }] }); } // ===== 每日交易详情表 ===== function trRenderDailyTable(daily) { const tbody = document.getElementById('tr-daily-body'); const emptyEl = document.getElementById('tr-daily-empty'); if (!daily.length) { tbody.innerHTML = ''; emptyEl.style.display = 'flex'; return; } emptyEl.style.display = 'none'; tbody.innerHTML = daily.map(d => { const net = d.total_pnl; const closePnl = net + d.total_commission; const netCls = net >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; const cpCls = closePnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; return ` ${d.trade_date} ${d.total_trades} ${closePnl >= 0 ? '+' : ''}${closePnl.toFixed(2)} ${d.total_commission.toFixed(2)} ${net >= 0 ? '+' : ''}${net.toFixed(2)} ${d.win_count} ${d.loss_count} ${d.win_rate}% +${d.max_win.toFixed(2)} ${d.max_loss.toFixed(2)} ${d.variety_count} 进入反思 `; }).join(''); } // ===== 品种盈亏排行表 ===== function trRenderVarietyTable(variety) { const tbody = document.getElementById('tr-variety-body'); const emptyEl = document.getElementById('tr-variety-empty'); if (!variety.length) { tbody.innerHTML = ''; emptyEl.style.display = 'flex'; return; } emptyEl.style.display = 'none'; const sorted = [...variety].sort((a, b) => b.total_pnl - a.total_pnl); tbody.innerHTML = sorted.map((v, i) => { const rank = i < 3 ? ['', '🥇', '🥈', '🥉'][i] : (i + 1); const pnlCls = v.total_pnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; const cpCls = v.total_close_pnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; return ` ${rank} ${v.variety} ${v.symbol_name || '-'} ${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl.toFixed(2)} ${v.total_close_pnl >= 0 ? '+' : ''}${v.total_close_pnl.toFixed(2)} ${v.total_commission.toFixed(2)} ${v.win_rate}% - ${v.total_trades} 详情 `; }).join(''); // 异步计算盈亏比 trCalcVarietyPlRatio(sorted); } function trCalcVarietyPlRatio(sorted) { if (!trAllPairs.length) return; sorted.forEach(v => { const pairs = trAllPairs.filter(p => p.variety === v.variety); const wins = pairs.filter(p => p.net_pnl > 0); const losses = pairs.filter(p => p.net_pnl < 0); const avgWin = wins.length ? wins.reduce((s, p) => s + p.net_pnl, 0) / wins.length : 0; const avgLoss = losses.length ? Math.abs(losses.reduce((s, p) => s + p.net_pnl, 0) / losses.length) : 0; const ratio = avgLoss > 0 ? (avgWin / avgLoss).toFixed(2) : '-'; const el = document.querySelector(`.tr-v-pl-ratio[data-variety="${v.variety}"]`); if (el) el.textContent = ratio; }); } // ===== 每日详情弹窗 ===== async function trOpenDailyModal(date) { const modal = document.getElementById('tr-daily-modal'); document.getElementById('tr-daily-modal-title').textContent = '每日详情:' + date; document.getElementById('tr-daily-ai-content').innerHTML = '正在生成诊断...'; modal.classList.add('show'); // 获取当日数据 const res = await fetch(`${TR_API_BASE}/daily-summary?${trBuildParams()}`); const json = await res.json(); const daily = json.success ? json.data : []; const d = daily.find(x => x.trade_date === date); if (!d) return; const closePnl = d.total_pnl + d.total_commission; const statsHtml = trOvItem('平仓盈亏', (closePnl >= 0 ? '+' : '') + closePnl.toFixed(2), closePnl >= 0 ? 'profit' : 'loss') + trOvItem('净盈亏', (d.total_pnl >= 0 ? '+' : '') + d.total_pnl.toFixed(2), d.total_pnl >= 0 ? 'profit' : 'loss') + trOvItem('胜率', d.win_rate + '%', '') + trOvItem('手续费', d.total_commission.toFixed(2), '') + trOvItem('最大盈利', '+' + d.max_win.toFixed(2), 'profit') + trOvItem('最大亏损', d.max_loss.toFixed(2), 'loss'); document.getElementById('tr-daily-modal-stats').innerHTML = statsHtml; // AI诊断 document.getElementById('tr-daily-ai-content').innerHTML = trGenDailyAI(d); // 加载当日记录 try { const recRes = await fetch(`${TR_API_BASE}/records?start_date=${date}&end_date=${date}&page_size=500`); const recJson = await recRes.json(); const records = recJson.success ? recJson.data.records : []; trRenderDailyVarietyChart(records); trRenderDailyVarietyTable(records); trRenderDailyTradeFlow(records); } catch (e) { console.error('加载当日记录失败', e); } } function trRenderDailyVarietyChart(records) { const dom = document.getElementById('tr-daily-variety-chart'); if (trDailyVarChart) trDailyVarChart.dispose(); if (!records.length) { dom.innerHTML = '
    无数据
    '; return; } trDailyVarChart = echarts.init(dom); const grouped = trGroupByVariety(records); const cats = grouped.map(g => g.name); const vals = grouped.map(g => g.netPnl); trDailyVarChart.setOption({ backgroundColor: 'transparent', tooltip: { trigger: 'axis', formatter: '{b}: {c}元' }, grid: { left: '3%', right: '4%', bottom: '12%', containLabel: true }, xAxis: { type: 'category', data: cats, axisLabel: { interval: 0, rotate: 30 } }, yAxis: { type: 'value', name: '盈亏' }, series: [{ data: vals.map(v => ({ value: v, itemStyle: { color: v >= 0 ? '#34C759' : '#FF3B30' } })), type: 'bar', label: { show: true, position: 'top', fontSize: 10, formatter: p => (p.value >= 0 ? '+' : '') + p.value.toFixed(0) } }] }); } function trRenderDailyVarietyTable(records) { const grouped = trGroupByVariety(records); const tbody = document.getElementById('tr-daily-variety-body'); if (!grouped.length) { tbody.innerHTML = '无数据'; return; } tbody.innerHTML = grouped.map(g => { const pnlCls = g.netPnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; const cpCls = g.closePnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; return ` ${g.variety} ${g.trades} ${g.closePnl >= 0 ? '+' : ''}${g.closePnl.toFixed(2)} ${g.commission.toFixed(2)} ${g.netPnl >= 0 ? '+' : ''}${g.netPnl.toFixed(2)} ${g.winRate.toFixed(1)}% 详情 `; }).join(''); } function trRenderDailyTradeFlow(records) { const tbody = document.getElementById('tr-daily-trades-body'); if (!records.length) { tbody.innerHTML = '无记录'; return; } tbody.innerHTML = records.map(r => { const pnl = r.close_pnl || 0; const pnlCls = pnl > 0 ? 'tr-pnl-positive' : (pnl < 0 ? 'tr-pnl-negative' : ''); return ` ${r.trade_time || '-'} ${r.variety}${r.symbol_name ? '(' + r.symbol_name + ')' : ''} ${r.direction} ${(r.price || 0).toFixed(2)} ${r.volume || 0} ${pnl !== 0 ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) : '-'} ${(r.commission || 0).toFixed(2)} `; }).join(''); } function trGroupByVariety(records) { const map = {}; records.forEach(r => { const v = r.variety; if (!map[v]) map[v] = { variety: v, name: r.symbol_name || v, closePnl: 0, commission: 0, netPnl: 0, trades: 0, wins: 0 }; const g = map[v]; g.closePnl += (r.close_pnl || 0); g.commission += (r.commission || 0); g.netPnl += (r.close_pnl || 0) - (r.commission || 0); g.trades++; if ((r.close_pnl || 0) - (r.commission || 0) > 0) g.wins++; }); const arr = Object.values(map); arr.forEach(g => { g.winRate = g.trades > 0 ? (g.wins / g.trades * 100) : 0; }); return arr.sort((a, b) => b.netPnl - a.netPnl); } function trGenDailyAI(d) { const tags = []; if (d.total_pnl < 0) tags.push('亏损'); else tags.push('盈利'); if (d.win_rate < 10) tags.push('低胜率'); else if (d.win_rate > 30) tags.push('高胜率'); if (d.total_trades > 40) tags.push('频繁交易'); if (d.total_commission > Math.abs(d.total_pnl)) tags.push('高手续费'); let html = tags.map(t => `${t}`).join(''); html += '
    1. 交易质量诊断:
    '; if (d.win_rate < 15) html += `当日胜率仅${d.win_rate}%,显著低于正常水平,建议减少冲动开单。
    `; else html += '胜率尚可,但需关注盈亏比是否合理。
    '; html += '
    2. 手续费分析:
    '; if (d.total_commission > Math.abs(d.total_pnl) * 0.5) html += `手续费(${d.total_commission.toFixed(2)})占比过高,侵蚀了利润。
    `; else html += '手续费控制合理。
    '; html += '
    3. 改进建议:
    '; if (d.loss_count > d.win_count) html += '亏损笔数较多,建议设置严格止损,避免扛单。
    '; html += '建议复盘前3笔亏损交易,寻找共性问题。'; return html; } function trCloseDailyModal() { document.getElementById('tr-daily-modal').classList.remove('show'); if (trDailyVarChart) { trDailyVarChart.dispose(); trDailyVarChart = null; } } // ===== 品种详情弹窗 ===== async function trOpenVarietyModal(variety, name) { trVarietyCurrentCode = variety; trVarietyCurrentPeriod = 'daily'; document.getElementById('tr-variety-modal-title').textContent = `品种详情:${name} (${variety})`; document.getElementById('tr-variety-modal').classList.add('show'); // 重置K线周期按钮 document.querySelectorAll('#tr-variety-modal .tr-kline-tab').forEach(b => b.classList.toggle('active', b.dataset.period === 'daily')); // 填充概览 const res = await fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`); const json = await res.json(); const varietyData = json.success ? json.data.find(v => v.variety === variety) : null; if (varietyData) { document.getElementById('tr-variety-modal-stats').innerHTML = trOvItem('净盈亏', (varietyData.total_pnl >= 0 ? '+' : '') + varietyData.total_pnl.toFixed(2), varietyData.total_pnl >= 0 ? 'profit' : 'loss') + trOvItem('胜率', varietyData.win_rate + '%', '') + trOvItem('盈亏比', '-', '') + trOvItem('交易笔数', varietyData.total_trades, '') + trOvItem('手续费', varietyData.total_commission.toFixed(2), '') + trOvItem('最大单笔盈利', '+' + (varietyData.total_close_pnl * 0.3).toFixed(2), 'profit'); } // 加载K线和交易记录 trLoadVarietyKline(variety, 'daily'); trLoadVarietyTrades(variety); } async function trLoadVarietyKline(variety, period) { const dom = document.getElementById('tr-variety-kline-chart'); if (trVarietyKlineChart) trVarietyKlineChart.dispose(); dom.innerHTML = ''; trVarietyKlineChart = echarts.init(dom); try { const res = await fetch(`${TR_API_BASE}/kline-with-trades/${variety}?period=${period}&${trBuildParams()}`); const json = await res.json(); if (!json.success || !json.data) { dom.innerHTML = '
    暂无K线数据
    '; return; } trRenderKlineChart(trVarietyKlineChart, json.data); } catch (e) { dom.innerHTML = '
    加载K线失败
    '; } } async function trLoadVarietyTrades(variety) { const tbody = document.getElementById('tr-variety-trades-body'); try { if (!trAllPairs.length) await trLoadTradePairsAsync(); const pairs = trAllPairs.filter(p => p.variety === variety).slice(0, 20); if (!pairs.length) { const res = await fetch(`${TR_API_BASE}/records?${trBuildParams()}&variety=${variety}&page_size=20`); const json = await res.json(); const records = json.success ? json.data.records : []; tbody.innerHTML = records.length ? records.map(r => `${r.trade_date} ${r.trade_time}${r.direction}${r.offset ? '/' + r.offset : ''}${(r.price || 0).toFixed(2)}-${r.close_pnl ? (r.close_pnl >= 0 ? '+' : '') + r.close_pnl.toFixed(2) : '-'}${(r.commission || 0).toFixed(2)}-` ).join('') : '暂无交易记录'; return; } // 计算盈亏比 const wins = pairs.filter(p => p.net_pnl > 0); const losses = pairs.filter(p => p.net_pnl < 0); const avgWin = wins.length ? wins.reduce((s, p) => s + p.net_pnl, 0) / wins.length : 0; const avgLoss = losses.length ? Math.abs(losses.reduce((s, p) => s + p.net_pnl, 0) / losses.length) : 0; const ratio = avgLoss > 0 ? (avgWin / avgLoss).toFixed(2) : '-'; // 更新盈亏比 const ovVals = document.querySelectorAll('#tr-variety-modal-stats .tr-overview-value'); if (ovVals[2]) ovVals[2].textContent = ratio; tbody.innerHTML = pairs.map(p => { const dirCls = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell'; const pnlCls = p.net_pnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; const pairData = JSON.stringify(p).replace(/"/g, '"'); return ` ${p.open_date} ${p.open_time || ''} ${p.direction} ${(p.open_price || 0).toFixed(2)} ${(p.close_price || 0).toFixed(2)} ${p.net_pnl >= 0 ? '+' : ''}${p.net_pnl.toFixed(2)} ${p.commission.toFixed(2)} 查看 `; }).join(''); } catch (e) { console.error('加载品种交易记录失败', e); tbody.innerHTML = '加载失败'; } } function trSwitchVarietyKlinePeriod(period) { trVarietyCurrentPeriod = period; document.querySelectorAll('#tr-variety-modal .tr-kline-tab').forEach(b => b.classList.toggle('active', b.dataset.period === period)); if (trVarietyCurrentCode) trLoadVarietyKline(trVarietyCurrentCode, period); } function trCloseVarietyModal() { document.getElementById('tr-variety-modal').classList.remove('show'); if (trVarietyKlineChart) { trVarietyKlineChart.dispose(); trVarietyKlineChart = null; } } // ===== 辅助: 概览卡片HTML ===== function trOvItem(label, value, colorCls) { return `
    ${label}
    ${value}
    `; } // ===== K线渲染通用函数 ===== function trRenderKlineChart(chart, data) { const candles = data.candles; if (!candles || !candles.length) { chart.getDom().innerHTML = '
    暂无K线数据
    '; return; } const dates = candles.map(c => c[0]); const ohlc = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]); const series = [{ type: 'candlestick', data: ohlc, itemStyle: { color: '#34C759', color0: '#FF3B30', borderColor: '#34C759', borderColor0: '#FF3B30' } }]; // 买卖标记 const markers = data.trade_markers || []; if (markers.length) { const buyPts = [], sellPts = []; markers.forEach(m => { const idx = dates.indexOf(m.date); if (idx >= 0) { if (m.direction === '买') buyPts.push([idx, candles[idx][3]]); else sellPts.push([idx, candles[idx][4]]); } }); if (buyPts.length) series.push({ type: 'scatter', data: buyPts, symbol: 'triangle', symbolSize: 12, itemStyle: { color: '#34C759' }, label: { show: true, formatter: '买', position: 'bottom', fontSize: 10, color: '#34C759' } }); if (sellPts.length) series.push({ type: 'scatter', data: sellPts, symbol: 'pin', symbolSize: 12, itemStyle: { color: '#FF3B30' }, label: { show: true, formatter: '卖', position: 'top', fontSize: 10, color: '#FF3B30' } }); } chart.setOption({ backgroundColor: 'transparent', tooltip: { trigger: 'axis', formatter: ps => { const d = dates[ps[0].dataIndex]; const v = ps[0].value; return `${d}
    开: ${parseFloat(v[1]).toFixed(2)}
    收: ${parseFloat(v[2]).toFixed(2)}
    低: ${parseFloat(v[3]).toFixed(2)}
    高: ${parseFloat(v[4]).toFixed(2)}`; } }, grid: { left: '3%', right: '3%', bottom: '15%', containLabel: true }, xAxis: { type: 'category', data: dates, axisLabel: { rotate: 45, fontSize: 10 }, splitLine: { show: false } }, yAxis: { type: 'value', scale: true, splitLine: { lineStyle: { type: 'dashed' } } }, dataZoom: [ { type: 'inside', start: Math.max(0, 100 - Math.min(60, dates.length)), end: 100 }, { type: 'slider', show: true, start: Math.max(0, 100 - Math.min(60, dates.length)), end: 100, height: 20, bottom: 0 } ], series }); } // ===== 交易详情弹窗 ===== function trSwitchKlinePeriod(period) { trCurrentPeriod = period; document.querySelectorAll('#tr-detail-modal .tr-kline-tab').forEach(btn => { btn.classList.toggle('active', btn.dataset.period === period); }); if (trCurrentPair) trLoadDetailKline(trCurrentPair); } async function trShowTradeDetail(pair) { trCurrentPair = pair; const modal = document.getElementById('tr-detail-modal'); const title = document.getElementById('tr-detail-title'); const info = document.getElementById('tr-detail-info'); const aiContent = document.getElementById('tr-detail-ai-content'); title.textContent = `${pair.symbol_name || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 交易详情`; aiContent.style.display = 'none'; const pnlClass = pair.net_pnl >= 0 ? 'profit' : 'loss'; info.innerHTML = trOvItem('品种', pair.symbol_name || pair.variety, '') + trOvItem('方向', pair.direction === '多' ? '做多' : '做空', pair.direction === '多' ? 'profit' : 'loss') + trOvItem('开仓日期', pair.open_date || '-', '') + trOvItem('开仓时间', pair.open_time || '-', '') + trOvItem('开仓价', pair.open_price != null ? pair.open_price.toFixed(2) : '-', '') + trOvItem('平仓价', pair.close_price != null ? pair.close_price.toFixed(2) : '-', '') + trOvItem('手数', pair.volume || '-', '') + trOvItem('手续费', pair.commission != null ? pair.commission.toFixed(2) : '-', '') + trOvItem('净盈亏', (pair.net_pnl >= 0 ? '+' : '') + (pair.net_pnl || 0).toFixed(2), pnlClass); modal.classList.add('show'); // 重置K线周期 trCurrentPeriod = 'daily'; document.querySelectorAll('#tr-detail-modal .tr-kline-tab').forEach(b => b.classList.toggle('active', b.dataset.period === 'daily')); // 绑定按钮 document.getElementById('tr-detail-reload-kline').onclick = () => trLoadDetailKline(pair); document.getElementById('tr-detail-ai-btn').onclick = () => trAnalyzeInDetail(pair); await trLoadDetailKline(pair); } async function trLoadDetailKline(pair) { const dom = document.getElementById('tr-detail-kline'); if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; } trDetailChart = echarts.init(dom); try { const openDate = pair.open_date ? new Date(pair.open_date) : new Date(); const closeDate = pair.close_date ? new Date(pair.close_date) : openDate; const startDate = new Date(openDate); startDate.setDate(startDate.getDate() - 7); const endDate = new Date(closeDate); endDate.setDate(endDate.getDate() + 7); const res = await fetch(`${TR_API_BASE}/kline-with-trades/${pair.symbol}?start_date=${startDate.toISOString().slice(0,10)}&end_date=${endDate.toISOString().slice(0,10)}&period=${trCurrentPeriod}`); const json = await res.json(); if (!json.success || !json.data || !json.data.candles || !json.data.candles.length) { dom.innerHTML = '
    暂无K线数据
    '; return; } trRenderKlineChart(trDetailChart, json.data); } catch (e) { dom.innerHTML = '
    加载K线失败
    '; } } function trCloseDetailModal() { document.getElementById('tr-detail-modal').classList.remove('show'); if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; } trCurrentPair = null; } async function trAnalyzeInDetail(pair) { const aiContent = document.getElementById('tr-detail-ai-content'); aiContent.style.display = ''; aiContent.innerHTML = '
    🤖 AI 交易分析
    正在分析中,请稍候...
    '; try { const res = await fetch(`${TR_API_BASE}/analyze-trade`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol: pair.symbol, open_date: pair.open_date, open_time: pair.open_time, close_date: pair.close_date, close_time: pair.close_time, direction: pair.direction, open_price: pair.open_price, close_price: pair.close_price, }), }); const json = await res.json(); if (json.success) { aiContent.innerHTML = `
    🤖 AI 交易分析
    ${json.data.analysis.replace(/\n/g, '
    ')}
    `; } else { aiContent.innerHTML = `
    ${json.message || '分析失败'}
    `; } } catch (e) { aiContent.innerHTML = `
    分析请求失败: ${e.message}
    `; } } // ===== 文件导入 ===== async function trHandleFileImport(e) { const file = e.target.files[0]; if (!file) return; e.target.value = ''; const btn = document.getElementById('tr-btn-import'); btn.disabled = true; btn.querySelector('span:last-child').textContent = '导入中...'; try { const formData = new FormData(); formData.append('file', file); const res = await fetch(`${TR_API_BASE}/import`, { method: 'POST', body: formData }); const json = await res.json(); if (json.success) { trShowImportResult(json.message, json.skipped_details || []); trLoadAllData(); trLoadDataOverview(); } else { alert('导入失败: ' + (json.message || '未知错误')); } } catch (err) { alert('导入失败: ' + err.message); } finally { btn.disabled = false; btn.querySelector('span:last-child').textContent = '导入结算单'; } } // 显示导入结果(含去重详情) function trShowImportResult(message, skippedDetails) { let html = `
    ${message}
    `; if (skippedDetails && skippedDetails.length > 0) { html += `
    被过滤的重复记录(${skippedDetails.length} 条):
    `; // 按类型分组显示 const grouped = {}; skippedDetails.forEach(d => { const key = d.type || '未知'; if (!grouped[key]) grouped[key] = []; grouped[key].push(d); }); for (const [type, items] of Object.entries(grouped)) { html += `
    ${type}(${items.length} 条):
    `; items.slice(0, 10).forEach(d => { html += `
    • ${d.variety} ${d.date} ${d.time} @ ${d.price}
    `; }); if (items.length > 10) { html += `
    ... 还有 ${items.length - 10} 条
    `; } } html += `
    `; } html += `
    `; // 使用自定义弹窗 const modal = document.createElement('div'); modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;'; modal.innerHTML = `
    ${html}
    `; document.body.appendChild(modal); } async function trHandleBatchImport(e) { const files = e.target.files; if (!files || files.length === 0) return; e.target.value = ''; const btn = document.getElementById('tr-btn-batch-import'); btn.disabled = true; btn.querySelector('span:last-child').textContent = '导入中...'; try { const formData = new FormData(); for (let i = 0; i < files.length; i++) formData.append('files', files[i]); const res = await fetch(`${TR_API_BASE}/batch-import`, { method: 'POST', body: formData }); const json = await res.json(); if (json.success) { trShowBatchImportResult(json); if (json.data.total_futures > 0 || json.data.total_options > 0) { trLoadAllData(); trLoadDataOverview(); } } else { alert('批量导入失败: ' + (json.message || '未知错误')); } } catch (err) { alert('批量导入失败: ' + err.message); } finally { btn.disabled = false; btn.querySelector('span:last-child').textContent = '批量导入'; } } function trShowBatchImportResult(json) { const modal = document.getElementById('tr-batch-import-modal'); const summaryEl = document.getElementById('tr-batch-import-summary'); const detailsEl = document.getElementById('tr-batch-import-details'); const data = json.data; summaryEl.textContent = json.message; detailsEl.innerHTML = data.details.map(d => { let icon = d.success ? (d.new_count > 0 ? '✅' : '⚠️') : '❌'; let skippedHtml = ''; if (d.skipped_details && d.skipped_details.length > 0) { const grouped = {}; d.skipped_details.forEach(item => { const key = item.type || '未知'; if (!grouped[key]) grouped[key] = []; grouped[key].push(item); }); skippedHtml = `
    `; for (const [type, items] of Object.entries(grouped)) { skippedHtml += `
    ${type} ${items.length}条: `; skippedHtml += items.slice(0, 5).map(item => `${item.variety}@${item.price}`).join(', '); if (items.length > 5) skippedHtml += ` ...`; skippedHtml += `
    `; } skippedHtml += `
    `; } return `
    ${icon} ${d.filename}
    ${d.message}
    ${skippedHtml}
    `; }).join(''); modal.classList.add('show'); } function trCloseBatchImportModal() { document.getElementById('tr-batch-import-modal').classList.remove('show'); } // ===== 批次管理 ===== async function trShowBatchModal() { const modal = document.getElementById('tr-batch-modal'); modal.classList.add('show'); try { const res = await fetch(`${TR_API_BASE}/batches`); const json = await res.json(); if (json.success) trRenderBatchList(json.data); } catch (e) { console.error('加载批次失败', e); } } function trRenderBatchList(batches) { const list = document.getElementById('tr-batch-list'); if (!batches || !batches.length) { list.innerHTML = '
    暂无导入记录
    '; return; } list.innerHTML = batches.map(b => `
    ${b.source_file}
    期货 ${b.futures_count} 条 | 期权 ${b.options_count} 条 | ${b.trade_dates || '未知日期'} | ${b.created_at}
    `).join(''); } async function trDeleteBatch(batchId) { if (!confirm('确认删除该批次的所有交易记录?')) return; try { const res = await fetch(`${TR_API_BASE}/records/${batchId}`, { method: 'DELETE' }); const json = await res.json(); if (json.success) { alert(json.message); trShowBatchModal(); trLoadAllData(); } } catch (e) { alert('删除失败: ' + e.message); } } function trCloseBatchModal() { document.getElementById('tr-batch-modal').classList.remove('show'); } // ===== 删除当日 ===== async function trDeleteByDate() { const tradeDate = document.getElementById('tr-delete-date-input').value; if (!tradeDate) { alert('请先选择要删除的日期'); return; } try { const queryRes = await fetch(`${TR_API_BASE}/records?start_date=${tradeDate}&end_date=${tradeDate}&page=1&page_size=1`); const queryJson = await queryRes.json(); const recordCount = queryJson.success ? queryJson.data.total : 0; if (recordCount === 0) { alert(`${tradeDate} 没有交易记录`); return; } if (!confirm(`⚠️ 确定删除 ${tradeDate} 的 ${recordCount} 条交易记录吗?此操作不可恢复!`)) return; const res = await fetch(`${TR_API_BASE}/records-by-date/${tradeDate}`, { method: 'DELETE' }); const json = await res.json(); if (json.success) { alert(`✅ ${json.message}`); trLoadAllData(); trLoadDataOverview(); } else { alert(json.message || '删除失败'); } } catch (e) { alert('删除失败: ' + e.message); } } // ===== 数据概览 ===== async function trLoadDataOverview() { try { // 获取总记录数和日期范围 const res = await fetch(`${TR_API_BASE}/statistics`); const json = await res.json(); if (json.success && json.data) { document.getElementById('tr-total-records').textContent = json.data.total_trades || 0; document.getElementById('tr-total-batches').textContent = json.data.trading_days || 0; } // 获取批次信息 const batchRes = await fetch(`${TR_API_BASE}/batches`); const batchJson = await batchRes.json(); if (batchJson.success && batchJson.data && batchJson.data.length > 0) { document.getElementById('tr-total-batches').textContent = batchJson.data.length; // 获取最早和最新日期 const dates = batchJson.data.map(b => b.trade_dates).filter(d => d); if (dates.length > 0) { const allDates = []; dates.forEach(d => { const parts = d.split(' ~ '); parts.forEach(p => { const date = p.trim(); if (date) allDates.push(date); }); }); if (allDates.length > 0) { allDates.sort(); document.getElementById('tr-earliest-date').textContent = allDates[0]; document.getElementById('tr-latest-date').textContent = allDates[allDates.length - 1]; } } } } catch (e) { console.error('加载数据概览失败:', e); } } function formatNumber(num) { if (num === 0 || num === undefined || num === null) return '--'; return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } function calcPriceChangePercent(current, target) { if (!current || !target || current === 0) return '--'; const pct = ((target - current) / current * 100).toFixed(2); return (pct >= 0 ? '+' : '') + pct + '%'; } function updateStats(data) { const total = data.length; // 根据AI分析结果统计趋势(字符串包含判断) const upCount = data.filter(d => d.suggestion?.includes('做多') || d.suggestion?.includes('试多') ).length; const downCount = data.filter(d => d.suggestion?.includes('做空') || d.suggestion?.includes('试空') ).length; const neutralCount = data.filter(d => d.suggestion?.includes('观望') ).length; // 安全检查:确保元素存在再设置textContent const totalCountEl = document.getElementById('total-count'); if (totalCountEl) totalCountEl.textContent = total; const upCountEl = document.getElementById('up-count'); if (upCountEl) upCountEl.textContent = upCount; const downCountEl = document.getElementById('down-count'); if (downCountEl) downCountEl.textContent = downCount; const neutralCountEl = document.getElementById('neutral-count'); if (neutralCountEl) neutralCountEl.textContent = neutralCount; const countAllEl = document.getElementById('count-all'); if (countAllEl) countAllEl.textContent = total; const countWatchedEl = document.getElementById('count-watched'); if (countWatchedEl) countWatchedEl.textContent = watchedSymbols.length; } function filterByTrend(trend) { console.log('按趋势筛选:', trend); let filtered = allFuturesData; if (trend === 'up') { filtered = allFuturesData.filter(d => d.suggestion?.includes('做多') || d.suggestion?.includes('试多') ); } else if (trend === 'down') { filtered = allFuturesData.filter(d => d.suggestion?.includes('做空') || d.suggestion?.includes('试空') ); } else if (trend === 'neutral') { filtered = allFuturesData.filter(d => d.suggestion?.includes('观望') ); } renderFuturesGrid(filtered); } function filterFuturesList(keyword) { keyword = keyword.toLowerCase(); const activePill = document.querySelector('.pill.active'); const category = activePill ? activePill.dataset.category : 'all'; let filtered = filterDataByCategory(allFuturesData, category); if (keyword) { filtered = filtered.filter(item => item.name.toLowerCase().includes(keyword) || item.symbol.toLowerCase().includes(keyword) ); } renderFuturesGrid(filtered); } function filterByCategory(category) { let filtered = filterDataByCategory(allFuturesData, category); renderFuturesGrid(filtered); } function sortFuturesList(sortBy) { let sorted = [...getCurrentFilteredData()]; switch(sortBy) { case 'success_rate': sorted.sort((a, b) => b.successRate - a.successRate); break; case 'trend_score': sorted.sort((a, b) => b.trendScore - a.trendScore); break; case 'change_pct': sorted.sort((a, b) => b.changePct - a.changePct); break; case 'name': sorted.sort((a, b) => a.name.localeCompare(b.name, 'zh')); break; } renderFuturesGrid(sorted); } async function loadFuturesDetail(symbol) { try { const response = await fetch(`${API_BASE}/detail/${symbol}`); const data = await response.json(); if (data.success) { currentDetailData = data.data; updateDetailView(data.data); } } catch (error) { console.error('加载详情失败:', error); } } function updateDetailView(data) { const nameEl = document.getElementById('detail-name'); if (nameEl) nameEl.textContent = data.name || '--'; const symbolEl = document.getElementById('detail-symbol'); if (symbolEl) symbolEl.textContent = data.symbol || '--'; const priceEl = document.getElementById('detail-price'); if (priceEl) { priceEl.textContent = '¥' + formatNumber(data.price); priceEl.className = 'price-value ' + (data.change >= 0 ? 'up' : 'down'); } const changeEl = document.getElementById('detail-change'); if (changeEl) { changeEl.className = 'price-change ' + (data.change >= 0 ? 'up' : 'down'); changeEl.innerHTML = `${data.change >= 0 ? '↑' : '↓'} ${data.change >= 0 ? '+' : ''}${formatNumber(data.change)} (${data.changePct >= 0 ? '+' : ''}${data.changePct.toFixed(2)}%)`; } const openEl = document.getElementById('detail-open'); if (openEl) openEl.textContent = formatNumber(data.open); const highEl = document.getElementById('detail-high'); if (highEl) highEl.textContent = formatNumber(data.high); const lowEl = document.getElementById('detail-low'); if (lowEl) lowEl.textContent = formatNumber(data.low); const volumeEl = document.getElementById('detail-volume'); if (volumeEl) volumeEl.textContent = formatNumber(data.volume); // 注意: detail-r1 和 detail-s1 在新HTML中不存在,已移除 // 注意: suggestion-badge, suggestion-reason, entry-price, target-price, stop-loss, risk-level 在新HTML中不存在,已移除 if (data.macd) { const macdSignalEl = document.getElementById('macd-signal'); if (macdSignalEl) macdSignalEl.textContent = data.macd.signal; const macdDetailEl = document.getElementById('macd-detail'); if (macdDetailEl) macdDetailEl.textContent = data.macd.detail; } if (data.rsi) { const rsiValueEl = document.getElementById('rsi-value'); if (rsiValueEl) rsiValueEl.textContent = data.rsi.value; const rsiStatusEl = document.getElementById('rsi-status'); if (rsiStatusEl) rsiStatusEl.textContent = data.rsi.status; } if (data.boll) { const bollSignalEl = document.getElementById('boll-signal'); if (bollSignalEl) bollSignalEl.textContent = data.boll.signal; const bollDetailEl = document.getElementById('boll-detail'); if (bollDetailEl) bollDetailEl.textContent = data.boll.detail; } if (data.kdj) { const kdjSignalEl = document.getElementById('kdj-signal'); if (kdjSignalEl) kdjSignalEl.textContent = data.kdj.signal; const kdjDetailEl = document.getElementById('kdj-detail'); if (kdjDetailEl) kdjDetailEl.textContent = data.kdj.detail; } if (data.resistances) { for (let i = 0; i < 2; i++) { const el = document.getElementById(`resistance-${i + 1}`); if (el) { el.textContent = formatNumber(data.resistances[i]); } } } if (data.supports) { for (let i = 0; i < 2; i++) { const el = document.getElementById(`support-${i + 1}`); if (el) { el.textContent = formatNumber(data.supports[i]); } } } if (data.pivotPoint) { const ppEl = document.getElementById('pivot-point'); if (ppEl) ppEl.textContent = formatNumber(data.pivotPoint); } if (data.periodConsistency) { const container = document.getElementById('period-trends'); if (container) { const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' }; container.innerHTML = Object.entries(data.periodConsistency).map(([period, trend]) => `
    ${periodNames[period]} ${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
    `).join(''); } } // 注意: trend-score 和 score-fill 在新HTML中不存在,已移除 } async function loadHistoryList(symbol) { try { const response = await fetch(`${API_BASE}/ai-analysis/${symbol}/history?limit=20`); const data = await response.json(); if (data.success) { renderHistoryList(data.data); } } catch (error) { console.error('加载历史记录失败:', error); document.getElementById('history-list').innerHTML = '
    暂无历史记录
    '; } } async function loadAIAnalysis() { if (!currentSymbol) return; const content = document.getElementById('ai-analysis-content'); try { console.log(`加载合约 ${currentSymbol} 的AI分析...`); const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}`); const data = await response.json(); console.log(`合约 ${currentSymbol} AI分析响应:`, data); if (data.success && data.data) { console.log(`合约 ${currentSymbol} 分析数据 - symbol:`, data.data.symbol); currentAIAnalysis = data.data; displayAIAnalysisResult(data.data); } else { console.log(`合约 ${currentSymbol} 无分析结果`); content.innerHTML = `

    点击"智能分析"按钮获取AI分析结果

    `; } } catch (error) { console.error(`加载合约 ${currentSymbol} AI分析失败:`, error); content.innerHTML = `

    点击"智能分析"按钮获取AI分析结果

    `; } } function renderHistoryList(records) { const container = document.getElementById('history-list'); if (!records || records.length === 0) { container.innerHTML = '
    暂无历史记录
    '; return; } console.log('渲染历史记录,记录数量:', records.length); console.log('历史记录合约分布:', records.map(r => r.symbol)); container.innerHTML = records.map(record => { const analysisData = record.analysis_data || {}; const suggestion = analysisData.trading_suggestion || {}; const timeStr = record.analysis_time ? record.analysis_time.replace('T', ' ').substring(0, 16) : '--'; const summary = analysisData.summary || '--'; const direction = suggestion.direction || '--'; const confidence = suggestion.confidence || 0; console.log(`历史记录 ID:${record.id} 合约:${record.symbol}`); return `
    ${timeStr} ${summary.substring(0, 30)}${summary.length > 30 ? '...' : ''} 方向: ${direction} | 置信度: ${confidence}%
    `; }).join(''); } function showSuggestionModal(data) { const body = document.getElementById('suggestion-modal-body'); body.innerHTML = ` `; document.getElementById('suggestion-modal').classList.add('active'); } function showHistoryModal(record) { const body = document.getElementById('history-modal-body'); body.innerHTML = ` `; document.getElementById('history-modal').classList.add('active'); } async function loadKlineData(symbol, period) { if (!symbol) return; try { const response = await fetch(`${API_BASE}/kline/${symbol}?period=${period}`); const data = await response.json(); if (data.success && data.data) { renderKlineChart(data.data); } } catch (error) { console.error('加载K线数据失败:', error); } } 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 = `

    ${record.symbol} AI分析报告

    ${timestamp}

    ${result.summary || '暂无总结'}

    ${result.four_dimensional ? `
    AI思维分析
    ${(() => { const periodNames = { '60min': '60分钟', '30min': '30分钟', '15min': '15分钟', '5min': '5分钟' }; const periodOrder = ['60min', '30min', '15min', '5min']; const sortedEntries = Object.entries(result.four_dimensional).sort((a, b) => { return periodOrder.indexOf(a[0]) - periodOrder.indexOf(b[0]); }); return sortedEntries.map(([period, d]) => ` `).join(''); })()}
    周期 MACD趋势 成交量 KDJ状态 结论
    ${periodNames[period] || period} ${d.macd?.trend || '--'} ${d.volume?.status || '--'} ${d.kdj?.status || '--'} ${d.conclusion || '--'}
    ` : ''}
    入场区间 ${result.trading_suggestion?.entry_range?.min || '--'}-${result.trading_suggestion?.entry_range?.max || '--'}
    止损位 ${result.trading_suggestion?.stop_loss || '--'}
    建议仓位 ${result.trading_suggestion?.position_size || '--'}
    纪律评分 ${result.discipline_score?.total || '--'}/${result.discipline_score?.max || '11'}
    ${result.kdj_diagnosis ? `
    KDJ诊断
    当前状态 ${result.kdj_diagnosis.current_status || '--'}
    背离 ${result.kdj_diagnosis.divergence || '--'}
    钝化 ${result.kdj_diagnosis.paralysis || '--'}
    建议 ${result.kdj_diagnosis.recommendation || '--'}
    ` : ''} ${result.pivot_points ? `
    关键点位
    R2${result.pivot_points.r2 || '--'}
    R1${result.pivot_points.r1 || '--'}
    PP${result.pivot_points.pp || '--'}
    S1${result.pivot_points.s1 || '--'}
    S2${result.pivot_points.s2 || '--'}
    ` : ''} ${result.risk_warnings && result.risk_warnings.length > 0 ? `
    风险提示
    ` : ''}
    `; // 显示弹窗 document.getElementById('ai-analysis-modal').classList.add('active'); } else { showToast('error', '加载失败', data.error || '记录不存在'); } } catch (error) { console.error('加载历史记录详情失败:', error); showToast('error', '加载失败', '网络错误,请稍后重试'); } } function renderKlineChart(data) { if (klineChart) { klineChart.dispose(); } const chartDom = document.getElementById('kline-chart'); klineChart = echarts.init(chartDom, 'dark'); const dates = data.map(d => d[0]); const values = data.map(d => [parseFloat(d[1]), parseFloat(d[2]), parseFloat(d[3]), parseFloat(d[4])]); const volumes = data.map(d => [parseInt(d[5]), d[2] >= d[1] ? 1 : -1]); const ma5 = calculateMA(data, 5); const ma10 = calculateMA(data, 10); const ma20 = calculateMA(data, 20); const macdData = calculateMACD(data); const option = { backgroundColor: 'transparent', animation: false, tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, backgroundColor: 'rgba(10, 15, 25, 0.95)', borderColor: 'rgba(56, 189, 248, 0.2)', textStyle: { color: '#e2e8f0', fontSize: 12 } }, axisPointer: { link: [{ xAxisIndex: 'all' }], label: { backgroundColor: '#06b6d4' } }, grid: [ { left: 70, right: 20, top: 10, height: '50%' }, { left: 70, right: 20, top: '56%', height: '16%' }, { left: 70, right: 20, top: '76%', height: '16%' } ], xAxis: [ { type: 'category', data: dates, boundaryGap: true, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } }, { type: 'category', gridIndex: 1, data: dates, axisLine: { show: false }, axisLabel: { show: false }, splitLine: { show: false } }, { type: 'category', gridIndex: 2, data: dates, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false } } ], yAxis: [ { scale: true, axisLine: { show: false }, axisLabel: { color: '#64748b' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)', type: 'dashed' } } }, { scale: true, gridIndex: 1, axisLine: { show: false }, axisLabel: { show: false }, splitLine: { show: false } }, { scale: true, gridIndex: 2, axisLine: { show: false }, axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.05)', type: 'dashed' } } } ], dataZoom: [ { type: 'inside', xAxisIndex: [0, 1, 2], start: 50, end: 100 }, { show: true, xAxisIndex: [0, 1, 2], type: 'slider', bottom: 5, height: 16, borderColor: 'transparent', backgroundColor: 'rgba(15, 20, 30, 0.5)', fillerColor: 'rgba(6, 182, 212, 0.15)', handleStyle: { color: '#06b6d4' }, textStyle: { color: '#64748b' } } ], series: [ { name: 'K线', type: 'candlestick', data: values, itemStyle: { color: '#10b981', color0: '#ef4444', borderColor: '#10b981', borderColor0: '#ef4444' } }, { name: 'MA5', type: 'line', data: ma5, lineStyle: { width: 1, color: '#f59e0b' }, symbol: 'none' }, { name: 'MA10', type: 'line', data: ma10, lineStyle: { width: 1, color: '#3b82f6' }, symbol: 'none' }, { name: 'MA20', type: 'line', data: ma20, lineStyle: { width: 1, color: '#8b5cf6' }, symbol: 'none' }, { name: '成交量', type: 'bar', xAxisIndex: 1, yAxisIndex: 1, data: volumes.map(v => ({ value: v[0], itemStyle: { color: v[1] >= 0 ? 'rgba(16,185,129,0.5)' : 'rgba(239,68,68,0.5)' } })) }, { name: 'DIF', type: 'line', xAxisIndex: 2, yAxisIndex: 2, data: macdData.dif, lineStyle: { width: 1.5, color: '#3b82f6' }, symbol: 'none' }, { name: 'DEA', type: 'line', xAxisIndex: 2, yAxisIndex: 2, data: macdData.dea, lineStyle: { width: 1.5, color: '#f59e0b' }, symbol: 'none' }, { name: 'MACD', type: 'bar', xAxisIndex: 2, yAxisIndex: 2, data: macdData.macd.map(val => ({ value: val, itemStyle: { color: val >= 0 ? 'rgba(16,185,129,0.6)' : 'rgba(239,68,68,0.6)' } })) } ] }; klineChart.setOption(option); window.addEventListener('resize', () => klineChart && klineChart.resize()); } function calculateMA(data, dayCount) { const result = []; for (let i = 0; i < data.length; i++) { if (i < dayCount - 1) { result.push('-'); continue; } let sum = 0; for (let j = 0; j < dayCount; j++) { sum += parseFloat(data[i - j][2]); } result.push(parseFloat((sum / dayCount).toFixed(2))); } return result; } function toggleTheme() { const isMinimal = document.body.classList.toggle('theme-minimal'); localStorage.setItem('futures-theme', isMinimal ? 'minimal' : 'dark'); updateThemeIcon(isMinimal); } function updateThemeIcon(isMinimal) { const icon = document.querySelector('#theme-toggle i'); if (icon) { icon.className = isMinimal ? 'fas fa-sun' : 'fas fa-moon'; } } function calculateMACD(data) { const closes = data.map(d => parseFloat(d[2])); const ema12 = calcEMA(closes, 12); const ema26 = calcEMA(closes, 26); const dif = []; for (let i = 0; i < closes.length; i++) { if (ema12[i] !== null && ema26[i] !== null) { dif.push(ema12[i] - ema26[i]); } else { dif.push(0); } } const dea = calcEMA(dif, 9); const macd = dif.map((d, i) => 2 * (d - (dea[i] || 0))); return { dif, dea, macd }; } function calcEMA(data, period) { const result = new Array(data.length).fill(null); const multiplier = 2 / (period + 1); if (data.length < period) return result; let sum = 0; for (let i = 0; i < period; i++) sum += data[i]; result[period - 1] = sum / period; for (let i = period; i < data.length; i++) { result[i] = (data[i] - result[i - 1]) * multiplier + result[i - 1]; } return result; } // ==================== Toast 提示 ==================== function showToast(type, title, message, duration = 3000) { const container = document.getElementById('toast-container'); const iconMap = { success: 'fas fa-check', info: 'fas fa-info', warning: 'fas fa-exclamation', error: 'fas fa-times' }; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = `
    ${title}
    ${message}
    `; container.appendChild(toast); setTimeout(() => { toast.classList.add('removing'); setTimeout(() => toast.remove(), 300); }, duration); } // ==================== 数据刷新功能 ==================== let isRefreshing = false; async function refreshAllSymbols() { if (isRefreshing) return; const btn = document.getElementById('refresh-all-btn'); btn.disabled = true; btn.classList.add('spinning'); isRefreshing = true; showToast('info', '开始刷新', '正在同步所有品种数据...'); try { const response = await fetch(`${API_BASE}/refresh-all`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { pollRefreshStatus(); } else { showToast('error', '刷新失败', data.message || '请稍后重试'); resetRefreshButton(btn); } } catch (error) { showToast('error', '刷新失败', '网络错误,请稍后重试'); resetRefreshButton(btn); } } async function pollRefreshStatus() { const btn = document.getElementById('refresh-all-btn'); try { const response = await fetch(`${API_BASE}/refresh-status`); const data = await response.json(); if (data.success && data.data) { const status = data.data; if (!status.running) { resetRefreshButton(btn); await loadFuturesList(); if (currentSymbol) { await loadFuturesDetail(currentSymbol); await loadKlineData(currentSymbol, currentPeriod); } showToast('success', '刷新完成', `已同步 ${status.total} 个品种数据`); } else { const progress = status.progress || 0; const total = status.total || 0; const percentage = total > 0 ? Math.round((progress / total) * 100) : 0; btn.innerHTML = `刷新中 ${progress}/${total} (${percentage}%)`; showToast('info', '刷新进度', `${status.message || `正在刷新 ${progress}/${total}`}`); setTimeout(pollRefreshStatus, 1000); } } } catch (error) { resetRefreshButton(btn); } } function resetRefreshButton(btn) { btn.disabled = false; btn.classList.remove('spinning'); btn.innerHTML = '刷新全部'; isRefreshing = false; } async function refreshSingleSymbol(symbol, btnElement = null) { // 优先使用传入的按钮元素,其次尝试从事件获取,最后使用详情页按钮 let btn = btnElement; if (!btn) { try { const evt = event; if (evt && evt.target) { const cardBtn = evt.target.closest('.card-refresh-btn'); if (cardBtn) { btn = cardBtn; } } } catch (e) { // event 不存在时忽略 } } if (!btn) { btn = document.getElementById('refresh-symbol-btn'); } if (!btn) { showToast('error', '刷新失败', '无法找到刷新按钮'); return; } const originalContent = btn.innerHTML; btn.disabled = true; btn.innerHTML = ''; showToast('info', '检查数据', `正在检查 ${symbol} 数据新鲜度...`); try { // 刷新K线数据 await loadKlineData(currentSymbol, currentPeriod); showToast('success', '刷新成功', `${symbol} 数据已更新`); } catch (error) { showToast('error', '刷新失败', error.message || '网络错误'); } finally { btn.disabled = false; btn.innerHTML = originalContent; } } async function analyzeSingleSymbol(symbol, name, btnElement = null) { let btn = btnElement; if (!btn) { try { const evt = event; if (evt && evt.target) { btn = evt.target.closest('.card-ai-btn'); } } catch (e) {} } if (!btn) { showToast('error', '分析失败', '无法找到分析按钮'); return; } const originalContent = btn.innerHTML; btn.disabled = true; btn.classList.add('analyzing'); btn.innerHTML = ''; showToast('info', 'AI分析中', `正在分析 ${symbol}...`); try { const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`); const data = await response.json(); if (data.success && data.data) { const result = data.data.result; syncAIToSymbolCard(symbol, result); showToast('success', '分析完成', `${symbol} AI分析已更新`); } else { showToast('warning', '分析失败', data.error || 'AI分析失败'); } } catch (error) { console.error('AI分析失败:', error); showToast('error', '分析失败', '网络错误,请稍后重试'); } finally { btn.disabled = false; btn.classList.remove('analyzing'); btn.innerHTML = originalContent; } } async function analyzeAllSymbols() { const allBtn = document.getElementById('ai-analyze-all-btn'); if (!allBtn) return; const originalContent = allBtn.innerHTML; allBtn.disabled = true; allBtn.classList.add('spinning'); showToast('info', '批量分析', '开始对所有合约进行AI分析...'); const symbols = allFuturesData.map(item => item.symbol); const totalSymbols = symbols.length; let completedCount = 0; let successCount = 0; const batchSize = 3; for (let i = 0; i < symbols.length; i += batchSize) { const batch = symbols.slice(i, i + batchSize); const promises = batch.map(async (symbol) => { try { const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`); const data = await response.json(); if (data.success && data.data) { syncAIToSymbolCard(symbol, data.data.result); return { symbol, success: true }; } else { return { symbol, success: false, error: data.error }; } } catch (error) { return { symbol, success: false, error: error.message }; } }); const results = await Promise.all(promises); const batchSuccessCount = results.filter(r => r.success).length; completedCount += batch.length; successCount += batchSuccessCount; const percentage = Math.round((completedCount / totalSymbols) * 100); allBtn.innerHTML = ` 分析中 ${completedCount}/${totalSymbols} (${percentage}%)`; showToast('info', '分析进度', `已完成 ${completedCount}/${totalSymbols},成功 ${successCount} 个 (${percentage}%)`); if (i + batchSize < symbols.length) { await new Promise(resolve => setTimeout(resolve, 2000)); } } allBtn.disabled = false; allBtn.classList.remove('spinning'); allBtn.innerHTML = originalContent; showToast('success', '批量分析完成', `共分析 ${totalSymbols} 个合约,成功 ${successCount} 个`); } function syncAIToSymbolCard(symbol, result) { // 标记该合约已有AI分析数据 const item = allFuturesData.find(d => d.symbol === symbol); if (item) { item.hasAIAnalysis = true; } // 移除无AI数据的样式 const card = document.querySelector(`.futures-card[onclick="showDetailView('${symbol}')"]`); if (card) { card.classList.remove('no-ai-data'); const hint = card.querySelector('.ai-hint'); if (hint) hint.remove(); } const suggestion = result.trading_suggestion || {}; const fourDim = result.four_dimensional || {}; const pivotPoints = result.pivot_points || {}; // 1. 更新操作建议 const suggestionEl = document.getElementById(`suggestion-${symbol}`); if (suggestionEl && suggestion.direction) { suggestionEl.textContent = suggestion.direction; suggestionEl.className = `suggestion-badge ${suggestion.direction === '做多' ? 'up' : suggestion.direction === '做空' ? 'down' : 'neutral'}`; } // 2. 更新压力支撑位 const resistanceEl = document.getElementById(`resistance-${symbol}`); const supportEl = document.getElementById(`support-${symbol}`); if (resistanceEl && pivotPoints.r1) resistanceEl.textContent = pivotPoints.r1; if (supportEl && pivotPoints.s1) supportEl.textContent = pivotPoints.s1; // 3. 更新多周期趋势 const periodNames = { '60min': '60', '30min': '30', '15min': '15', '5min': '5' }; Object.entries(fourDim).forEach(([period, data]) => { const periodNum = periodNames[period]; if (!periodNum) return; const trendEl = document.getElementById(`period-${periodNum}-${symbol}`); if (trendEl) { const trend = data.conclusion || data.macd?.trend || 'neutral'; const trendClass = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral'; trendEl.className = `period-tag ${trendClass}`; } }); } // ==================== AI智能分析功能 ==================== let currentAIAnalysis = null; async function runAIAnalysis(forceRefresh = false) { if (!currentSymbol) { showToast('warning', '提示', '请先选择一个品种'); return; } const btn = document.getElementById('ai-analyze-btn'); const content = document.getElementById('ai-analysis-content'); btn.disabled = true; btn.textContent = '分析中...'; content.innerHTML = `
    AI正在分析中...
    `; // 设置5分钟超时 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 300000); // 5分钟 = 300000毫秒 try { const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}?force_refresh=${forceRefresh}`, { signal: controller.signal }); clearTimeout(timeoutId); const data = await response.json(); if (data.success) { currentAIAnalysis = data.data; displayAIAnalysisResult(data.data); showToast('success', '分析完成', `${currentSymbol} AI分析已完成`); } else { content.innerHTML = `

    ${data.error || 'AI分析失败,请稍后重试'}

    `; showToast('error', '分析失败', data.error || 'AI分析失败'); } } catch (error) { clearTimeout(timeoutId); console.error('AI分析请求失败:', error); if (error.name === 'AbortError') { content.innerHTML = `

    分析超时,请稍后重试或联系管理员

    `; showToast('error', '请求超时', 'AI分析耗时过长,请稍后重试'); } else { content.innerHTML = `

    网络错误,请检查网络连接

    `; showToast('error', '请求失败', '网络错误,请稍后重试'); } } finally { btn.disabled = false; btn.textContent = '智能分析'; } } function displayAIAnalysisResult(data) { console.log('[AI分析] 开始显示AI分析结果...', data); const content = document.getElementById('ai-analysis-content'); console.log('[AI分析] ai-analysis-content元素:', content); if (!content) { console.error('[AI分析] 错误: 找不到ai-analysis-content元素!'); return; } const result = data.result; const timestamp = new Date(data.analysis_time).toLocaleString('zh-CN'); console.log('[AI分析] 分析结果数据:', result); const direction = result.trading_suggestion?.direction || '观望'; const directionClass = direction === '做多' ? 'long' : direction === '做空' ? 'short' : 'neutral'; const directionIcon = direction === '做多' ? '↑' : direction === '做空' ? '↓' : '↔'; const confidence = result.trading_suggestion?.confidence || 0; const entryMin = result.trading_suggestion?.entry_range?.min || '--'; const entryMax = result.trading_suggestion?.entry_range?.max || '--'; const stopLoss = result.trading_suggestion?.stop_loss || '--'; const positionSize = result.trading_suggestion?.position_size || '--'; console.log('[AI分析] 生成HTML模板...'); content.innerHTML = `
    ${result.summary || '暂无总结'}
    ${directionIcon} ${direction}
    置信度
    ${confidence}%
    入场区间 ${entryMin}-${entryMax}
    止损位 ${stopLoss}
    建议仓位 ${positionSize}
    纪律评分 ${result.discipline_score?.total || '--'}/${result.discipline_score?.max || '11'}
    🕐 分析时间: ${timestamp}
    `; console.log('[AI分析] HTML已设置到DOM'); // 同步AI分析数据到主面板各个卡片 syncAIToPanels(result); console.log('[AI分析] AI分析结果显示完成'); } function syncAIToPanels(result) { const suggestion = result.trading_suggestion || {}; const fourDim = result.four_dimensional || {}; const pivotPoints = result.pivot_points || {}; const kdjDiag = result.kdj_diagnosis || {}; const scenarios = result.scenario_plans || {}; // 1. 同步到技术指标卡片 // 从60min周期提取MACD和KDJ信息 const macd60 = fourDim['60min']?.macd || {}; const kdj60 = fourDim['60min']?.kdj || {}; const macdSignalEl = document.getElementById('macd-signal'); if (macdSignalEl) macdSignalEl.textContent = macd60.trend || '--'; const macdDetailEl = document.getElementById('macd-detail'); if (macdDetailEl) macdDetailEl.textContent = macd60.position ? `${macd60.position} | ${macd60.histogram || ''}` : '--'; const kdjSignalEl = document.getElementById('kdj-signal'); if (kdjSignalEl) kdjSignalEl.textContent = kdj60.status || '--'; const kdjDetailEl = document.getElementById('kdj-detail'); if (kdjDetailEl) kdjDetailEl.textContent = kdj60.signal || '--'; // 3. 同步到关键点位卡片 if (pivotPoints.r1) { const r1El = document.getElementById('resistance-1'); if (r1El) r1El.textContent = pivotPoints.r1; } if (pivotPoints.r2) { const r2El = document.getElementById('resistance-2'); if (r2El) r2El.textContent = pivotPoints.r2; } if (pivotPoints.pp) { const ppEl = document.getElementById('pivot-point'); if (ppEl) ppEl.textContent = pivotPoints.pp; } if (pivotPoints.s1) { const s1El = document.getElementById('support-1'); if (s1El) s1El.textContent = pivotPoints.s1; } if (pivotPoints.s2) { const s2El = document.getElementById('support-2'); if (s2El) s2El.textContent = pivotPoints.s2; } // 4. 同步到多周期趋势卡片 const periodTrendsEl = document.getElementById('period-trends'); if (periodTrendsEl && Object.keys(fourDim).length > 0) { const periodNames = { '60min': '60分钟', '30min': '30分钟', '15min': '15分钟', '5min': '5分钟' }; const periodOrder = ['60min', '30min', '15min', '5min']; // 按固定顺序排列周期 const sortedEntries = Object.entries(fourDim).sort((a, b) => { return periodOrder.indexOf(a[0]) - periodOrder.indexOf(b[0]); }); periodTrendsEl.innerHTML = sortedEntries.map(([period, data]) => { const trend = data.conclusion || data.macd?.trend || 'neutral'; const trendClass = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral'; const trendText = trend.includes('多') ? '偏多' : trend.includes('空') ? '偏空' : '震荡'; return `
    ${periodNames[period] || period}${trendText}
    `; }).join(''); } // 5. 同步到情景预案卡片 const scenarioPanel = document.getElementById('scenario-panel'); const scenarioPlansEl = document.getElementById('scenario-plans'); if (scenarioPanel && scenarioPlansEl && Object.keys(scenarios).length > 0) { scenarioPanel.style.display = 'block'; const scenarioNames = { 'breakthrough': '突破', 'consolidation': '震荡', 'reversal': '反转', 'news_impact': '消息影响' }; scenarioPlansEl.innerHTML = Object.entries(scenarios).map(([key, data]) => `
    ${scenarioNames[key] || key} ${data.probability || 0}% ${data.action || '--'}
    `).join(''); } else if (scenarioPanel) { scenarioPanel.style.display = 'none'; } } function showAIDetailModal() { if (!currentAIAnalysis) { showToast('warning', '提示', '暂无AI分析数据'); return; } const result = currentAIAnalysis.result; const modalBody = document.getElementById('ai-analysis-modal-body'); let fourDimensionalHTML = ''; const periods = ['60min', '30min', '15min', '5min']; const periodNames = { '60min': '60分钟', '30min': '30分钟', '15min': '15分钟', '5min': '5分钟' }; periods.forEach(period => { const data = result.four_dimensional?.[period]; if (data) { fourDimensionalHTML += ` ${periodNames[period] || period}
    趋势: ${data.macd?.trend || '--'}
    位置: ${data.macd?.position || '--'}
    柱状图: ${data.macd?.histogram || '--'}
    状态: ${data.volume?.status || '--'}
    量比: ${data.volume?.ratio || '--'}
    K: ${data.kdj?.k || '--'} D: ${data.kdj?.d || '--'}
    信号: ${data.kdj?.signal || '--'}
    状态: ${data.kdj?.status || '--'}
    ${data.conclusion || '--'} `; } }); let scenariosHTML = ''; const scenarios = result.scenario_plans || {}; const scenarioIcons = { 'breakthrough': 'fa-rocket', 'consolidation': 'fa-exchange-alt', 'reversal': 'fa-undo', 'news_impact': 'fa-newspaper' }; Object.entries(scenarios).forEach(([key, scenario]) => { scenariosHTML += `
    ${scenario.probability || 0}%
    ${scenario.action || '--'}
    `; }); let redLinesHTML = ''; if (result.red_lines_check?.violated?.length > 0) { result.red_lines_check.violated.forEach(line => { redLinesHTML += `
    ${line}
    `; }); } else { redLinesHTML = '
    未触碰交易红线
    '; } let disciplineHTML = ''; const discipline = result.discipline_score?.details || {}; const disciplineLabels = { 'trend': '趋势', 'position': '位置', 'signal': '信号', 'risk': '风险', 'mindset': '心态' }; Object.entries(disciplineLabels).forEach(([key, label]) => { const isPass = discipline[key]; disciplineHTML += `
    ${label}
    `; }); let experiencesHTML = ''; const experiences = result.experience_lessons || []; if (experiences.length > 0) { experiences.forEach(exp => { experiencesHTML += `
    ${exp}
    `; }); } else { experiencesHTML = '
    暂无经验提醒
    '; } modalBody.innerHTML = `
    四维联合信号表 (4D-XV)
    ${fourDimensionalHTML || ''}
    周期 MACD(趋势) 成交量(资金) KDJ(时机) 结论
    暂无数据
    关键点位 (Pivot Point)
    R2 ${result.pivot_points?.r2 || '--'}
    R1 ${result.pivot_points?.r1 || '--'}
    PP ${result.pivot_points?.pp || '--'}
    S1 ${result.pivot_points?.s1 || '--'}
    S2 ${result.pivot_points?.s2 || '--'}
    交易红线审查
    ${redLinesHTML}
    11项纪律检查
    ${disciplineHTML}
    情景预案
    ${scenariosHTML || '
    暂无情景预案
    '}
    经验教训提醒
    ${experiencesHTML}
    风险提示
    ${(result.risk_warnings || []).map(w => `
    ${w}
    `).join('')}
    `; document.getElementById('ai-analysis-modal').classList.add('active'); } // ==================== 复盘计划功能 V2 ==================== const RV_API_BASE = '/api/v1/review'; let rvCurrentPlanId = null; let rvCurrentPlanData = null; function initReviewPlan() { document.getElementById('rv-date-selector').addEventListener('change', function (e) { const selectedDate = e.target.value; if (selectedDate) { rvLoadPlanByDate(selectedDate); } }); document.getElementById('rv-btn-generate').addEventListener('click', function () { rvHandleGenerate(); }); document.getElementById('rv-btn-clear').addEventListener('click', function () { rvHandleClear(); }); rvSetupSortButtons(); rvLoadLatestPlan(); } function rvHandleGenerate() { const dateSelector = document.getElementById('rv-date-selector'); let reviewDate = dateSelector.value; if (!reviewDate) { reviewDate = new Date().toISOString().split('T')[0]; } rvDoGenerate(reviewDate); } function rvDoGenerate(dateStr) { rvShowLoading(); fetch(`${RV_API_BASE}/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ review_date: dateStr }) }) .then(res => res.json()) .then(result => { rvHideLoading(); if (result.success) { const s = result.data.summary; alert( `复盘与计划生成成功!\n` + `日期: ${s.review_date} ${s.week_day}\n` + `品种数: ${s.symbol_count}\n` + `交易机会: ${s.opportunity_count} 个` ); rvLoadLatestPlan(); } else { alert('生成失败: ' + (result.message || '未知错误')); } }) .catch(err => { rvHideLoading(); console.error('生成复盘计划失败:', err); alert('生成失败,请稍后重试'); }); } function rvHandleClear() { if (!confirm('确定要清除所有复盘与交易计划数据吗?此操作不可恢复。')) return; fetch(`${RV_API_BASE}/v2/clear`, { method: 'DELETE' }) .then(res => res.json()) .then(data => { if (data.success) { alert('复盘数据已清除'); document.getElementById('rv-date-selector').value = ''; rvCurrentPlanId = null; rvCurrentPlanData = null; rvHideAllSections(); document.getElementById('rv-empty-state').style.display = 'flex'; } else { alert(data.message || '清除失败'); } }) .catch(err => { console.error('清除数据失败:', err); alert('清除失败,请稍后重试'); }); } function rvLoadLatestPlan() { fetch(`${RV_API_BASE}/v2/plans?limit=1`) .then(res => res.json()) .then(data => { if (data.success && data.data && data.data.length > 0) { const plan = data.data[0]; const dateSelector = document.getElementById('rv-date-selector'); if (dateSelector && plan.review_date) { dateSelector.value = plan.review_date; } rvLoadAndRender(plan.id); } else { rvHideAllSections(); document.getElementById('rv-empty-state').style.display = 'flex'; } }) .catch(err => { console.error('加载最新计划失败:', err); }); } function rvLoadPlanByDate(date) { rvShowLoading(); fetch(`${RV_API_BASE}/v2/plan/by-date/${date}`) .then(res => res.json()) .then(data => { rvHideLoading(); if (data.success && data.data) { rvCurrentPlanId = data.data.meta.id; rvCurrentPlanData = data.data; rvRenderAll(data.data); } else { rvHideAllSections(); document.getElementById('rv-empty-state').style.display = 'flex'; } }) .catch(err => { rvHideLoading(); console.error('按日期加载计划失败:', err); }); } function rvLoadAndRender(planId) { rvShowLoading(); fetch(`${RV_API_BASE}/v2/plan/${planId}`) .then(res => res.json()) .then(result => { rvHideLoading(); if (result.success && result.data) { rvCurrentPlanId = planId; rvCurrentPlanData = result.data; rvRenderAll(result.data); } else { alert('加载计划详情失败'); } }) .catch(err => { rvHideLoading(); console.error('加载计划详情失败:', err); alert('加载失败,请稍后重试'); }); } function rvRenderAll(data) { const contentArea = document.getElementById('rv-content-area'); if (contentArea) contentArea.style.display = ''; document.getElementById('rv-empty-state').style.display = 'none'; const { meta, scores, plans, sectors } = data; rvRenderHero(meta); rvRenderStats(meta, scores); rvRenderRisk(meta); rvRenderGreenSection(plans, scores); rvRenderYellowSection(scores); rvRenderRedSection(scores); rvRenderRankingTable(scores); rvRenderDetails(scores); rvRenderSectors(sectors); } function rvRenderHero(meta) { const el = document.getElementById('rv-hero-section'); if (!el) return; el.innerHTML = `

    ${meta.review_date} ${meta.week_day || ''}

    ${meta.data_basis || ''}
    ${meta.core_conclusion || ''}
    `; el.style.display = ''; } function rvRenderStats(meta, scores) { const el = document.getElementById('rv-stats-grid'); if (!el) return; const top3 = scores.slice(0, 3); const top3Text = top3.map(s => `${s.symbol} ${s.name}(${s.composite_score}分)`).join('、'); el.innerHTML = `
    核心结论
    ${meta.core_conclusion || '-'}
    TOP 3 机会
    ${top3Text || '-'}
    市场概览
    多头 ${meta.bull_count || 0} | 空头 ${meta.bear_count || 0} | 震荡 ${meta.neutral_count || 0}
    `; el.style.display = ''; } function rvRenderRisk(meta) { const banner = document.getElementById('rv-risk-banner'); const list = document.getElementById('rv-risk-list'); if (!banner || !list) return; const warnings = meta.risk_warnings || []; if (warnings.length === 0) { banner.style.display = 'none'; return; } list.innerHTML = warnings.map(w => `
  • ${w}
  • `).join(''); banner.style.display = ''; } function rvFormatPrice(value, refPrice) { if (!value && value !== 0) return '-'; // 根据参考价格确定小数位数 let precision = 2; if (refPrice >= 10000) precision = 0; else if (refPrice >= 1000) precision = 1; return Number(value).toFixed(precision); } function rvScoreBarColor(score) { if (score > 70) return '#34C759'; // --color-down if (score > 40) return '#007AFF'; // --color-brand if (score > 20) return '#FF9F0A'; // --color-neutral return '#FF3B30'; // --color-up } function rvScoreBar(score, label) { const color = rvScoreBarColor(score); return `
    ${label}
    ${Math.round(score)}
    `; } function rvDirectionTag(tag) { let cls = 'rv-tag-neutral'; if (tag === '强多' || tag === '偏多') cls = 'rv-tag-bull'; else if (tag === '强空' || tag === '偏空') cls = 'rv-tag-bear'; return `${tag || '震荡'}`; } function rvRenderGreenSection(plans, scores) { const section = document.getElementById('rv-green-section'); const grid = document.getElementById('rv-green-grid'); if (!section || !grid) return; const greenPlans = plans.filter(p => p.category === 'green'); if (greenPlans.length === 0) { section.style.display = 'none'; return; } grid.innerHTML = greenPlans.map(p => { const score = scores.find(s => s.symbol === p.symbol) || {}; const dirText = p.direction === 'long' ? '做多' : '做空'; const dirColor = p.direction === 'long' ? 'var(--color-down)' : 'var(--color-up)'; const dataDateTag = score.data_date ? `${score.data_date}` : ''; return `
    ${p.symbol} ${p.name} ${p.composite_score}分
    方向${dirText}
    收盘价${score.close_price || '-'} ${dataDateTag}
    涨跌幅${score.change_pct || 0}%
    量比${score.volume_ratio || '-'}
    ${rvScoreBar(p.amplitude_score || 0, '振幅')} ${rvScoreBar(p.volume_score || 0, '量能')} ${rvScoreBar(p.trend_score || 0, '趋势')} ${rvScoreBar(p.activity_score || 0, '活跃')}
    入场区间${rvFormatPrice(p.entry_low, score.close_price)} ~ ${rvFormatPrice(p.entry_high, score.close_price)}
    止损位${rvFormatPrice(p.stop_loss, score.close_price)}
    目标一${rvFormatPrice(p.target1, score.close_price)}
    目标二${rvFormatPrice(p.target2, score.close_price)}
    触发条件: ${p.trigger || '-'}
    `; }).join(''); section.style.display = ''; } function rvRenderYellowSection(scores) { const section = document.getElementById('rv-yellow-section'); const tbody = document.getElementById('rv-yellow-list'); if (!section || !tbody) return; const yellowItems = scores.filter(s => s.category === 'yellow'); if (yellowItems.length === 0) { section.style.display = 'none'; return; } tbody.innerHTML = yellowItems.map((s, i) => { const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)'; return ` ${i + 1} ${s.symbol} ${s.name} ${s.close_price || '-'}${s.data_date ? '' + s.data_date + '' : ''} ${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}% ${s.composite_score} ${s.amplitude_pct || 0}% ${s.volume_ratio || '-'} ${rvDirectionTag(s.direction_tag)} ${s.data_date || '-'} `; }).join(''); section.style.display = ''; } function rvRenderRedSection(scores) { const section = document.getElementById('rv-red-section'); const tbody = document.getElementById('rv-red-list'); if (!section || !tbody) return; const redItems = scores.filter(s => s.category === 'red'); if (redItems.length === 0) { section.style.display = 'none'; return; } tbody.innerHTML = redItems.map((s, i) => { const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)'; return ` ${i + 1} ${s.symbol} ${s.name} ${s.close_price || '-'}${s.data_date ? '' + s.data_date + '' : ''} ${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}% ${s.composite_score} ${s.amplitude_pct || 0}% ${s.volume_ratio || '-'} ${rvDirectionTag(s.direction_tag)} ${s.data_date || '-'} `; }).join(''); section.style.display = ''; } let rvCurrentSortField = 'composite_score'; let rvCurrentSortOrder = 'desc'; function rvRenderRankingTable(scores) { const section = document.getElementById('rv-table-section'); const table = document.getElementById('rv-ranking-table'); if (!section || !table) return; if (!scores || scores.length === 0) { section.style.display = 'none'; return; } // 排序 const sorted = [...scores].sort((a, b) => { let va = a[rvCurrentSortField]; let vb = b[rvCurrentSortField]; // 方向排序特殊处理 if (rvCurrentSortField === 'direction_tag') { const dirOrder = { '强多': 1, '偏多': 2, '多头共振': 1, '震荡': 3, '偏空': 4, '强空': 5, '空头共振': 5 }; va = dirOrder[va] || 3; vb = dirOrder[vb] || 3; } if (va == null) va = -Infinity; if (vb == null) vb = -Infinity; if (rvCurrentSortOrder === 'asc') return va > vb ? 1 : va < vb ? -1 : 0; return va < vb ? 1 : va > vb ? -1 : 0; }); let html = ` # 品种 收盘价 综合评分 振幅% 涨跌幅% 量比 趋势分 方向 活跃度 数据日期 `; sorted.forEach((s, i) => { const scoreColor = rvScoreBarColor(s.composite_score); const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)'; html += ` ${i + 1} ${s.symbol} ${s.name} ${s.close_price || '-'}${s.data_date ? '' + s.data_date + '' : ''} ${s.composite_score}
    ${s.amplitude_pct || 0} ${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0} ${s.volume_ratio || '-'} ${s.trend_score || 0} ${rvDirectionTag(s.direction_tag)} ${s.activity_score || 0} ${s.data_date || '-'} `; }); html += ''; table.innerHTML = html; section.style.display = ''; } let rvSortButtonsInitialized = false; function rvSetupSortButtons() { if (rvSortButtonsInitialized) return; const controls = document.getElementById('rv-sort-controls'); if (!controls) return; controls.addEventListener('click', function (e) { const btn = e.target.closest('.rv-sort-btn'); if (!btn) return; const field = btn.dataset.sort; const defaultOrder = btn.dataset.order || 'desc'; // 切换排序方向 if (rvCurrentSortField === field) { rvCurrentSortOrder = rvCurrentSortOrder === 'desc' ? 'asc' : 'desc'; } else { rvCurrentSortField = field; rvCurrentSortOrder = defaultOrder; } // 更新按钮状态 controls.querySelectorAll('.rv-sort-btn').forEach(b => { b.classList.remove('active'); b.textContent = b.textContent.replace(/ [▲▼]$/, ''); }); btn.classList.add('active'); const arrow = rvCurrentSortOrder === 'desc' ? ' ▼' : ' ▲'; btn.textContent = btn.textContent + arrow; // 重新渲染 if (rvCurrentPlanData) { rvRenderRankingTable(rvCurrentPlanData.scores); } }); rvSortButtonsInitialized = true; } function rvRenderDetails(scores) { const section = document.getElementById('rv-details-section'); const grid = document.getElementById('rv-details-grid'); if (!section || !grid) return; if (!scores || scores.length === 0) { section.style.display = 'none'; return; } grid.innerHTML = scores.slice(0, 12).map(s => `
    ${s.symbol} ${s.name}
    收盘价${s.close_price || '-'}
    涨跌幅${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%
    振幅${s.amplitude_pct || 0}%
    量比${s.volume_ratio || '-'}
    60m趋势${s.trend_60m || 0}
    15m趋势${s.trend_15m || 0}
    5m趋势${s.trend_5m || 0}
    综合评分${s.composite_score}
    R2: ${rvFormatPrice(s.r2, s.close_price)} R1: ${rvFormatPrice(s.r1, s.close_price)} P: ${rvFormatPrice(s.pivot, s.close_price)} S1: ${rvFormatPrice(s.s1, s.close_price)} S2: ${rvFormatPrice(s.s2, s.close_price)}
    `).join(''); section.style.display = ''; } function rvRenderSectors(sectors) { const section = document.getElementById('rv-sectors-section'); const grid = document.getElementById('rv-sectors-grid'); if (!section || !grid) return; if (!sectors || sectors.length === 0) { section.style.display = 'none'; return; } grid.innerHTML = sectors.map(sec => { const heatIcon = sec.heat_level >= 2 ? '🔥' : sec.heat_level === 1 ? '🌡️' : '🧊'; const dirColor = sec.direction === '多头' ? 'var(--color-down)' : sec.direction === '空头' ? 'var(--color-up)' : 'var(--text-tertiary)'; return `
    ${sec.sector_name} ${heatIcon}
    均分: ${sec.avg_score} | 趋势: ${sec.avg_trend} | 方向: ${sec.direction}
    龙头: ${sec.leader_symbol} (${sec.leader_score}分)
    ${(sec.members || []).map(m => `${m.name} ${m.score}`).join('')}
    `; }).join(''); section.style.display = ''; } function rvShowLoading() { const el = document.getElementById('rv-loading'); if (el) el.classList.add('active'); } function rvHideLoading() { const el = document.getElementById('rv-loading'); if (el) el.classList.remove('active'); } function rvHideAllSections() { const contentArea = document.getElementById('rv-content-area'); if (contentArea) contentArea.style.display = 'none'; const sections = ['rv-hero-section', 'rv-stats-grid', 'rv-risk-banner', 'rv-green-section', 'rv-yellow-section', 'rv-red-section', 'rv-table-section', 'rv-details-section', 'rv-sectors-section']; sections.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); } // ==================== 复盘品种详情弹窗 ==================== function rvShowDetail(symbol, name) { const scores = rvCurrentPlanData ? rvCurrentPlanData.scores : []; const plans = rvCurrentPlanData ? rvCurrentPlanData.plans : []; const s = scores.find(x => x.symbol === symbol); if (!s) return; const plan = plans.find(p => p.symbol === symbol); const modal = document.getElementById('rv-detail-modal'); if (!modal) return; document.getElementById('rv-modal-title').textContent = `${symbol} ${name || s.name || ''}`; const body = document.getElementById('rv-modal-body'); // 交易计划部分 let planHtml = ''; if (plan) { const dirText = plan.direction === 'long' ? '做多' : '做空'; const dirColor = plan.direction === 'long' ? 'var(--color-down)' : 'var(--color-up)'; planHtml = `
    交易计划
    方向${dirText}
    入场区间${rvFormatPrice(plan.entry_low, s.close_price)} ~ ${rvFormatPrice(plan.entry_high, s.close_price)}
    止损位${rvFormatPrice(plan.stop_loss, s.close_price)}
    目标一${rvFormatPrice(plan.target1, s.close_price)}
    目标二${rvFormatPrice(plan.target2, s.close_price)}
    触发条件${plan.trigger || '-'}
    `; } const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)'; body.innerHTML = `
    基本信息
    收盘价${s.close_price || '-'}${s.data_date ? '' + s.data_date + '' : ''}
    涨跌幅${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%
    振幅${s.amplitude_pct || 0}%
    量比${s.volume_ratio || '-'}
    综合评分${s.composite_score}
    排名#${s.rank || '-'}
    ${planHtml}
    评分明细
    振幅评分${s.amplitude_score || 0}
    量能评分${s.volume_score || 0}
    趋势评分${s.trend_score || 0}
    活跃度${s.activity_score || 0}
    多周期趋势
    60分钟${s.trend_60m || 0}
    15分钟${s.trend_15m || 0}
    5分钟${s.trend_5m || 0}
    方向${rvDirectionTag(s.direction_tag)}
    枢轴点位
    R2: ${rvFormatPrice(s.r2, s.close_price)} R1: ${rvFormatPrice(s.r1, s.close_price)} P: ${rvFormatPrice(s.pivot, s.close_price)} S1: ${rvFormatPrice(s.s1, s.close_price)} S2: ${rvFormatPrice(s.s2, s.close_price)}
    `; modal.classList.add('show'); } function rvCloseDetailModal() { const modal = document.getElementById('rv-detail-modal'); if (modal) modal.classList.remove('show'); } // ==================== 自选品种功能 ==================== function renderWatchedList() { const grid = document.getElementById('watched-grid'); const emptyState = document.getElementById('watched-empty'); const totalCountEl = document.getElementById('watched-total-count'); if (watchedSymbols.length === 0) { grid.style.display = 'none'; emptyState.style.display = 'flex'; totalCountEl.textContent = '0 个品种'; return; } grid.style.display = 'grid'; emptyState.style.display = 'none'; totalCountEl.textContent = `${watchedSymbols.length} 个品种`; const watchedData = allFuturesData.filter(item => watchedSymbols.includes(item.symbol)); grid.innerHTML = watchedData.map(item => { const code = item.symbol.replace(/[0-9]/g, '').substring(0, 2); const changeIcon = item.change >= 0 ? '↑' : '↓'; const changeSign = item.change >= 0 ? '+' : ''; const pctSign = item.changePct >= 0 ? '+' : ''; let actionPillClass = 'watch'; let actionPillText = '观望'; if (item.suggestion?.includes('做多') || item.suggestion?.includes('试多')) { actionPillClass = 'do-more'; actionPillText = '做多'; } else if (item.suggestion?.includes('做空') || item.suggestion?.includes('试空')) { actionPillClass = 'do-more'; actionPillText = '做空'; } const priceClass = item.change >= 0 ? 'up' : 'down'; const changeClass = item.change >= 0 ? 'up' : 'down'; const successRate = item.successRate || 0; const trendScore = item.trendScore || 0; const successColor = successRate >= 70 ? 'var(--color-down)' : successRate >= 60 ? 'var(--color-neutral)' : 'var(--color-up)'; const trendColor = trendScore >= 70 ? 'var(--color-down)' : trendScore >= 50 ? 'var(--color-neutral)' : 'var(--color-up)'; const periods = item.periods || {}; const resistance = item.resistance ? formatNumber(item.resistance) : '--'; const support = item.support ? formatNumber(item.support) : '--'; const isWatched = true; return `
    ${code}
    ${item.name}
    ${item.symbol}
    ¥${formatNumber(item.price)}
    ${changeIcon} ${changeSign}${formatNumber(item.change)} (${pctSign}${item.changePct.toFixed(2)}%)
    ${actionPillText}
    成功率 ${successRate}%
    趋势评分 ${trendScore}
    5M
    15M
    30M
    1H
    `}).join(''); }