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(''); } // ==================== 交易复盘功能 ==================== const TR_API_BASE = '/api/v1/trade-review'; let trCurrentPage = 1; let trInitialized = false; 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', () => { trCurrentPage = 1; trLoadAllData(); }); // 删除当日按钮 document.getElementById('tr-btn-delete-date').addEventListener('click', trDeleteByDate); // 批次管理 document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal); // 子导航切换 document.querySelectorAll('.tr-sub-nav-item').forEach(item => { item.addEventListener('click', function(e) { e.preventDefault(); document.querySelectorAll('.tr-sub-nav-item').forEach(i => i.classList.remove('active')); this.classList.add('active'); const sub = this.dataset.sub; document.querySelectorAll('.tr-sub-page').forEach(p => p.classList.remove('active')); document.getElementById('tr-sub-' + sub).classList.add('active'); }); }); // 初始加载:先获取最后交易日,设为默认日期,再加载数据 trInitDefaultDate(); } 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); } 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() { trLoadStatistics(); trLoadRecords(); } async function trLoadStatistics() { try { const res = await fetch(`${TR_API_BASE}/statistics?${trBuildParams()}`); const json = await res.json(); if (json.success) trRenderStatistics(json.data); } catch (e) { console.error('加载统计失败', e); } } function trRenderStatistics(stats) { const grid = document.getElementById('tr-stats-grid'); const pnlClass = stats.total_pnl >= 0 ? 'profit' : 'loss'; grid.innerHTML = `
总盈亏
${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl.toFixed(2)}
手续费: ${stats.total_commission.toFixed(2)}
胜率
${stats.win_rate}%
盈亏比: ${stats.profit_loss_ratio}
交易统计
${stats.total_trades}
${stats.trading_days} 个交易日
日均盈亏
${stats.avg_daily_pnl >= 0 ? '+' : ''}${stats.avg_daily_pnl}
最大连盈 ${stats.max_consecutive_wins} / 连亏 ${stats.max_consecutive_losses}
`; } async function trLoadRecords() { try { const params = trBuildParams({ page: trCurrentPage, page_size: 50 }); const res = await fetch(`${TR_API_BASE}/records?${params}`); const json = await res.json(); if (json.success) trRenderRecords(json.data); } catch (e) { console.error('加载记录失败', e); } } function trRenderRecords(data) { const tbody = document.getElementById('tr-records-body'); const emptyEl = document.getElementById('tr-records-empty'); const tableWrap = document.getElementById('tr-records-table-wrap'); const pagination = document.getElementById('tr-records-pagination'); if (!data.records || data.records.length === 0) { tableWrap.style.display = 'none'; pagination.style.display = 'none'; emptyEl.style.display = 'flex'; return; } tableWrap.style.display = ''; emptyEl.style.display = 'none'; tbody.innerHTML = data.records.map(r => { const pnlClass = r.close_pnl > 0 ? 'tr-pnl-positive' : (r.close_pnl < 0 ? 'tr-pnl-negative' : ''); const dirClass = r.direction === '买' ? 'tr-dir-buy' : 'tr-dir-sell'; return ` ${r.trade_date || '-'} ${r.trade_time || '-'} ${r.trade_type} ${r.symbol} ${r.symbol_name || r.variety} ${r.direction} ${r.offset || '-'} ${r.price != null ? r.price.toFixed(2) : '-'} ${r.volume || '-'} ${r.amount != null ? r.amount.toFixed(2) : '-'} ${r.close_pnl != null ? r.close_pnl.toFixed(2) : '-'} ${r.commission != null ? r.commission.toFixed(2) : '-'} ${(r.source_file || '').substring(0, 15)} `; }).join(''); // 分页 const totalPages = Math.ceil(data.total / data.page_size); pagination.style.display = totalPages > 1 ? 'flex' : 'none'; pagination.innerHTML = ` ${trCurrentPage} / ${totalPages} (共 ${data.total} 条) `; } 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) { alert(json.message); trCurrentPage = 1; trLoadAllData(); } else { alert('导入失败: ' + (json.message || '未知错误')); } } catch (err) { alert('导入失败: ' + err.message); } finally { btn.disabled = false; btn.querySelector('span:last-child').textContent = '导入结算单'; } } 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); const data = json.data; if (data.total_futures > 0 || data.total_options > 0) { trCurrentPage = 1; trLoadAllData(); } } 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 statusIcon, statusColor; if (!d.success) { statusIcon = '❌'; statusColor = 'var(--color-error, #ef4444)'; } else if (d.new_count > 0 && d.skipped_count > 0) { statusIcon = '⚡'; statusColor = 'var(--color-warning, #f59e0b)'; } else if (d.new_count > 0) { statusIcon = '✅'; statusColor = 'var(--color-success, #22c55e)'; } else { statusIcon = '⚠️'; statusColor = 'var(--color-warning, #f59e0b)'; } return `
${statusIcon} ${d.filename}
${d.message}
`; }).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 === 0) { 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(); trCurrentPage = 1; trLoadAllData(); } } catch (e) { alert('删除失败: ' + e.message); } } function trCloseBatchModal() { document.getElementById('tr-batch-modal').classList.remove('show'); } async function trDeleteByDate() { const startDate = document.getElementById('tr-start-date').value; const endDate = document.getElementById('tr-end-date').value; if (!startDate || !endDate) { alert('请先选择要删除的日期范围'); return; } if (startDate !== endDate) { alert('删除功能仅支持单个日期,请确保开始日期和结束日期相同'); return; } const tradeDate = startDate; // 第一次确认:查询该日期的记录数 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; } // 第一次确认弹窗 const firstConfirm = confirm( `⚠️ 警告:即将删除交易数据\n\n` + `交易日:${tradeDate}\n` + `记录数量:${recordCount} 条\n\n` + `此操作将永久删除该日期的所有交易记录,不可恢复!\n\n` + `点击"确定"继续,点击"取消"返回` ); if (!firstConfirm) return; // 第二次确认弹窗(更强烈的警告) const secondConfirm = confirm( `🔴 最终确认\n\n` + `您确定要删除 ${tradeDate} 的 ${recordCount} 条交易记录吗?\n\n` + `删除后无法恢复,请确认!` ); if (!secondConfirm) 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}`); trCurrentPage = 1; trLoadAllData(); } else { alert(json.message || '删除失败'); } } catch (e) { alert('删除失败: ' + e.message); } } // ==================== 每日分析 ==================== async function trLoadDailySummary() { try { const res = await fetch(`${TR_API_BASE}/daily-summary?${trBuildParams()}`); const json = await res.json(); if (json.success) trRenderDailySummary(json.data); } catch (e) { console.error('加载每日汇总失败', e); } } function trRenderDailySummary(data) { const tbody = document.getElementById('tr-daily-body'); const emptyEl = document.getElementById('tr-daily-empty'); const chartDom = document.getElementById('tr-daily-chart'); if (!data || data.length === 0) { tbody.innerHTML = ''; emptyEl.style.display = 'flex'; chartDom.style.display = 'none'; return; } emptyEl.style.display = 'none'; chartDom.style.display = ''; tbody.innerHTML = data.map(d => { const pnlClass = d.total_pnl > 0 ? 'tr-pnl-positive' : (d.total_pnl < 0 ? 'tr-pnl-negative' : ''); return ` ${d.trade_date} ${d.total_trades} ${d.total_pnl >= 0 ? '+' : ''}${d.total_pnl} ${d.total_commission} ${d.win_count} ${d.loss_count} ${d.win_rate}% ${d.max_win > 0 ? '+' + d.max_win : '-'} ${d.max_loss < 0 ? d.max_loss : '-'} ${d.variety_count} `; }).join(''); // 渲染柱状图 trRenderDailyChart(data); } function trRenderDailyChart(data) { const chartDom = document.getElementById('tr-daily-chart'); const chart = echarts.init(chartDom); chart.setOption({ backgroundColor: 'transparent', tooltip: { trigger: 'axis' }, grid: { left: 60, right: 20, top: 20, bottom: 40 }, xAxis: { type: 'category', data: data.map(d => d.trade_date), axisLabel: { fontSize: 10, rotate: 30 } }, yAxis: { type: 'value', axisLabel: { fontSize: 11 } }, series: [{ type: 'bar', data: data.map(d => ({ value: d.total_pnl, itemStyle: { color: d.total_pnl >= 0 ? '#34C759' : '#FF3B30' }, })), }], }); } // ==================== 品种汇总 ==================== async function trLoadVarietySummary() { try { const res = await fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`); const json = await res.json(); if (json.success) trRenderVarietySummary(json.data); } catch (e) { console.error('加载品种汇总失败', e); } } function trRenderVarietySummary(data) { const tbody = document.getElementById('tr-variety-body'); const emptyEl = document.getElementById('tr-variety-empty'); if (!data || data.length === 0) { tbody.innerHTML = ''; emptyEl.style.display = 'flex'; return; } emptyEl.style.display = 'none'; tbody.innerHTML = data.map(v => { const pnlClass = v.total_pnl > 0 ? 'tr-pnl-positive' : (v.total_pnl < 0 ? 'tr-pnl-negative' : ''); return ` ${v.variety} ${v.symbol_name || '-'} ${v.total_trades} ${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl} ${v.total_close_pnl} ${v.total_commission} ${v.total_volume} ${v.buy_count} ${v.sell_count} ${v.win_rate}% `; }).join(''); } // ==================== 逐笔分析 ==================== async function trLoadTradePairs() { try { const res = await fetch(`${TR_API_BASE}/trade-pairs?${trBuildParams()}`); const json = await res.json(); if (json.success) trRenderTradePairs(json.data); } catch (e) { console.error('加载逐笔数据失败', e); } } function trRenderTradePairs(pairs) { const grid = document.getElementById('tr-pairs-grid'); const emptyEl = document.getElementById('tr-pairs-empty'); if (!pairs || pairs.length === 0) { grid.innerHTML = ''; emptyEl.style.display = 'flex'; return; } emptyEl.style.display = 'none'; grid.innerHTML = pairs.map(p => { const pnlClass = p.net_pnl >= 0 ? 'profit' : 'loss'; const dirText = p.direction === '多' ? '做多' : '做空'; const dirClass = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell'; const pairData = JSON.stringify(p).replace(/"/g, '"'); return `
${p.symbol_name || p.symbol} ${dirText} ${p.net_pnl >= 0 ? '+' : ''}${p.net_pnl}
开仓${p.open_date} ${p.open_time || ''}
平仓${p.close_date || '-'} ${p.close_time || ''}
开仓价${p.open_price != null ? p.open_price.toFixed(2) : '-'}
平仓价${p.close_price != null ? p.close_price.toFixed(2) : '-'}
手数${p.volume || '-'}
手续费${p.commission}
`; }).join(''); } async function trAnalyzeTrade(pair) { const modal = document.getElementById('tr-analysis-modal'); const title = document.getElementById('tr-analysis-title'); const content = document.getElementById('tr-analysis-content'); title.textContent = `AI 分析 - ${pair.symbol_name || pair.symbol} ${pair.direction}`; content.textContent = '正在分析中,请稍候...'; modal.classList.add('show'); 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) { content.textContent = json.data.analysis; } else { content.textContent = json.message || '分析失败'; } } catch (e) { content.textContent = '分析请求失败: ' + e.message; } } function trCloseAnalysisModal() { document.getElementById('tr-analysis-modal').classList.remove('show'); } // ==================== 交易详情弹窗 ==================== let trDetailChart = null; let trCurrentPair = null; let trCurrentPeriod = 'daily'; // 当前K线周期 function trSwitchKlinePeriod(period) { trCurrentPeriod = period; // 更新标签页样式 document.querySelectorAll('.tr-kline-tab').forEach(btn => { btn.classList.toggle('active', btn.dataset.period === period); }); // 重新加载K线 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 === '多' ? '做多' : '做空'} 交易详情`; // 渲染交易信息 const pnlClass = pair.net_pnl >= 0 ? 'profit' : 'loss'; info.innerHTML = `
品种 ${pair.symbol_name || pair.symbol}
方向 ${pair.direction === '多' ? '做多' : '做空'}
开仓日期 ${pair.open_date || '-'}
开仓时间 ${pair.open_time || '-'}
开仓价 ${pair.open_price != null ? pair.open_price.toFixed(2) : '-'}
平仓日期 ${pair.close_date || '-'}
平仓时间 ${pair.close_time || '-'}
平仓价 ${pair.close_price != null ? pair.close_price.toFixed(2) : '-'}
手数 ${pair.volume || '-'}
手续费 ${pair.commission || '-'}
净盈亏 ${pair.net_pnl >= 0 ? '+' : ''}${pair.net_pnl || 0}
`; // 重置AI分析区域 aiContent.innerHTML = ``; document.getElementById('tr-detail-ai-btn').addEventListener('click', () => trAnalyzeInDetail(pair)); modal.classList.add('show'); // 绑定刷新K线按钮事件 document.getElementById('tr-detail-reload-kline').onclick = () => trLoadDetailKline(pair); // 加载K线 await trLoadDetailKline(pair); } async function trLoadDetailKline(pair) { const period = trCurrentPeriod; const chartDom = document.getElementById('tr-detail-kline'); console.log('[K线加载] 开始加载K线数据', { symbol: pair.symbol, variety: pair.variety, period: period, open_date: pair.open_date, close_date: pair.close_date, }); try { // 构建日期范围:开仓日前7天到平仓日后7天,确保交易前后都有K线数据 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 params = new URLSearchParams({ start_date: startDate.toISOString().slice(0, 10), end_date: endDate.toISOString().slice(0, 10), period: period, }); const url = `${TR_API_BASE}/kline-with-trades/${pair.symbol}?${params}`; console.log('[K线加载] 请求URL:', url); const res = await fetch(url); const json = await res.json(); console.log('[K线加载] API响应:', { success: json.success, message: json.message, hasData: !!json.data, symbol: json.data?.symbol, candlesCount: json.data?.candles?.length || 0, markersCount: json.data?.trade_markers?.length || 0, }); if (!json.success) { console.warn('[K线加载] API返回失败:', json.message); chartDom.innerHTML = `
暂无K线数据: ${json.message || '未知错误'}
`; if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; } return; } if (!json.data.candles || json.data.candles.length === 0) { console.warn('[K线加载] K线数据为空'); chartDom.innerHTML = '
暂无K线数据
'; if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; } return; } console.log('[K线加载] 开始渲染K线图'); trRenderDetailKline(json.data, pair); console.log('[K线加载] K线图渲染完成'); } catch (e) { console.error('[K线加载] 加载K线失败:', e); chartDom.innerHTML = `
加载K线失败: ${e.message}
`; } } function trRenderDetailKline(data, pair) { if (trDetailChart) trDetailChart.dispose(); const chartDom = document.getElementById('tr-detail-kline'); trDetailChart = echarts.init(chartDom, 'dark'); const candles = data.candles; const dates = candles.map(c => c[0]); const values = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]); const volumes = candles.map(c => [parseInt(c[5]), parseFloat(c[2]) >= parseFloat(c[1]) ? 1 : -1]); // 计算MA均线 const ma5 = calculateMA(candles, 5); const ma10 = calculateMA(candles, 10); const ma20 = calculateMA(candles, 20); // 构建买卖标记(每根K线柱只保留一对买/卖标记) const buyMarkers = []; const sellMarkers = []; const markers = data.trade_markers || []; const period = data.period || 'daily'; // 按K线索引分组,每根K线只保留第一个买和第一个卖 const buyByIndex = {}; // { candleIndex: marker } const sellByIndex = {}; markers.forEach(m => { let dateIdx = -1; if (period === 'daily') { // 日线:按日期匹配 dateIdx = dates.indexOf(m.date); } else { // 分钟线:按时间戳匹配(找到最接近的K线) const tradeTime = new Date(`${m.date} ${m.time || '00:00:00'}`).getTime(); let minDiff = Infinity; for (let i = 0; i < candles.length; i++) { const candleTime = new Date(candles[i][0]).getTime(); const diff = Math.abs(candleTime - tradeTime); if (diff < minDiff) { minDiff = diff; dateIdx = i; } } } if (dateIdx === -1) return; // 按K线索引去重,只保留第一个 if (m.direction === '买') { if (buyByIndex[dateIdx] === undefined) { buyByIndex[dateIdx] = m; } } else { if (sellByIndex[dateIdx] === undefined) { sellByIndex[dateIdx] = m; } } }); // 生成标记数据 let minMarkerIdx = Infinity; let maxMarkerIdx = -Infinity; Object.entries(buyByIndex).forEach(([idx, m]) => { const dateIdx = parseInt(idx); const candle = candles[dateIdx]; const price = parseFloat(m.price); const low = parseFloat(candle[3]); const high = parseFloat(candle[4]); const markerPrice = Math.max(low, Math.min(high, price)); buyMarkers.push({ name: `买${m.offset || ''}`, coord: [dateIdx, markerPrice], value: `买${m.offset || ''} ${price}`, itemStyle: { color: '#10b981' }, }); minMarkerIdx = Math.min(minMarkerIdx, dateIdx); maxMarkerIdx = Math.max(maxMarkerIdx, dateIdx); }); Object.entries(sellByIndex).forEach(([idx, m]) => { const dateIdx = parseInt(idx); const candle = candles[dateIdx]; const price = parseFloat(m.price); const low = parseFloat(candle[3]); const high = parseFloat(candle[4]); const markerPrice = Math.max(low, Math.min(high, price)); sellMarkers.push({ name: `卖${m.offset || ''}`, coord: [dateIdx, markerPrice], value: `卖${m.offset || ''} ${price}`, itemStyle: { color: '#ef4444' }, }); minMarkerIdx = Math.min(minMarkerIdx, dateIdx); maxMarkerIdx = Math.max(maxMarkerIdx, dateIdx); }); // 计算dataZoom范围:确保至少显示50根K线,并自动定位到买卖点区域 let zoomStart = 0; let zoomEnd = 100; const totalLen = candles.length; const MIN_CANDLES = 50; // 最少显示50根K线 if (minMarkerIdx !== Infinity && maxMarkerIdx !== -Infinity) { const markerRange = maxMarkerIdx - minMarkerIdx; // 计算需要显示的范围:至少50根,或者买卖点范围+前后各20根 let displayCount = Math.max(MIN_CANDLES, markerRange + 40); displayCount = Math.min(displayCount, totalLen); // 不超过总长度 // 以买卖点中心为基准,前后扩展 const centerIdx = Math.floor((minMarkerIdx + maxMarkerIdx) / 2); let startIdx = centerIdx - Math.floor(displayCount / 2); let endIdx = startIdx + displayCount - 1; // 边界调整 if (startIdx < 0) { startIdx = 0; endIdx = Math.min(displayCount - 1, totalLen - 1); } if (endIdx >= totalLen) { endIdx = totalLen - 1; startIdx = Math.max(0, endIdx - displayCount + 1); } zoomStart = Math.floor((startIdx / totalLen) * 100); zoomEnd = Math.ceil(((endIdx + 1) / totalLen) * 100); } else { // 没有买卖点,显示全部数据 zoomStart = 0; zoomEnd = 100; } 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: '66%', height: '24%' }, ], xAxis: [ { type: 'category', data: dates, gridIndex: 0, boundaryGap: true, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, axisLabel: { show: false }, splitLine: { show: false }, }, { type: 'category', data: dates, gridIndex: 1, boundaryGap: true, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }, axisLabel: { color: '#64748b', fontSize: 10 }, splitLine: { show: false }, }, ], yAxis: [ { scale: true, gridIndex: 0, axisLine: { show: false }, axisLabel: { color: '#64748b' }, splitLine: { show: false }, }, { scale: true, gridIndex: 1, axisLine: { show: false }, axisLabel: { show: false }, splitLine: { show: false }, }, ], series: [ { name: 'K线', type: 'candlestick', data: values, xAxisIndex: 0, yAxisIndex: 0, itemStyle: { color: '#10b981', color0: '#ef4444', borderColor: '#10b981', borderColor0: '#ef4444', }, markPoint: { symbol: 'triangle', symbolSize: 12, data: buyMarkers, label: { show: true, formatter: '{b}', fontSize: 10, color: '#10b981' }, }, }, { name: 'K线', type: 'candlestick', data: values, xAxisIndex: 0, yAxisIndex: 0, itemStyle: { color: '#10b981', color0: '#ef4444', borderColor: '#10b981', borderColor0: '#ef4444', }, markPoint: { symbol: 'triangle', symbolSize: 12, symbolRotate: 180, data: sellMarkers, label: { show: true, formatter: '{b}', fontSize: 10, color: '#ef4444' }, }, }, { name: 'MA5', type: 'line', data: ma5, xAxisIndex: 0, yAxisIndex: 0, lineStyle: { width: 1, color: '#f59e0b' }, symbol: 'none', }, { name: 'MA10', type: 'line', data: ma10, xAxisIndex: 0, yAxisIndex: 0, lineStyle: { width: 1, color: '#3b82f6' }, symbol: 'none', }, { name: 'MA20', type: 'line', data: ma20, xAxisIndex: 0, yAxisIndex: 0, 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)' } })), }, ], dataZoom: [ { type: 'inside', xAxisIndex: [0, 1], start: zoomStart, end: zoomEnd }, { show: true, type: 'slider', xAxisIndex: [0, 1], bottom: 5, height: 16, start: zoomStart, end: zoomEnd, borderColor: 'transparent', backgroundColor: 'rgba(15, 20, 30, 0.5)', fillerColor: 'rgba(6, 182, 212, 0.15)', handleStyle: { color: '#06b6d4' }, textStyle: { color: '#64748b' }, }, ], }; trDetailChart.setOption(option); } 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.innerHTML = '
正在分析中,请稍候...
'; 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 = `
${json.data.analysis}
`; } else { aiContent.innerHTML = `
${json.message || '分析失败'}
`; } } catch (e) { aiContent.innerHTML = `
分析请求失败: ${e.message}
`; } } // 子页面切换时懒加载 document.addEventListener('click', function(e) { if (!e.target.classList.contains('tr-sub-nav-item')) return; const sub = e.target.dataset.sub; setTimeout(() => { if (sub === 'daily') trLoadDailySummary(); else if (sub === 'variety') trLoadVarietySummary(); else if (sub === 'pairs') trLoadTradePairs(); }, 50); }); 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(''); }