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'); } }); }); } 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'); if (klineChart) { klineChart.dispose(); klineChart = null; } } 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); document.getElementById('count-watched').textContent = watchedSymbols.length; } } 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}) 已添加到自选列表`); } } document.getElementById('count-watched').textContent = watchedSymbols.length; // 重新渲染当前视图 const activePill = document.querySelector('.pill.active'); const category = activePill ? activePill.dataset.category : 'all'; if (category === 'watched') { filterByCategory('watched'); } else { renderFuturesGrid(getCurrentFilteredData()); } } catch (error) { console.error('切换自选失败:', error); showToast('error', '操作失败', '网络错误,请稍后重试'); } } 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; if (category === 'watched') { return data.filter(item => watchedSymbols.includes(item.symbol)); } 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(''); } 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' }); const data = await response.json(); if (data.success) { pollRefreshStatus(); } else { showToast('error', '刷新失败', data.message || '请稍后重试'); resetRefreshButton(btn); } } catch (error) { console.error('刷新全部失败:', 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 { btn.innerHTML = `刷新中 ${status.progress}/${status.total}`; setTimeout(pollRefreshStatus, 2000); } } } catch (error) { console.error('获取刷新状态失败:', 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.innerHTML = ' 分析中...'; showToast('info', '批量分析', '开始对所有合约进行AI分析...'); const symbols = allFuturesData.map(item => item.symbol); const batchSize = 3; // 每次并发分析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) { console.error(`${symbol} 分析失败:`, error); return { symbol, success: false, error: error.message }; } }); const results = await Promise.all(promises); const successCount = results.filter(r => r.success).length; showToast('info', '批量分析进度', `已完成 ${Math.min(i + batchSize, symbols.length)}/${symbols.length} 个合约,本批成功 ${successCount} 个`); // 每批之间等待2秒,避免API限流 if (i + batchSize < symbols.length) { await new Promise(resolve => setTimeout(resolve, 2000)); } } showToast('success', '批量分析完成', `所有 ${symbols.length} 个合约AI分析已完成`); allBtn.disabled = false; allBtn.innerHTML = originalContent; } 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}`; } }); } async function refreshAllSymbols() { const btn = document.getElementById('refresh-all-btn'); if (!btn) return; const originalContent = btn.innerHTML; btn.disabled = true; btn.innerHTML = ' 刷新中...'; showToast('info', '刷新中', '正在刷新所有合约数据...'); try { const response = await fetch(`${API_BASE}/refresh-all`); const data = await response.json(); if (data.success) { showToast('success', '刷新成功', `已刷新 ${data.count} 个合约`); loadFuturesData(); } else { showToast('error', '刷新失败', data.error || '未知错误'); } } catch (error) { showToast('error', '刷新失败', '网络错误,请稍后重试'); } finally { btn.disabled = false; btn.innerHTML = originalContent; } } // ==================== 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正在分析中...
`; try { const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}?force_refresh=${forceRefresh}`); 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) { console.error('AI分析请求失败:', error); 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'); }