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

2125 lines
86 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() {
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 = `
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
<p>点击"智能分析"按钮获取AI分析结果</p>
</div>
`;
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 toggleFav(symbol, name, event) {
event.stopPropagation();
// 调用原有的toggleWatch函数处理API请求和数据更新
// toggleWatch 会重新渲染网格,星标状态会根据 watchedSymbols 自动更新
toggleWatch(symbol, name, event);
}
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 = '<div class="empty-state">暂无数据</div>';
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 `
<div class="card" onclick="showDetailView('${item.symbol}')">
<div class="card-header">
<div class="card-left">
<div class="code-box">${code}</div>
<div class="info-group">
<div class="name-row">${item.name} <span class="fav-icon ${isWatched ? 'active' : ''}" onclick="toggleFav('${item.symbol}', '${item.name}', event)">★</span></div>
<div class="contract-code">${item.symbol}</div>
</div>
</div>
<div class="price-area">
<div class="price ${priceClass}">¥${formatNumber(item.price)}</div>
<div class="change ${changeClass}">${changeIcon} ${changeSign}${formatNumber(item.change)} (${pctSign}${item.changePct.toFixed(2)}%)</div>
</div>
</div>
<div class="action-pill ${actionPillClass}">${actionPillText}</div>
<div class="metrics">
<div class="metric">
<div class="metric-label">成功率 ${successRate}%</div>
<div class="bar-bg"><div class="bar-fill" style="width:${successRate}%; background:${successColor};"></div></div>
</div>
<div class="metric">
<div class="metric-label">趋势评分 ${trendScore}</div>
<div class="bar-bg"><div class="bar-fill" style="width:${trendScore}%; background:${trendColor};"></div></div>
</div>
</div>
<div class="timeframes">
<div class="tf ${periods['5'] === 'up' ? 'active' : ''}">5M</div>
<div class="tf ${periods['15'] === 'up' ? 'active' : ''}">15M</div>
<div class="tf ${periods['30'] === 'up' ? 'active' : ''}">30M</div>
<div class="tf ${periods['60'] === 'up' ? 'active' : ''}">1H</div>
</div>
<div class="card-footer">
<div class="support-resist">
<span>压力 <b class="red">${resistance}</b></span>
<span>支撑 <b class="green">${support}</b></span>
</div>
<a href="#" class="link" onclick="event.stopPropagation(); showDetailView('${item.symbol}'); return false;">详情 →</a>
</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;
// 根据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]) => `
<div class="trend-row">
<span class="trend-period">${periodNames[period]}</span>
<span class="trend-badge ${trend}">
${trend === 'up' ? '上涨' : trend === 'down' ? '下跌' : '震荡'}
</span>
</div>
`).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 = '<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 style="text-align: center; padding: 28px; color: var(--text-tertiary);">
<p>点击"智能分析"按钮获取AI分析结果</p>
</div>
`;
}
} catch (error) {
console.error(`加载合约 ${currentSymbol} AI分析失败:`, error);
content.innerHTML = `
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
<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',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
pollRefreshStatus();
} else {
showToast('error', '刷新失败', data.message || '请稍后重试');
resetRefreshButton(btn);
}
} catch (error) {
showToast('error', '刷新失败', '网络错误,请稍后重试');
resetRefreshButton(btn);
}
}
async function pollRefreshStatus() {
const btn = document.getElementById('refresh-all-btn');
try {
const response = await fetch(`${API_BASE}/refresh-status`);
const data = await response.json();
if (data.success && data.data) {
const status = data.data;
if (!status.running) {
resetRefreshButton(btn);
await loadFuturesList();
if (currentSymbol) {
await loadFuturesDetail(currentSymbol);
await loadKlineData(currentSymbol, currentPeriod);
}
showToast('success', '刷新完成', `已同步 ${status.total} 个品种数据`);
} else {
const progress = status.progress || 0;
const total = status.total || 0;
const percentage = total > 0 ? Math.round((progress / total) * 100) : 0;
btn.innerHTML = `<i class="fas fa-sync-alt fa-spin"></i><span>刷新中 ${progress}/${total} (${percentage}%)</span>`;
showToast('info', '刷新进度', `${status.message || `正在刷新 ${progress}/${total}`}`);
setTimeout(pollRefreshStatus, 1000);
}
}
} catch (error) {
resetRefreshButton(btn);
}
}
function resetRefreshButton(btn) {
btn.disabled = false;
btn.classList.remove('spinning');
btn.innerHTML = '<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 {
// 刷新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 = '<i class="fas fa-spinner fa-spin"></i>';
showToast('info', 'AI分析中', `正在分析 ${symbol}...`);
try {
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`);
const data = await response.json();
if (data.success && data.data) {
const result = data.data.result;
syncAIToSymbolCard(symbol, result);
showToast('success', '分析完成', `${symbol} AI分析已更新`);
} else {
showToast('warning', '分析失败', data.error || 'AI分析失败');
}
} catch (error) {
console.error('AI分析失败:', error);
showToast('error', '分析失败', '网络错误,请稍后重试');
} finally {
btn.disabled = false;
btn.classList.remove('analyzing');
btn.innerHTML = originalContent;
}
}
async function analyzeAllSymbols() {
const allBtn = document.getElementById('ai-analyze-all-btn');
if (!allBtn) return;
const originalContent = allBtn.innerHTML;
allBtn.disabled = true;
allBtn.classList.add('spinning');
showToast('info', '批量分析', '开始对所有合约进行AI分析...');
const symbols = allFuturesData.map(item => item.symbol);
const totalSymbols = symbols.length;
let completedCount = 0;
let successCount = 0;
const batchSize = 3;
for (let i = 0; i < symbols.length; i += batchSize) {
const batch = symbols.slice(i, i + batchSize);
const promises = batch.map(async (symbol) => {
try {
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`);
const data = await response.json();
if (data.success && data.data) {
syncAIToSymbolCard(symbol, data.data.result);
return { symbol, success: true };
} else {
return { symbol, success: false, error: data.error };
}
} catch (error) {
return { symbol, success: false, error: error.message };
}
});
const results = await Promise.all(promises);
const batchSuccessCount = results.filter(r => r.success).length;
completedCount += batch.length;
successCount += batchSuccessCount;
const percentage = Math.round((completedCount / totalSymbols) * 100);
allBtn.innerHTML = `<i class="fas fa-brain fa-spin"></i> 分析中 ${completedCount}/${totalSymbols} (${percentage}%)`;
showToast('info', '分析进度', `已完成 ${completedCount}/${totalSymbols},成功 ${successCount} 个 (${percentage}%)`);
if (i + batchSize < symbols.length) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
allBtn.disabled = false;
allBtn.classList.remove('spinning');
allBtn.innerHTML = originalContent;
showToast('success', '批量分析完成', `共分析 ${totalSymbols} 个合约,成功 ${successCount}`);
}
function syncAIToSymbolCard(symbol, result) {
// 标记该合约已有AI分析数据
const item = allFuturesData.find(d => d.symbol === symbol);
if (item) {
item.hasAIAnalysis = true;
}
// 移除无AI数据的样式
const card = document.querySelector(`.futures-card[onclick="showDetailView('${symbol}')"]`);
if (card) {
card.classList.remove('no-ai-data');
const hint = card.querySelector('.ai-hint');
if (hint) hint.remove();
}
const suggestion = result.trading_suggestion || {};
const fourDim = result.four_dimensional || {};
const pivotPoints = result.pivot_points || {};
// 1. 更新操作建议
const suggestionEl = document.getElementById(`suggestion-${symbol}`);
if (suggestionEl && suggestion.direction) {
suggestionEl.textContent = suggestion.direction;
suggestionEl.className = `suggestion-badge ${suggestion.direction === '做多' ? 'up' : suggestion.direction === '做空' ? 'down' : 'neutral'}`;
}
// 2. 更新压力支撑位
const resistanceEl = document.getElementById(`resistance-${symbol}`);
const supportEl = document.getElementById(`support-${symbol}`);
if (resistanceEl && pivotPoints.r1) resistanceEl.textContent = pivotPoints.r1;
if (supportEl && pivotPoints.s1) supportEl.textContent = pivotPoints.s1;
// 3. 更新多周期趋势
const periodNames = { '60min': '60', '30min': '30', '15min': '15', '5min': '5' };
Object.entries(fourDim).forEach(([period, data]) => {
const periodNum = periodNames[period];
if (!periodNum) return;
const trendEl = document.getElementById(`period-${periodNum}-${symbol}`);
if (trendEl) {
const trend = data.conclusion || data.macd?.trend || 'neutral';
const trendClass = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral';
trendEl.className = `period-tag ${trendClass}`;
}
});
}
// ==================== AI智能分析功能 ====================
let currentAIAnalysis = null;
async function runAIAnalysis(forceRefresh = false) {
if (!currentSymbol) {
showToast('warning', '提示', '请先选择一个品种');
return;
}
const btn = document.getElementById('ai-analyze-btn');
const content = document.getElementById('ai-analysis-content');
btn.disabled = true;
btn.textContent = '分析中...';
content.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 28px; gap: 14px;">
<div style="font-size: 32px; color: var(--color-ai); animation: spin 1.5s linear infinite;">⟳</div>
<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 style="text-align: center; padding: 28px; color: var(--text-tertiary);">
<p>${data.error || 'AI分析失败请稍后重试'}</p>
</div>
`;
showToast('error', '分析失败', data.error || 'AI分析失败');
}
} catch (error) {
console.error('AI分析请求失败:', error);
content.innerHTML = `
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
<p>网络错误,请检查网络连接</p>
</div>
`;
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 = `
<div class="ai-analysis-result">
<div class="ai-summary">${result.summary || '暂无总结'}</div>
<div class="ai-suggestion-row">
<div class="ai-suggestion-direction ${directionClass}">
<span style="font-size:18px;font-weight:700;">${directionIcon}</span>
<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">
<span>🕐</span> 分析时间: ${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;">
查看详情 →
</button>
</div>
</div>
`;
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 `<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');
}