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

2128 lines
86 KiB

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 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>
<div class="code-box">${code}</div>
<div style="font-weight:600;">${item.name}</div>
<div style="font-size:12px; color:var(--text-tertiary);">${item.symbol}</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' });
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 {
// 刷新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.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 分析中...';
showToast('info', '批量分析', '开始对所有合约进行AI分析...');
const symbols = allFuturesData.map(item => item.symbol);
const batchSize = 3; // 每次并发分析3个合约
for (let i = 0; i < symbols.length; i += batchSize) {
const batch = symbols.slice(i, i + batchSize);
const promises = batch.map(async (symbol) => {
try {
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}?force_refresh=false`);
const data = await response.json();
if (data.success && data.data) {
syncAIToSymbolCard(symbol, data.data.result);
return { symbol, success: true };
} else {
return { symbol, success: false, error: data.error };
}
} catch (error) {
console.error(`${symbol} 分析失败:`, error);
return { symbol, success: false, error: error.message };
}
});
const results = await Promise.all(promises);
const successCount = results.filter(r => r.success).length;
showToast('info', '批量分析进度', `已完成 ${Math.min(i + batchSize, symbols.length)}/${symbols.length} 个合约,本批成功 ${successCount}`);
// 每批之间等待2秒避免API限流
if (i + batchSize < symbols.length) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
showToast('success', '批量分析完成', `所有 ${symbols.length} 个合约AI分析已完成`);
allBtn.disabled = false;
allBtn.innerHTML = originalContent;
}
function syncAIToSymbolCard(symbol, result) {
// 标记该合约已有AI分析数据
const item = allFuturesData.find(d => d.symbol === symbol);
if (item) {
item.hasAIAnalysis = true;
}
// 移除无AI数据的样式
const card = document.querySelector(`.futures-card[onclick="showDetailView('${symbol}')"]`);
if (card) {
card.classList.remove('no-ai-data');
const hint = card.querySelector('.ai-hint');
if (hint) hint.remove();
}
const suggestion = result.trading_suggestion || {};
const fourDim = result.four_dimensional || {};
const pivotPoints = result.pivot_points || {};
// 1. 更新操作建议
const suggestionEl = document.getElementById(`suggestion-${symbol}`);
if (suggestionEl && suggestion.direction) {
suggestionEl.textContent = suggestion.direction;
suggestionEl.className = `suggestion-badge ${suggestion.direction === '做多' ? 'up' : suggestion.direction === '做空' ? 'down' : 'neutral'}`;
}
// 2. 更新压力支撑位
const resistanceEl = document.getElementById(`resistance-${symbol}`);
const supportEl = document.getElementById(`support-${symbol}`);
if (resistanceEl && pivotPoints.r1) resistanceEl.textContent = pivotPoints.r1;
if (supportEl && pivotPoints.s1) supportEl.textContent = pivotPoints.s1;
// 3. 更新多周期趋势
const periodNames = { '60min': '60', '30min': '30', '15min': '15', '5min': '5' };
Object.entries(fourDim).forEach(([period, data]) => {
const periodNum = periodNames[period];
if (!periodNum) return;
const trendEl = document.getElementById(`period-${periodNum}-${symbol}`);
if (trendEl) {
const trend = data.conclusion || data.macd?.trend || 'neutral';
const trendClass = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral';
trendEl.className = `period-tag ${trendClass}`;
}
});
}
async function refreshAllSymbols() {
const btn = document.getElementById('refresh-all-btn');
if (!btn) return;
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 刷新中...';
showToast('info', '刷新中', '正在刷新所有合约数据...');
try {
const response = await fetch(`${API_BASE}/refresh-all`);
const data = await response.json();
if (data.success) {
showToast('success', '刷新成功', `已刷新 ${data.count} 个合约`);
loadFuturesData();
} else {
showToast('error', '刷新失败', data.error || '未知错误');
}
} catch (error) {
showToast('error', '刷新失败', '网络错误,请稍后重试');
} finally {
btn.disabled = false;
btn.innerHTML = originalContent;
}
}
// ==================== AI智能分析功能 ====================
let currentAIAnalysis = null;
async function runAIAnalysis(forceRefresh = false) {
if (!currentSymbol) {
showToast('warning', '提示', '请先选择一个品种');
return;
}
const btn = document.getElementById('ai-analyze-btn');
const content = document.getElementById('ai-analysis-content');
btn.disabled = true;
btn.textContent = '分析中...';
content.innerHTML = `
<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');
}