You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/app/static/futures_analysis.js

1745 lines
72 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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() {
document.getElementById('back-btn').addEventListener('click', showListView);
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
document.getElementById('suggestion-card').addEventListener('click', function() {
if (currentDetailData) {
showSuggestionModal(currentDetailData);
}
});
// 刷新全部按钮
document.getElementById('refresh-all-btn').addEventListener('click', refreshAllSymbols);
// 详情页刷新按钮
document.getElementById('refresh-symbol-btn').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);
});
});
document.getElementById('search-input').addEventListener('input', function() {
filterFuturesList(this.value);
});
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.addEventListener('click', function() {
document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
filterByCategory(this.dataset.category);
});
});
document.getElementById('sort-select').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;
}
}
function showDetailView(symbol) {
currentSymbol = symbol;
document.getElementById('list-view').classList.remove('active');
document.getElementById('detail-view').classList.add('active');
loadFuturesDetail(symbol);
loadKlineData(symbol, currentPeriod);
loadHistoryList(symbol);
loadAIAnalysis();
}
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);
}
} 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);
}
}
document.getElementById('count-watched').textContent = watchedSymbols.length;
const activeTab = document.querySelector('.filter-tab.active');
if (activeTab && activeTab.dataset.category === 'watched') {
filterByCategory('watched');
} else {
renderFuturesGrid(getCurrentFilteredData());
}
} catch (error) {
console.error('切换自选失败:', error);
}
}
function getCurrentFilteredData() {
const activeTab = document.querySelector('.filter-tab.active');
const category = activeTab ? activeTab.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 {
const response = await fetch(`${API_BASE}/list`);
const data = await response.json();
if (data.success) {
allFuturesData = data.data;
renderFuturesGrid(allFuturesData);
updateStats(allFuturesData);
}
} catch (error) {
console.error('加载品种列表失败:', error);
loadFuturesFromConfig();
}
}
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');
if (data.length === 0) {
grid.innerHTML = '<div class="empty-state">暂无数据</div>';
return;
}
grid.innerHTML = data.map(item => {
const isWatched = watchedSymbols.includes(item.symbol);
return `
<div class="futures-card" onclick="showDetailView('${item.symbol}')">
<div class="card-top">
<div class="card-symbol">
<div class="symbol-tag">${item.symbol.replace(/[0-9]/g, '').substring(0, 2)}</div>
<div>
<div class="card-name">${item.name}</div>
<div class="card-code">${item.symbol}</div>
</div>
</div>
<div class="card-price">
<div class="price-value ${item.change >= 0 ? 'up' : 'down'}">¥${formatNumber(item.price)}</div>
<div class="price-change ${item.change >= 0 ? 'up' : 'down'}">
<i class="fas fa-arrow-${item.change >= 0 ? 'up' : 'down'}"></i>
${item.change >= 0 ? '+' : ''}${formatNumber(item.change)} (${item.changePct >= 0 ? '+' : ''}${item.changePct.toFixed(2)}%)
</div>
</div>
</div>
<span class="suggestion-badge ${item.suggestionType}">${item.suggestion}</span>
<div class="card-metrics">
<div class="metric-item">
<span class="metric-label">成功率</span>
<div class="metric-bar"><div class="metric-fill ${item.successRate >= 70 ? 'up' : item.successRate >= 60 ? 'orange' : 'down'}" style="width: ${item.successRate}%"></div></div>
<span class="metric-value ${item.successRate >= 70 ? 'up' : item.successRate >= 60 ? '' : 'down'}">${item.successRate}%</span>
</div>
<div class="metric-item">
<span class="metric-label">趋势评分</span>
<div class="metric-bar"><div class="metric-fill ${item.trendScore >= 70 ? 'up' : item.trendScore >= 50 ? 'orange' : 'down'}" style="width: ${item.trendScore}%"></div></div>
<span class="metric-value ${item.trendScore >= 70 ? 'up' : item.trendScore >= 50 ? '' : 'down'}">${item.trendScore}</span>
</div>
</div>
<div class="period-trends">
<span class="period-tag ${item.periods['5']}">5M</span>
<span class="period-tag ${item.periods['15']}">15M</span>
<span class="period-tag ${item.periods['30']}">30M</span>
<span class="period-tag ${item.periods['60']}">1H</span>
</div>
<div class="card-footer">
<div class="key-levels">
<span><span class="label">压力</span> <span class="down">${formatNumber(item.resistance)}</span></span>
<span><span class="label">支撑</span> <span class="up">${formatNumber(item.support)}</span></span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<button class="card-refresh-btn" onclick="event.stopPropagation(); refreshSingleSymbol('${item.symbol}', this)" title="刷新数据">
<i class="fas fa-sync-alt"></i>
</button>
<button class="watch-btn ${isWatched ? 'active' : ''}" onclick="toggleWatch('${item.symbol}', '${item.name}', event)" title="${isWatched ? '取消自选' : '加入自选'}">
<i class="fas fa-star"></i>
</button>
<span class="detail-link"><i class="fas fa-arrow-right"></i> 详情</span>
</div>
</div>
</div>
`}).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;
const upCount = data.filter(d => d.change > 0).length;
const downCount = data.filter(d => d.change < 0).length;
const neutralCount = total - upCount - downCount;
document.getElementById('total-count').textContent = total;
document.getElementById('up-count').textContent = upCount;
document.getElementById('down-count').textContent = downCount;
document.getElementById('neutral-count').textContent = neutralCount;
document.getElementById('count-all').textContent = total;
document.getElementById('count-watched').textContent = watchedSymbols.length;
}
function filterFuturesList(keyword) {
keyword = keyword.toLowerCase();
const activeTab = document.querySelector('.filter-tab.active');
const category = activeTab ? activeTab.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) {
document.getElementById('detail-name').textContent = data.name || '--';
document.getElementById('detail-symbol').textContent = data.symbol || '--';
const priceEl = document.getElementById('detail-price');
priceEl.textContent = '¥' + formatNumber(data.price);
priceEl.className = 'price-value ' + (data.change >= 0 ? 'up' : 'down');
const changeEl = document.getElementById('detail-change');
changeEl.className = 'price-change ' + (data.change >= 0 ? 'up' : 'down');
changeEl.innerHTML = `<i class="fas fa-arrow-${data.change >= 0 ? 'up' : 'down'}"></i> ${data.change >= 0 ? '+' : ''}${formatNumber(data.change)} (${data.changePct >= 0 ? '+' : ''}${data.changePct.toFixed(2)}%)`;
document.getElementById('detail-open').textContent = formatNumber(data.open);
document.getElementById('detail-high').textContent = formatNumber(data.high);
document.getElementById('detail-low').textContent = formatNumber(data.low);
document.getElementById('detail-volume').textContent = formatNumber(data.volume);
const r1 = data.resistances ? data.resistances[0] : data.resistance;
const s1 = data.supports ? data.supports[0] : data.support;
document.getElementById('detail-r1').textContent = `R1: ${formatNumber(r1)} (${calcPriceChangePercent(data.price, r1)})`;
document.getElementById('detail-s1').textContent = `S1: ${formatNumber(s1)} (${calcPriceChangePercent(data.price, s1)})`;
const badge = document.getElementById('suggestion-badge');
badge.textContent = data.suggestion || '--';
badge.className = 'suggestion-badge ' + (data.suggestionType || 'neutral');
document.getElementById('suggestion-reason').textContent = data.suggestionReason || '--';
document.getElementById('entry-price').textContent = formatNumber(data.entryPrice);
document.getElementById('target-price').textContent = formatNumber(data.targetPrice);
document.getElementById('stop-loss').textContent = formatNumber(data.stopLoss);
document.getElementById('risk-level').textContent = data.riskLevel || '--';
if (data.macd) {
document.getElementById('macd-signal').textContent = data.macd.signal;
document.getElementById('macd-detail').textContent = data.macd.detail;
}
if (data.rsi) {
document.getElementById('rsi-value').textContent = data.rsi.value;
document.getElementById('rsi-status').textContent = data.rsi.status;
}
if (data.boll) {
document.getElementById('boll-signal').textContent = data.boll.signal;
document.getElementById('boll-detail').textContent = data.boll.detail;
}
if (data.kdj) {
document.getElementById('kdj-signal').textContent = data.kdj.signal;
document.getElementById('kdj-detail').textContent = data.kdj.detail;
}
if (data.resistances) {
for (let i = 0; i < 2; i++) {
const el = document.getElementById(`resistance-${i + 1}`);
if (el) {
el.querySelector('span:last-child').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.querySelector('span:last-child').textContent = formatNumber(data.supports[i]);
}
}
}
if (data.pivotPoint) {
document.getElementById('pivot-point').querySelector('span:last-child').textContent = formatNumber(data.pivotPoint);
}
if (data.periodConsistency) {
const container = document.getElementById('period-trends');
const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' };
container.innerHTML = Object.entries(data.periodConsistency).map(([period, trend]) => `
<div class="trend-row">
<span class="trend-period">${periodNames[period]}</span>
<span class="trend-badge ${trend}">
${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
</span>
</div>
`).join('');
}
if (data.trendScore !== undefined) {
document.getElementById('trend-score').textContent = data.trendScore;
const circle = document.getElementById('score-fill');
const circumference = 2 * Math.PI * 45;
const offset = circumference - (data.trendScore / 100) * circumference;
circle.style.strokeDasharray = circumference;
circle.style.strokeDashoffset = offset;
}
}
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 = '<div class="empty-state">暂无历史记录</div>';
}
}
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 = `
<div class="ai-analysis-placeholder">
<i class="fas fa-brain"></i>
<p>点击"智能分析"按钮获取AI分析结果</p>
</div>
`;
}
} catch (error) {
console.error(`加载合约 ${currentSymbol} AI分析失败:`, error);
content.innerHTML = `
<div class="ai-analysis-placeholder">
<i class="fas fa-brain"></i>
<p>点击"智能分析"按钮获取AI分析结果</p>
</div>
`;
}
}
function renderHistoryList(records) {
const container = document.getElementById('history-list');
if (!records || records.length === 0) {
container.innerHTML = '<div class="empty-state">暂无历史记录</div>';
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 `
<div class="history-item" onclick="showAIHistoryDetail(${record.id})">
<div class="history-item-left">
<span class="history-time">${timeStr}</span>
<span class="history-suggestion">${summary.substring(0, 30)}${summary.length > 30 ? '...' : ''}</span>
<span class="history-score">方向: ${direction} | 置信度: ${confidence}%</span>
</div>
<div class="history-item-right">
<button class="history-detail-btn" onclick="event.stopPropagation(); showAIHistoryDetail(${record.id})">
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
`;
}).join('');
}
function showSuggestionModal(data) {
const body = document.getElementById('suggestion-modal-body');
body.innerHTML = `
<div class="modal-suggestion-main">
<div class="suggestion-badge ${data.suggestionType || 'neutral'}" style="font-size:24px;font-weight:700;">${data.suggestion || '--'}</div>
<div class="suggestion-reason" style="margin-top:8px;">${data.suggestionReason || '--'}</div>
</div>
<div class="modal-params-grid">
<div class="modal-param-card">
<span class="param-label">建议入场</span>
<span class="param-value">${formatNumber(data.entryPrice)}</span>
</div>
<div class="modal-param-card">
<span class="param-label">目标价位</span>
<span class="param-value up">${formatNumber(data.targetPrice)}</span>
</div>
<div class="modal-param-card">
<span class="param-label">止损价位</span>
<span class="param-value down">${formatNumber(data.stopLoss)}</span>
</div>
<div class="modal-param-card">
<span class="param-label">风险等级</span>
<span class="param-value">${data.riskLevel || '--'}</span>
</div>
</div>
`;
document.getElementById('suggestion-modal').classList.add('active');
}
function showHistoryModal(record) {
const body = document.getElementById('history-modal-body');
body.innerHTML = `
<div class="modal-section">
<div class="modal-section-title"><i class="fas fa-robot"></i> AI交易建议</div>
<div class="modal-suggestion-main" style="margin-bottom:0;">
<div class="suggestion-badge ${record.suggestion_type || 'neutral'}" style="font-size:20px;font-weight:700;">${record.suggestion || '--'}</div>
</div>
<div class="modal-params-grid" style="margin-top:12px;">
<div class="modal-param-card">
<span class="param-label">入场</span>
<span class="param-value">${formatNumber(record.entry_price)}</span>
</div>
<div class="modal-param-card">
<span class="param-label">目标</span>
<span class="param-value up">${formatNumber(record.target_price)}</span>
</div>
<div class="modal-param-card">
<span class="param-label">止损</span>
<span class="param-value down">${formatNumber(record.stop_loss)}</span>
</div>
<div class="modal-param-card">
<span class="param-label">风险</span>
<span class="param-value">${record.risk_level || '--'}</span>
</div>
</div>
</div>
<div class="modal-section">
<div class="modal-section-title"><i class="fas fa-wave-pulse"></i> 技术指标</div>
<div class="modal-indicators-grid">
<div class="modal-indicator-item">
<span class="indicator-label">MACD</span>
<span class="indicator-value">${record.macd_signal || '--'}</span>
</div>
<div class="modal-indicator-item">
<span class="indicator-label">RSI</span>
<span class="indicator-value">${record.rsi_value || '--'}</span>
</div>
<div class="modal-indicator-item">
<span class="indicator-label">BOLL</span>
<span class="indicator-value">${record.boll_signal || '--'}</span>
</div>
<div class="modal-indicator-item">
<span class="indicator-label">KDJ</span>
<span class="indicator-value">${record.kdj_signal || '--'}</span>
</div>
</div>
</div>
<div class="modal-section">
<div class="modal-section-title"><i class="fas fa-crosshairs"></i> 关键点位</div>
<div class="modal-levels-list">
${(record.resistance_levels || []).map((v, i) => `
<div class="modal-level-row">
<span class="level-label">压力${i + 1}</span>
<span class="level-value down">${formatNumber(v)}</span>
</div>
`).join('')}
${record.pivot_point ? `
<div class="modal-level-row" style="background:rgba(139,92,246,0.1);border-radius:8px;">
<span class="level-label" style="color:#8b5cf6;font-weight:600;">中枢 (PP)</span>
<span class="level-value" style="color:#8b5cf6;">${formatNumber(record.pivot_point)}</span>
</div>
` : ''}
${(record.support_levels || []).map((v, i) => `
<div class="modal-level-row">
<span class="level-label">支撑${i + 1}</span>
<span class="level-value up">${formatNumber(v)}</span>
</div>
`).join('')}
</div>
</div>
<div class="modal-section">
<div class="modal-section-title"><i class="fas fa-timeline"></i> 多周期趋势</div>
<div class="modal-trends-list">
${Object.entries(record.period_trends || {}).map(([period, trend]) => {
const names = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' };
return `<div class="modal-trend-row">
<span class="modal-trend-period">${names[period] || period}</span>
<span class="modal-trend-badge ${trend}">${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}</span>
</div>`;
}).join('')}
</div>
</div>
<div class="modal-section">
<div class="modal-section-title"><i class="fas fa-gauge-high"></i> </div>
<div class="modal-param-card" style="text-align:center;">
<span class="param-value" style="font-size:32px;">${record.trend_score || '--'}</span>
<span class="param-label" style="display:block;margin-top:4px;">综合评分</span>
</div>
</div>
`;
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 = `
<div class="ai-history-detail">
<div class="detail-header">
<h4><i class="fas fa-file-alt"></i> ${record.symbol} AI</h4>
<span class="detail-time"><i class="fas fa-clock"></i> ${timestamp}</span>
</div>
<div class="detail-summary">
<i class="fas fa-quote-left"></i>
<p>${result.summary || '暂无总结'}</p>
</div>
<!-- AI交易建议卡片已隐藏 -->
<!-- <div class="detail-suggestion">
<div class="suggestion-card ${directionClass}">
<i class="fas ${directionIcon}"></i>
<span class="suggestion-text">${direction}</span>
<span class="confidence-text">置信度: ${confidence}%</span>
</div>
</div> -->
${result.four_dimensional ? `
<div class="detail-section">
<h5><i class="fas fa-brain"></i> AI思维分析</h5>
<table class="four-d-table">
<thead>
<tr>
<th>周期</th>
<th>MACD趋势</th>
<th>成交量</th>
<th>KDJ状态</th>
<th>结论</th>
</tr>
</thead>
<tbody>
${(() => {
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]) => `
<tr>
<td><strong>${periodNames[period] || period}</strong></td>
<td>${d.macd?.trend || '--'}</td>
<td>${d.volume?.status || '--'}</td>
<td>${d.kdj?.status || '--'}</td>
<td>${d.conclusion || '--'}</td>
</tr>
`).join('');
})()}
</tbody>
</table>
</div>
` : ''}
<div class="detail-metrics">
<div class="metric-card">
<span class="metric-label">入场区间</span>
<span class="metric-value">${result.trading_suggestion?.entry_range?.min || '--'}-${result.trading_suggestion?.entry_range?.max || '--'}</span>
</div>
<div class="metric-card">
<span class="metric-label">止损位</span>
<span class="metric-value down">${result.trading_suggestion?.stop_loss || '--'}</span>
</div>
<div class="metric-card">
<span class="metric-label">建议仓位</span>
<span class="metric-value">${result.trading_suggestion?.position_size || '--'}</span>
</div>
<div class="metric-card">
<span class="metric-label">纪律评分</span>
<span class="metric-value">${result.discipline_score?.total || '--'}/${result.discipline_score?.max || '11'}</span>
</div>
</div>
${result.kdj_diagnosis ? `
<div class="detail-section">
<h5><i class="fas fa-stethoscope"></i> KDJ诊断</h5>
<div class="kdj-diagnosis-grid">
<div class="kdj-item">
<span class="kdj-label">当前状态</span>
<span class="kdj-value">${result.kdj_diagnosis.current_status || '--'}</span>
</div>
<div class="kdj-item">
<span class="kdj-label">背离</span>
<span class="kdj-value">${result.kdj_diagnosis.divergence || '--'}</span>
</div>
<div class="kdj-item">
<span class="kdj-label">钝化</span>
<span class="kdj-value">${result.kdj_diagnosis.paralysis || '--'}</span>
</div>
<div class="kdj-item" style="grid-column: 1 / -1;">
<span class="kdj-label">建议</span>
<span class="kdj-value">${result.kdj_diagnosis.recommendation || '--'}</span>
</div>
</div>
</div>
` : ''}
${result.pivot_points ? `
<div class="detail-section">
<h5><i class="fas fa-crosshairs"></i> 关键点位</h5>
<div class="pivot-points-grid">
<div class="pivot-item resistance">
<span>R2</span><strong>${result.pivot_points.r2 || '--'}</strong>
</div>
<div class="pivot-item resistance">
<span>R1</span><strong>${result.pivot_points.r1 || '--'}</strong>
</div>
<div class="pivot-item center">
<span>PP</span><strong>${result.pivot_points.pp || '--'}</strong>
</div>
<div class="pivot-item support">
<span>S1</span><strong>${result.pivot_points.s1 || '--'}</strong>
</div>
<div class="pivot-item support">
<span>S2</span><strong>${result.pivot_points.s2 || '--'}</strong>
</div>
</div>
</div>
` : ''}
${result.risk_warnings && result.risk_warnings.length > 0 ? `
<div class="detail-section">
<h5><i class="fas fa-exclamation-triangle"></i> 风险提示</h5>
<ul class="warning-list">
${result.risk_warnings.map(w => `<li>${w}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
// 显示弹窗
document.getElementById('ai-analysis-modal').classList.add('active');
} else {
showToast('error', '加载失败', data.error || '记录不存在');
}
} catch (error) {
console.error('加载历史记录详情失败:', error);
showToast('error', '加载失败', '网络错误,请稍后重试');
}
}
function renderKlineChart(data) {
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 = `
<div class="toast-icon"><i class="${iconMap[type]}"></i></div>
<div class="toast-content">
<div class="toast-title">${title}</div>
<div class="toast-message">${message}</div>
</div>
`;
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 = `<i class="fas fa-sync-alt"></i><span>刷新中 ${status.progress}/${status.total}</span>`;
setTimeout(pollRefreshStatus, 2000);
}
}
} catch (error) {
console.error('获取刷新状态失败:', error);
resetRefreshButton(btn);
}
}
function resetRefreshButton(btn) {
btn.disabled = false;
btn.classList.remove('spinning');
btn.innerHTML = '<i class="fas fa-sync-alt"></i><span>刷新全部</span>';
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 = '<i class="fas fa-spinner fa-spin"></i>';
showToast('info', '检查数据', `正在检查 ${symbol} 数据新鲜度...`);
try {
const response = await fetch(`${API_BASE}/refresh/${symbol}`, { method: 'POST' });
const data = await response.json();
if (data.success) {
if (data.refreshed) {
btn.innerHTML = '<i class="fas fa-check"></i>';
await loadFuturesDetail(symbol);
await loadKlineData(symbol, currentPeriod);
await loadFuturesList();
showToast('success', '数据已更新', `${symbol} 最新数据已同步`);
} else {
btn.innerHTML = '<i class="fas fa-check"></i>';
showToast('success', '数据新鲜', `${symbol} 数据仍在有效期内,无需刷新`);
}
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalContent;
}, 1000);
} else {
showToast('error', '刷新失败', data.message || '请稍后重试');
btn.disabled = false;
btn.innerHTML = originalContent;
}
} catch (error) {
console.error('刷新失败:', error);
showToast('error', '刷新失败', '网络错误,请稍后重试');
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.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>分析中...</span>';
content.innerHTML = `
<div class="ai-analysis-loading">
<i class="fas fa-brain"></i>
<span>AI正在分析中...</span>
</div>
`;
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 = `
<div class="ai-analysis-placeholder">
<i class="fas fa-exclamation-triangle"></i>
<p>${data.error || 'AI分析失败请稍后重试'}</p>
</div>
`;
showToast('error', '分析失败', data.error || 'AI分析失败');
}
} catch (error) {
console.error('AI分析请求失败:', error);
content.innerHTML = `
<div class="ai-analysis-placeholder">
<i class="fas fa-exclamation-triangle"></i>
<p>网络错误,请检查网络连接</p>
</div>
`;
showToast('error', '请求失败', '网络错误,请稍后重试');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-play"></i><span>智能分析</span>';
}
}
function displayAIAnalysisResult(data) {
const content = document.getElementById('ai-analysis-content');
const result = data.result;
const timestamp = new Date(data.analysis_time).toLocaleString('zh-CN');
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;
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 || '--';
content.innerHTML = `
<div class="ai-analysis-result">
<div class="ai-summary">${result.summary || '暂无总结'}</div>
<div class="ai-suggestion-row">
<div class="ai-suggestion-direction ${directionClass}">
<i class="fas ${directionIcon}"></i>
<span style="font-weight:600;">${direction}</span>
</div>
<div class="ai-confidence">
<span>置信度</span>
<div class="ai-confidence-bar">
<div class="ai-confidence-fill" style="width: ${confidence}%"></div>
</div>
<span>${confidence}%</span>
</div>
</div>
<div class="ai-key-metrics">
<div class="ai-metric-item">
<span class="label">入场区间</span>
<span class="value">${entryMin}-${entryMax}</span>
</div>
<div class="ai-metric-item">
<span class="label">止损位</span>
<span class="value down">${stopLoss}</span>
</div>
<div class="ai-metric-item">
<span class="label">建议仓位</span>
<span class="value">${positionSize}</span>
</div>
<div class="ai-metric-item">
<span class="label">纪律评分</span>
<span class="value">${result.discipline_score?.total || '--'}/${result.discipline_score?.max || '11'}</span>
</div>
</div>
<div class="ai-timestamp">
<i class="fas fa-clock"></i> 分析时间: ${timestamp}
<button class="ai-detail-btn" onclick="showAIDetailModal()" style="margin-left: 8px; background: none; border: 1px solid var(--border-color); padding: 4px 12px; border-radius: 4px; color: var(--cyan); cursor: pointer; font-size: 11px;">
查看详情 <i class="fas fa-external-link-alt"></i>
</button>
</div>
</div>
`;
// 同步AI分析数据到主面板各个卡片
syncAIToPanels(result);
}
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. 同步到AI交易建议卡片
const suggestionBadge = document.getElementById('suggestion-badge');
if (suggestionBadge) {
suggestionBadge.textContent = suggestion.direction || '--';
suggestionBadge.className = `suggestion-badge ${suggestion.direction === '做多' ? 'up' : suggestion.direction === '做空' ? 'down' : 'neutral'}`;
}
const entryPriceEl = document.getElementById('entry-price');
if (entryPriceEl) entryPriceEl.textContent = suggestion.entry_range ? `${suggestion.entry_range.min}-${suggestion.entry_range.max}` : '--';
const targetPriceEl = document.getElementById('target-price');
if (targetPriceEl) {
const takeProfit = suggestion.take_profit?.[0];
targetPriceEl.textContent = takeProfit?.price || '--';
}
const stopLossEl = document.getElementById('stop-loss');
if (stopLossEl) stopLossEl.textContent = suggestion.stop_loss || '--';
const riskLevelEl = document.getElementById('risk-level');
if (riskLevelEl) riskLevelEl.textContent = suggestion.position_size || '--';
// 2. 同步到技术指标卡片
// 从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.querySelector('span:last-child').textContent = pivotPoints.r1;
}
if (pivotPoints.r2) {
const r2El = document.getElementById('resistance-2');
if (r2El) r2El.querySelector('span:last-child').textContent = pivotPoints.r2;
}
if (pivotPoints.pp) {
const ppEl = document.getElementById('pivot-point');
if (ppEl) ppEl.querySelector('span:last-child').textContent = pivotPoints.pp;
}
if (pivotPoints.s1) {
const s1El = document.getElementById('support-1');
if (s1El) s1El.querySelector('span:last-child').textContent = pivotPoints.s1;
}
if (pivotPoints.s2) {
const s2El = document.getElementById('support-2');
if (s2El) s2El.querySelector('span:last-child').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 `<div class="trend-item"><span class="trend-period">${periodNames[period] || period}</span><span class="trend-badge ${trendClass}">${trendText}</span></div>`;
}).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]) => `
<div class="scenario-item">
<span class="scenario-name">${scenarioNames[key] || key}</span>
<span class="scenario-probability">${data.probability || 0}%</span>
<span class="scenario-action">${data.action || '--'}</span>
</div>
`).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 += `
<tr>
<td class="period-cell">${periodNames[period] || period}</td>
<td>
<div>趋势: ${data.macd?.trend || '--'}</div>
<div>位置: ${data.macd?.position || '--'}</div>
<div>柱状图: ${data.macd?.histogram || '--'}</div>
</td>
<td>
<div>状态: ${data.volume?.status || '--'}</div>
<div>量比: ${data.volume?.ratio || '--'}</div>
</td>
<td>
<div>K: ${data.kdj?.k || '--'} D: ${data.kdj?.d || '--'}</div>
<div>信号: ${data.kdj?.signal || '--'}</div>
<div>状态: ${data.kdj?.status || '--'}</div>
</td>
<td>${data.conclusion || '--'}</td>
</tr>
`;
}
});
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 += `
<div class="scenario-card ${key}">
<div class="scenario-probability">${scenario.probability || 0}%</div>
<div class="scenario-action">${scenario.action || '--'}</div>
</div>
`;
});
let redLinesHTML = '';
if (result.red_lines_check?.violated?.length > 0) {
result.red_lines_check.violated.forEach(line => {
redLinesHTML += `<div class="red-line-item"><i class="fas fa-exclamation-circle"></i> ${line}</div>`;
});
} else {
redLinesHTML = '<div class="red-line-item pass"><i class="fas fa-check-circle"></i> 未触碰交易红线</div>';
}
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 += `
<div class="discipline-item ${isPass ? 'pass' : 'fail'}">
<i class="fas ${isPass ? 'fa-check-circle' : 'fa-times-circle'}"></i>
<span class="discipline-label">${label}</span>
</div>
`;
});
let experiencesHTML = '';
const experiences = result.experience_lessons || [];
if (experiences.length > 0) {
experiences.forEach(exp => {
experiencesHTML += `<div class="experience-item"><i class="fas fa-lightbulb"></i> ${exp}</div>`;
});
} else {
experiencesHTML = '<div class="experience-item"><i class="fas fa-info-circle"></i> 暂无经验提醒</div>';
}
modalBody.innerHTML = `
<div class="ai-modal-section">
<div class="ai-modal-section-title">
<i class="fas fa-chart-bar"></i>
四维联合信号表 (4D-XV)
</div>
<table class="four-dimensional-table">
<thead>
<tr>
<th>周期</th>
<th>MACD(趋势)</th>
<th>成交量(资金)</th>
<th>KDJ(时机)</th>
<th>结论</th>
</tr>
</thead>
<tbody>
${fourDimensionalHTML || '<tr><td colspan="5">暂无数据</td></tr>'}
</tbody>
</table>
</div>
<div class="ai-modal-section">
<div class="ai-modal-section-title">
<i class="fas fa-crosshairs"></i>
关键点位 (Pivot Point)
</div>
<div class="ai-key-metrics">
<div class="ai-metric-item">
<span class="label">R2</span>
<span class="value down">${result.pivot_points?.r2 || '--'}</span>
</div>
<div class="ai-metric-item">
<span class="label">R1</span>
<span class="value down">${result.pivot_points?.r1 || '--'}</span>
</div>
<div class="ai-metric-item">
<span class="label">PP</span>
<span class="value" style="color: var(--purple);">${result.pivot_points?.pp || '--'}</span>
</div>
<div class="ai-metric-item">
<span class="label">S1</span>
<span class="value up">${result.pivot_points?.s1 || '--'}</span>
</div>
<div class="ai-metric-item">
<span class="label">S2</span>
<span class="value up">${result.pivot_points?.s2 || '--'}</span>
</div>
</div>
</div>
<div class="ai-modal-section">
<div class="ai-modal-section-title">
<i class="fas fa-balance-scale"></i>
交易红线审查
</div>
<div class="red-lines-list">
${redLinesHTML}
</div>
</div>
<div class="ai-modal-section">
<div class="ai-modal-section-title">
<i class="fas fa-check-double"></i>
11项纪律检查
</div>
<div class="discipline-grid">
${disciplineHTML}
</div>
</div>
<div class="ai-modal-section">
<div class="ai-modal-section-title">
<i class="fas fa-project-diagram"></i>
情景预案
</div>
<div class="scenario-cards">
${scenariosHTML || '<div class="scenario-card"><div class="scenario-action">暂无情景预案</div></div>'}
</div>
</div>
<div class="ai-modal-section">
<div class="ai-modal-section-title">
<i class="fas fa-lightbulb"></i>
经验教训提醒
</div>
<div class="ai-experience-list">
${experiencesHTML}
</div>
</div>
<div class="ai-modal-section">
<div class="ai-modal-section-title">
<i class="fas fa-exclamation-triangle"></i>
风险提示
</div>
<div class="ai-experience-list">
${(result.risk_warnings || []).map(w => `<div class="experience-item"><i class="fas fa-exclamation-circle"></i> ${w}</div>`).join('')}
</div>
</div>
`;
document.getElementById('ai-analysis-modal').classList.add('active');
}