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

3792 lines
154 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');
}
});
});
// 导航项点击事件
document.querySelectorAll('.nav-item[data-page]').forEach(navItem => {
navItem.addEventListener('click', function(e) {
e.preventDefault();
const page = this.dataset.page;
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
this.classList.add('active');
if (page === 'review') {
showReviewView();
} else if (page === 'trade-review') {
showTradeReviewView();
} else if (page === 'watched') {
showWatchedView();
} else {
showListView();
}
});
});
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
function showListView() {
document.getElementById('list-view').classList.add('active');
document.getElementById('detail-view').classList.remove('active');
document.getElementById('review-view').classList.remove('active');
document.getElementById('watched-view').classList.remove('active');
document.getElementById('trade-review-view').classList.remove('active');
if (klineChart) {
klineChart.dispose();
klineChart = null;
}
}
function showWatchedView() {
document.getElementById('list-view').classList.remove('active');
document.getElementById('detail-view').classList.remove('active');
document.getElementById('review-view').classList.remove('active');
document.getElementById('watched-view').classList.add('active');
document.getElementById('trade-review-view').classList.remove('active');
if (klineChart) {
klineChart.dispose();
klineChart = null;
}
renderWatchedList();
}
function showReviewView() {
document.getElementById('list-view').classList.remove('active');
document.getElementById('detail-view').classList.remove('active');
document.getElementById('review-view').classList.add('active');
document.getElementById('watched-view').classList.remove('active');
document.getElementById('trade-review-view').classList.remove('active');
if (klineChart) {
klineChart.dispose();
klineChart = null;
}
// 初始化复盘计划功能
initReviewPlan();
}
function showTradeReviewView() {
document.getElementById('list-view').classList.remove('active');
document.getElementById('detail-view').classList.remove('active');
document.getElementById('review-view').classList.remove('active');
document.getElementById('watched-view').classList.remove('active');
document.getElementById('trade-review-view').classList.add('active');
if (klineChart) {
klineChart.dispose();
klineChart = null;
}
initTradeReview();
}
async function showDetailView(symbol) {
currentSymbol = symbol;
document.getElementById('list-view').classList.remove('active');
document.getElementById('detail-view').classList.add('active');
// 1. 加载行情数据
loadFuturesDetail(symbol);
loadKlineData(symbol, currentPeriod);
// 2. 加载历史记录
await loadHistoryListForAnalysis(symbol);
}
async function loadHistoryListForAnalysis(symbol) {
console.log(`[AI分析] 开始加载 ${symbol} 的历史分析记录...`);
try {
const response = await fetch(`${API_BASE}/ai-analysis/${symbol}/history?limit=20`);
const data = await response.json();
console.log(`[AI分析] API响应:`, data);
if (data.success) {
renderHistoryList(data.data);
// 3. 查找今天的最新分析记录
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
console.log(`[AI分析] 查找日期: ${todayStr}`);
console.log(`[AI分析] 历史记录数量: ${data.data?.length || 0}`);
let todayRecord = null;
if (data.data && data.data.length > 0) {
for (const record of data.data) {
const recordDate = new Date(record.analysis_time);
const recordDateStr = recordDate.toISOString().split('T')[0];
console.log(`[AI分析] 检查记录: ${record.analysis_time} -> ${recordDateStr}`);
if (recordDateStr === todayStr) {
todayRecord = record;
console.log(`[AI分析] ✓ 找到今天的记录!`);
break;
}
}
}
// 4. 根据是否有今天的记录进行不同处理
if (todayRecord) {
console.log(`[AI分析] 显示AI分析结果...`);
currentAIAnalysis = {
id: todayRecord.id,
symbol: todayRecord.symbol,
analysis_time: todayRecord.analysis_time,
result: todayRecord.analysis_data
};
displayAIAnalysisResult(currentAIAnalysis);
syncAIToPanels(todayRecord.analysis_data);
} else {
console.log(`[AI分析] 没有找到今天的分析记录,显示占位符`);
showAIAnalysisPlaceholder();
}
} else {
console.log(`[AI分析] API返回失败显示占位符`);
showAIAnalysisPlaceholder();
}
} catch (error) {
console.error('[AI分析] 加载历史记录失败:', error);
showAIAnalysisPlaceholder();
}
}
function showAIAnalysisPlaceholder() {
console.log('[AI分析] 显示占位符...');
const content = document.getElementById('ai-analysis-content');
console.log('[AI分析] ai-analysis-content元素:', content);
if (!content) {
console.error('[AI分析] 错误: 找不到ai-analysis-content元素!');
return;
}
content.innerHTML = `
<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);
}
} catch (error) {
console.error('加载自选列表失败:', error);
watchedSymbols = [];
}
}
async function toggleWatch(symbol, name, event) {
event.stopPropagation();
const isWatched = watchedSymbols.includes(symbol);
try {
if (isWatched) {
const response = await fetch(`${API_BASE}/watched/${symbol}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
watchedSymbols = watchedSymbols.filter(s => s !== symbol);
showToast('success', '已取消自选', `${symbol} 已从自选列表移除`);
}
} else {
const response = await fetch(`${API_BASE}/watched`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ symbol, name })
});
const data = await response.json();
if (data.success) {
watchedSymbols.push(symbol);
showToast('success', '已添加自选', `${name}(${symbol}) 已添加到自选列表`);
}
}
// 重新渲染当前视图
const activePill = document.querySelector('.pill.active');
const category = activePill ? activePill.dataset.category : 'all';
// 如果当前视图不是自选页面,重新渲染主网格
if (category !== 'watched') {
renderFuturesGrid(getCurrentFilteredData());
}
} catch (error) {
console.error('切换自选失败:', error);
showToast('error', '操作失败', '网络错误,请稍后重试');
}
}
function toggleFav(symbol, name, event) {
event.stopPropagation();
// 调用原有的toggleWatch函数处理API请求和数据更新
toggleWatch(symbol, name, event);
// 如果当前在自选页面,刷新自选列表
const watchedView = document.getElementById('watched-view');
if (watchedView && watchedView.classList.contains('active')) {
setTimeout(() => renderWatchedList(), 200);
}
}
function getCurrentFilteredData() {
const activePill = document.querySelector('.pill.active');
const category = activePill ? activePill.dataset.category : 'all';
return filterDataByCategory(allFuturesData, category);
}
function filterDataByCategory(data, category) {
if (category === 'all') return data;
const categoryMap = {
'energy': ['SC', 'FU', 'LU', 'BU', 'RU', 'NR'],
'metal': ['AU', 'AG', 'CU', 'AL', 'ZN', 'NI', 'SN', 'PB', 'SS', 'RB', 'HC', 'I', 'J', 'JM', 'AO', 'SI', 'LC', 'PS'],
'agriculture': ['M', 'RM', 'C', 'CS', 'A', 'B', 'Y', 'P', 'OI', 'CF', 'SR', 'AP', 'LH'],
'finance': ['IF', 'IC', 'IH', 'IM', 'T', 'TF', 'TS', 'TL']
};
const symbols = categoryMap[category] || [];
return data.filter(item => {
const symbolBase = item.symbol.replace(/[0-9]/g, '').toUpperCase();
return symbols.includes(symbolBase);
});
}
async function loadFuturesList() {
try {
console.log('正在加载品种列表...');
const response = await fetch(`${API_BASE}/list`);
const data = await response.json();
console.log('品种列表响应:', data);
if (data.success) {
allFuturesData = data.data.map(item => ({
...item,
hasAIAnalysis: false // 默认没有AI分析数据
}));
console.log('加载的品种数据:', allFuturesData.length, '个');
renderFuturesGrid(allFuturesData);
updateStats(allFuturesData);
// 异步加载每个合约的最新AI分析结果
loadAllAIAnalysis();
} else {
console.error('加载品种列表失败:', data);
}
} catch (error) {
console.error('加载品种列表失败:', error);
loadFuturesFromConfig();
}
}
async function loadAllAIAnalysis() {
console.log('开始加载所有合约的AI分析结果...');
// 获取今天的日期字符串用于比较
const today = new Date();
const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
// 分批加载,避免并发请求过多
const batchSize = 5;
for (let i = 0; i < allFuturesData.length; i += batchSize) {
const batch = allFuturesData.slice(i, i + batchSize);
const promises = batch.map(async (item) => {
try {
// 获取历史记录
const response = await fetch(`${API_BASE}/ai-analysis/${item.symbol}/history?limit=1`);
const data = await response.json();
if (data.success && data.data && data.data.length > 0) {
const latestRecord = data.data[0]; // 最新的一条记录
const analysisTime = latestRecord.analysis_time;
// 判断是否是今天的记录
const recordDate = new Date(analysisTime);
const recordDateStr = recordDate.toISOString().split('T')[0];
if (recordDateStr === todayStr) {
// 是今天的记录,加载数据
const result = latestRecord.analysis_data;
const analysisItem = allFuturesData.find(d => d.symbol === item.symbol);
if (analysisItem) {
analysisItem.hasAIAnalysis = true;
analysisItem.aiResult = result;
analysisItem.analysisTime = analysisTime;
// 更新操作建议
if (result.trading_suggestion?.direction) {
analysisItem.suggestion = result.trading_suggestion.direction;
analysisItem.suggestionType = result.trading_suggestion.direction === '做多' ? 'up' : result.trading_suggestion.direction === '做空' ? 'down' : 'neutral';
}
// 更新压力支撑位
if (result.pivot_points) {
if (result.pivot_points.r1) analysisItem.resistance = result.pivot_points.r1;
if (result.pivot_points.s1) analysisItem.support = result.pivot_points.s1;
}
// 更新多周期趋势
if (result.four_dimensional) {
const periodMap = { '60min': '60', '30min': '30', '15min': '15', '5min': '5' };
analysisItem.periods = {};
Object.entries(result.four_dimensional).forEach(([period, pdata]) => {
const periodNum = periodMap[period];
if (periodNum) {
const trend = pdata.conclusion || pdata.macd?.trend || 'neutral';
analysisItem.periods[periodNum] = trend.includes('多') || trend === 'up' ? 'up' : trend.includes('空') || trend === 'down' ? 'down' : 'neutral';
}
});
}
// 更新趋势评分使用AI置信度
if (result.trading_suggestion?.confidence) {
analysisItem.trendScore = result.trading_suggestion.confidence;
}
// 更新成功率(根据判断方向设置一致概率)
const direction = analysisItem.suggestionType;
if (direction === 'up') {
analysisItem.successRate = 85; // 做多成功率
} else if (direction === 'down') {
analysisItem.successRate = 82; // 做空成功率
} else {
analysisItem.successRate = 60; // 观望成功率
}
}
} else {
console.log(`${item.symbol} 的分析记录不是今天的 (${recordDateStr}),不加载`);
}
} else {
console.log(`${item.symbol} 没有AI分析记录`);
}
} catch (error) {
console.error(`加载 ${item.symbol} AI分析失败:`, error);
}
});
await Promise.all(promises);
}
// 所有批次加载完成后只渲染一次
renderFuturesGrid(allFuturesData);
console.log('所有合约AI分析结果加载完成');
}
async function loadFuturesFromConfig() {
try {
const response = await fetch('/api/v1/config');
const config = await response.json();
const futuresConfig = config.futures || {};
allFuturesData = Object.entries(futuresConfig).map(([name, symbol]) => ({
symbol: symbol,
name: name,
price: 0,
change: 0,
changePct: 0,
suggestion: '等待数据',
suggestionType: 'neutral',
periods: { '5': 'neutral', '15': 'neutral', '30': 'neutral', '60': 'neutral' },
successRate: 0,
trendScore: 0,
resistance: 0,
support: 0,
open: 0,
high: 0,
low: 0,
volume: 0
}));
renderFuturesGrid(allFuturesData);
updateStats(allFuturesData);
} catch (error) {
console.error('加载配置失败:', error);
const mockData = generateMockFuturesData();
allFuturesData = mockData;
renderFuturesGrid(mockData);
updateStats(mockData);
}
}
function generateMockFuturesData() {
return [
{ symbol: 'SC2606', name: '原油', price: 528.6, change: 12.1, changePct: 2.35, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'neutral' }, successRate: 72, trendScore: 85, resistance: 535, support: 518, open: 516.5, high: 530, low: 515, volume: 78000 },
{ symbol: 'AU2606', name: '黄金', price: 685.2, change: 12.45, changePct: 1.85, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, successRate: 78, trendScore: 92, resistance: 692, support: 678, open: 672.75, high: 688, low: 670, volume: 128000 },
{ symbol: 'AG2606', name: '白银', price: 8250, change: 165, changePct: 2.04, suggestion: '逢低做多', suggestionType: 'up', periods: { '5': 'up', '15': 'up', '30': 'up', '60': 'up' }, successRate: 75, trendScore: 88, resistance: 8350, support: 8100, open: 8085, high: 8280, low: 8050, volume: 95000 },
{ symbol: 'CU2606', name: '沪铜', price: 80610, change: 112, changePct: 0.14, suggestion: '观望等待', suggestionType: 'neutral', periods: { '5': 'neutral', '15': 'up', '30': 'neutral', '60': 'up' }, successRate: 58, trendScore: 65, resistance: 81200, support: 79800, open: 80498, high: 80850, low: 80200, volume: 42000 },
{ symbol: 'M2609', name: '豆粕', price: 2985, change: -51, changePct: -1.68, suggestion: '逢高做空', suggestionType: 'down', periods: { '5': 'down', '15': 'down', '30': 'down', '60': 'neutral' }, successRate: 65, trendScore: 35, resistance: 3050, support: 2920, open: 3036, high: 3040, low: 2980, volume: 185000 }
];
}
function renderFuturesGrid(data) {
const grid = document.getElementById('futures-grid');
console.log('渲染品种网格,数据量:', data.length);
if (data.length === 0) {
grid.innerHTML = '<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('');
}
// ==================== 交易复盘功能 ====================
const TR_API_BASE = '/api/v1/trade-review';
let trCurrentPage = 1;
let trInitialized = false;
function initTradeReview() {
if (trInitialized) return;
trInitialized = true;
// 导入按钮
document.getElementById('tr-btn-import').addEventListener('click', () => {
document.getElementById('tr-file-input').click();
});
document.getElementById('tr-file-input').addEventListener('change', trHandleFileImport);
// 查询按钮
document.getElementById('tr-btn-query').addEventListener('click', () => {
trCurrentPage = 1;
trLoadAllData();
});
// 删除当日按钮
document.getElementById('tr-btn-delete-date').addEventListener('click', trDeleteByDate);
// 批次管理
document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal);
// 子导航切换
document.querySelectorAll('.tr-sub-nav-item').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.tr-sub-nav-item').forEach(i => i.classList.remove('active'));
this.classList.add('active');
const sub = this.dataset.sub;
document.querySelectorAll('.tr-sub-page').forEach(p => p.classList.remove('active'));
document.getElementById('tr-sub-' + sub).classList.add('active');
});
});
// 初始加载:先获取最后交易日,设为默认日期,再加载数据
trInitDefaultDate();
}
async function trInitDefaultDate() {
try {
const res = await fetch(`${TR_API_BASE}/latest-trade-date`);
const json = await res.json();
if (json.success && json.data.trade_date) {
const dateStr = json.data.trade_date;
document.getElementById('tr-start-date').value = dateStr;
document.getElementById('tr-end-date').value = dateStr;
}
} catch (e) {
console.error('获取最后交易日失败', e);
}
trLoadAllData();
}
function trGetDateRange() {
const start = document.getElementById('tr-start-date').value || null;
const end = document.getElementById('tr-end-date').value || null;
return { start, end };
}
function trBuildParams(extra = {}) {
const { start, end } = trGetDateRange();
const params = new URLSearchParams();
if (start) params.set('start_date', start);
if (end) params.set('end_date', end);
for (const [k, v] of Object.entries(extra)) {
if (v) params.set(k, v);
}
return params.toString();
}
async function trLoadAllData() {
trLoadStatistics();
trLoadRecords();
}
async function trLoadStatistics() {
try {
const res = await fetch(`${TR_API_BASE}/statistics?${trBuildParams()}`);
const json = await res.json();
if (json.success) trRenderStatistics(json.data);
} catch (e) {
console.error('加载统计失败', e);
}
}
function trRenderStatistics(stats) {
const grid = document.getElementById('tr-stats-grid');
const pnlClass = stats.total_pnl >= 0 ? 'profit' : 'loss';
grid.innerHTML = `
<div class="tr-stat-card">
<div class="tr-stat-label">总盈亏</div>
<div class="tr-stat-value ${pnlClass}">${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl.toFixed(2)}</div>
<div class="tr-stat-sub">手续费: ${stats.total_commission.toFixed(2)}</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">胜率</div>
<div class="tr-stat-value">${stats.win_rate}%</div>
<div class="tr-stat-sub">盈亏比: ${stats.profit_loss_ratio}</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">交易统计</div>
<div class="tr-stat-value">${stats.total_trades}</div>
<div class="tr-stat-sub">${stats.trading_days} 个交易日</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">日均盈亏</div>
<div class="tr-stat-value ${stats.avg_daily_pnl >= 0 ? 'profit' : 'loss'}">${stats.avg_daily_pnl >= 0 ? '+' : ''}${stats.avg_daily_pnl}</div>
<div class="tr-stat-sub">最大连盈 ${stats.max_consecutive_wins} / 连亏 ${stats.max_consecutive_losses}</div>
</div>
`;
}
async function trLoadRecords() {
try {
const params = trBuildParams({ page: trCurrentPage, page_size: 50 });
const res = await fetch(`${TR_API_BASE}/records?${params}`);
const json = await res.json();
if (json.success) trRenderRecords(json.data);
} catch (e) {
console.error('加载记录失败', e);
}
}
function trRenderRecords(data) {
const tbody = document.getElementById('tr-records-body');
const emptyEl = document.getElementById('tr-records-empty');
const tableWrap = document.getElementById('tr-records-table-wrap');
const pagination = document.getElementById('tr-records-pagination');
if (!data.records || data.records.length === 0) {
tableWrap.style.display = 'none';
pagination.style.display = 'none';
emptyEl.style.display = 'flex';
return;
}
tableWrap.style.display = '';
emptyEl.style.display = 'none';
tbody.innerHTML = data.records.map(r => {
const pnlClass = r.close_pnl > 0 ? 'tr-pnl-positive' : (r.close_pnl < 0 ? 'tr-pnl-negative' : '');
const dirClass = r.direction === '买' ? 'tr-dir-buy' : 'tr-dir-sell';
return `<tr>
<td>${r.trade_date || '-'}</td>
<td>${r.trade_time || '-'}</td>
<td>${r.trade_type}</td>
<td>${r.symbol}</td>
<td>${r.symbol_name || r.variety}</td>
<td class="${dirClass}">${r.direction}</td>
<td>${r.offset || '-'}</td>
<td>${r.price != null ? r.price.toFixed(2) : '-'}</td>
<td>${r.volume || '-'}</td>
<td>${r.amount != null ? r.amount.toFixed(2) : '-'}</td>
<td class="${pnlClass}">${r.close_pnl != null ? r.close_pnl.toFixed(2) : '-'}</td>
<td>${r.commission != null ? r.commission.toFixed(2) : '-'}</td>
<td title="${r.source_file || ''}">${(r.source_file || '').substring(0, 15)}</td>
</tr>`;
}).join('');
// 分页
const totalPages = Math.ceil(data.total / data.page_size);
pagination.style.display = totalPages > 1 ? 'flex' : 'none';
pagination.innerHTML = `
<button ${trCurrentPage <= 1 ? 'disabled' : ''} onclick="trCurrentPage--;trLoadRecords()">上一页</button>
<span>${trCurrentPage} / ${totalPages} (共 ${data.total} 条)</span>
<button ${trCurrentPage >= totalPages ? 'disabled' : ''} onclick="trCurrentPage++;trLoadRecords()">下一页</button>
`;
}
async function trHandleFileImport(e) {
const file = e.target.files[0];
if (!file) return;
e.target.value = '';
const btn = document.getElementById('tr-btn-import');
btn.disabled = true;
btn.querySelector('span:last-child').textContent = '导入中...';
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`${TR_API_BASE}/import`, { method: 'POST', body: formData });
const json = await res.json();
if (json.success) {
alert(json.message);
trCurrentPage = 1;
trLoadAllData();
} else {
alert(json.message || '导入失败');
}
} catch (err) {
alert('导入失败: ' + err.message);
} finally {
btn.disabled = false;
btn.querySelector('span:last-child').textContent = '导入结算单';
}
}
// ==================== 批次管理 ====================
async function trShowBatchModal() {
const modal = document.getElementById('tr-batch-modal');
modal.classList.add('show');
try {
const res = await fetch(`${TR_API_BASE}/batches`);
const json = await res.json();
if (json.success) trRenderBatchList(json.data);
} catch (e) {
console.error('加载批次失败', e);
}
}
function trRenderBatchList(batches) {
const list = document.getElementById('tr-batch-list');
if (!batches || batches.length === 0) {
list.innerHTML = '<div class="tr-empty" style="min-height:100px;"><div class="tr-empty-text">暂无导入记录</div></div>';
return;
}
list.innerHTML = batches.map(b => `
<div class="tr-batch-item">
<div class="tr-batch-info">
<div class="tr-batch-file">${b.source_file}</div>
<div class="tr-batch-meta">期货 ${b.futures_count} 条 | 期权 ${b.options_count} 条 | ${b.trade_dates || '未知日期'} | ${b.created_at}</div>
</div>
<button class="tr-batch-delete" onclick="trDeleteBatch('${b.batch_id}')">删除</button>
</div>
`).join('');
}
async function trDeleteBatch(batchId) {
if (!confirm('确认删除该批次的所有交易记录?')) return;
try {
const res = await fetch(`${TR_API_BASE}/records/${batchId}`, { method: 'DELETE' });
const json = await res.json();
if (json.success) {
alert(json.message);
trShowBatchModal();
trCurrentPage = 1;
trLoadAllData();
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
function trCloseBatchModal() {
document.getElementById('tr-batch-modal').classList.remove('show');
}
async function trDeleteByDate() {
const startDate = document.getElementById('tr-start-date').value;
const endDate = document.getElementById('tr-end-date').value;
if (!startDate || !endDate) {
alert('请先选择要删除的日期范围');
return;
}
if (startDate !== endDate) {
alert('删除功能仅支持单个日期,请确保开始日期和结束日期相同');
return;
}
const tradeDate = startDate;
// 第一次确认:查询该日期的记录数
try {
const queryRes = await fetch(`${TR_API_BASE}/records?start_date=${tradeDate}&end_date=${tradeDate}&page=1&page_size=1`);
const queryJson = await queryRes.json();
const recordCount = queryJson.success ? queryJson.data.total : 0;
if (recordCount === 0) {
alert(`${tradeDate} 没有交易记录,无需删除`);
return;
}
// 第一次确认弹窗
const firstConfirm = confirm(
`⚠️ 警告:即将删除交易数据\n\n` +
`交易日:${tradeDate}\n` +
`记录数量:${recordCount}\n\n` +
`此操作将永久删除该日期的所有交易记录,不可恢复!\n\n` +
`点击"确定"继续,点击"取消"返回`
);
if (!firstConfirm) return;
// 第二次确认弹窗(更强烈的警告)
const secondConfirm = confirm(
`🔴 最终确认\n\n` +
`您确定要删除 ${tradeDate}${recordCount} 条交易记录吗?\n\n` +
`删除后无法恢复,请确认!`
);
if (!secondConfirm) return;
// 执行删除
const res = await fetch(`${TR_API_BASE}/records-by-date/${tradeDate}`, { method: 'DELETE' });
const json = await res.json();
if (json.success) {
alert(`${json.message}`);
trCurrentPage = 1;
trLoadAllData();
} else {
alert(json.message || '删除失败');
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
// ==================== 每日分析 ====================
async function trLoadDailySummary() {
try {
const res = await fetch(`${TR_API_BASE}/daily-summary?${trBuildParams()}`);
const json = await res.json();
if (json.success) trRenderDailySummary(json.data);
} catch (e) {
console.error('加载每日汇总失败', e);
}
}
function trRenderDailySummary(data) {
const tbody = document.getElementById('tr-daily-body');
const emptyEl = document.getElementById('tr-daily-empty');
const chartDom = document.getElementById('tr-daily-chart');
if (!data || data.length === 0) {
tbody.innerHTML = '';
emptyEl.style.display = 'flex';
chartDom.style.display = 'none';
return;
}
emptyEl.style.display = 'none';
chartDom.style.display = '';
tbody.innerHTML = data.map(d => {
const pnlClass = d.total_pnl > 0 ? 'tr-pnl-positive' : (d.total_pnl < 0 ? 'tr-pnl-negative' : '');
return `<tr>
<td>${d.trade_date}</td>
<td>${d.total_trades}</td>
<td class="${pnlClass}">${d.total_pnl >= 0 ? '+' : ''}${d.total_pnl}</td>
<td>${d.total_commission}</td>
<td>${d.win_count}</td>
<td>${d.loss_count}</td>
<td>${d.win_rate}%</td>
<td class="tr-pnl-positive">${d.max_win > 0 ? '+' + d.max_win : '-'}</td>
<td class="tr-pnl-negative">${d.max_loss < 0 ? d.max_loss : '-'}</td>
<td>${d.variety_count}</td>
</tr>`;
}).join('');
// 渲染柱状图
trRenderDailyChart(data);
}
function trRenderDailyChart(data) {
const chartDom = document.getElementById('tr-daily-chart');
const chart = echarts.init(chartDom);
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 20, top: 20, bottom: 40 },
xAxis: { type: 'category', data: data.map(d => d.trade_date), axisLabel: { fontSize: 10, rotate: 30 } },
yAxis: { type: 'value', axisLabel: { fontSize: 11 } },
series: [{
type: 'bar',
data: data.map(d => ({
value: d.total_pnl,
itemStyle: { color: d.total_pnl >= 0 ? '#34C759' : '#FF3B30' },
})),
}],
});
}
// ==================== 品种汇总 ====================
async function trLoadVarietySummary() {
try {
const res = await fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`);
const json = await res.json();
if (json.success) trRenderVarietySummary(json.data);
} catch (e) {
console.error('加载品种汇总失败', e);
}
}
function trRenderVarietySummary(data) {
const tbody = document.getElementById('tr-variety-body');
const emptyEl = document.getElementById('tr-variety-empty');
if (!data || data.length === 0) {
tbody.innerHTML = '';
emptyEl.style.display = 'flex';
return;
}
emptyEl.style.display = 'none';
tbody.innerHTML = data.map(v => {
const pnlClass = v.total_pnl > 0 ? 'tr-pnl-positive' : (v.total_pnl < 0 ? 'tr-pnl-negative' : '');
return `<tr>
<td>${v.variety}</td>
<td>${v.symbol_name || '-'}</td>
<td>${v.total_trades}</td>
<td class="${pnlClass}">${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl}</td>
<td>${v.total_close_pnl}</td>
<td>${v.total_commission}</td>
<td>${v.total_volume}</td>
<td>${v.buy_count}</td>
<td>${v.sell_count}</td>
<td>${v.win_rate}%</td>
</tr>`;
}).join('');
}
// ==================== 逐笔分析 ====================
async function trLoadTradePairs() {
try {
const res = await fetch(`${TR_API_BASE}/trade-pairs?${trBuildParams()}`);
const json = await res.json();
if (json.success) trRenderTradePairs(json.data);
} catch (e) {
console.error('加载逐笔数据失败', e);
}
}
function trRenderTradePairs(pairs) {
const grid = document.getElementById('tr-pairs-grid');
const emptyEl = document.getElementById('tr-pairs-empty');
if (!pairs || pairs.length === 0) {
grid.innerHTML = '';
emptyEl.style.display = 'flex';
return;
}
emptyEl.style.display = 'none';
grid.innerHTML = pairs.map(p => {
const pnlClass = p.net_pnl >= 0 ? 'profit' : 'loss';
const dirText = p.direction === '多' ? '做多' : '做空';
const dirClass = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell';
const pairData = JSON.stringify(p).replace(/"/g, '&quot;');
return `
<div class="tr-pair-card ${pnlClass}">
<div class="tr-pair-header">
<span class="tr-pair-symbol-link" onclick='trShowTradeDetail(${pairData})'>${p.symbol_name || p.symbol} <span class="${dirClass}" style="font-size:12px;">${dirText}</span></span>
<span class="tr-pair-pnl ${pnlClass}">${p.net_pnl >= 0 ? '+' : ''}${p.net_pnl}</span>
</div>
<div class="tr-pair-info">
<div class="tr-pair-info-item"><span class="tr-pair-info-label">开仓</span><span class="tr-pair-info-value">${p.open_date} ${p.open_time || ''}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">平仓</span><span class="tr-pair-info-value">${p.close_date || '-'} ${p.close_time || ''}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">开仓价</span><span class="tr-pair-info-value">${p.open_price != null ? p.open_price.toFixed(2) : '-'}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">平仓价</span><span class="tr-pair-info-value">${p.close_price != null ? p.close_price.toFixed(2) : '-'}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">手数</span><span class="tr-pair-info-value">${p.volume || '-'}</span></div>
<div class="tr-pair-info-item"><span class="tr-pair-info-label">手续费</span><span class="tr-pair-info-value">${p.commission}</span></div>
</div>
<div class="tr-pair-actions">
<button class="tr-pair-btn-analyze" onclick='trShowTradeDetail(${pairData})'>查看详情</button>
</div>
</div>`;
}).join('');
}
async function trAnalyzeTrade(pair) {
const modal = document.getElementById('tr-analysis-modal');
const title = document.getElementById('tr-analysis-title');
const content = document.getElementById('tr-analysis-content');
title.textContent = `AI 分析 - ${pair.symbol_name || pair.symbol} ${pair.direction}`;
content.textContent = '正在分析中,请稍候...';
modal.classList.add('show');
try {
const res = await fetch(`${TR_API_BASE}/analyze-trade`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol: pair.symbol,
open_date: pair.open_date,
open_time: pair.open_time,
close_date: pair.close_date,
close_time: pair.close_time,
direction: pair.direction,
open_price: pair.open_price,
close_price: pair.close_price,
}),
});
const json = await res.json();
if (json.success) {
content.textContent = json.data.analysis;
} else {
content.textContent = json.message || '分析失败';
}
} catch (e) {
content.textContent = '分析请求失败: ' + e.message;
}
}
function trCloseAnalysisModal() {
document.getElementById('tr-analysis-modal').classList.remove('show');
}
// ==================== 交易详情弹窗 ====================
let trDetailChart = null;
let trCurrentPair = null;
async function trShowTradeDetail(pair) {
trCurrentPair = pair;
const modal = document.getElementById('tr-detail-modal');
const title = document.getElementById('tr-detail-title');
const info = document.getElementById('tr-detail-info');
const aiContent = document.getElementById('tr-detail-ai-content');
title.textContent = `${pair.symbol_name || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 交易详情`;
// 渲染交易信息
const pnlClass = pair.net_pnl >= 0 ? 'profit' : 'loss';
info.innerHTML = `
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">品种</span>
<span class="tr-detail-info-value">${pair.symbol_name || pair.symbol}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">方向</span>
<span class="tr-detail-info-value">${pair.direction === '多' ? '做多' : '做空'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">开仓日期</span>
<span class="tr-detail-info-value">${pair.open_date || '-'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">开仓时间</span>
<span class="tr-detail-info-value">${pair.open_time || '-'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">开仓价</span>
<span class="tr-detail-info-value">${pair.open_price != null ? pair.open_price.toFixed(2) : '-'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">平仓日期</span>
<span class="tr-detail-info-value">${pair.close_date || '-'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">平仓时间</span>
<span class="tr-detail-info-value">${pair.close_time || '-'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">平仓价</span>
<span class="tr-detail-info-value">${pair.close_price != null ? pair.close_price.toFixed(2) : '-'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">手数</span>
<span class="tr-detail-info-value">${pair.volume || '-'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">手续费</span>
<span class="tr-detail-info-value">${pair.commission || '-'}</span>
</div>
<div class="tr-detail-info-item">
<span class="tr-detail-info-label">净盈亏</span>
<span class="tr-detail-info-value ${pnlClass}">${pair.net_pnl >= 0 ? '+' : ''}${pair.net_pnl || 0}</span>
</div>
`;
// 重置AI分析区域
aiContent.innerHTML = `<button id="tr-detail-ai-btn" class="tr-btn tr-btn-primary">开始AI分析</button>`;
document.getElementById('tr-detail-ai-btn').addEventListener('click', () => trAnalyzeInDetail(pair));
modal.classList.add('show');
// 绑定刷新K线按钮事件
document.getElementById('tr-detail-reload-kline').onclick = () => trLoadDetailKline(pair);
// 加载K线
await trLoadDetailKline(pair);
}
async function trLoadDetailKline(pair) {
const period = document.getElementById('tr-detail-period').value;
const chartDom = document.getElementById('tr-detail-kline');
console.log('[K线加载] 开始加载K线数据', {
symbol: pair.symbol,
variety: pair.variety,
period: period,
open_date: pair.open_date,
close_date: pair.close_date,
});
try {
// 构建日期范围开仓日前后各7天
const openDate = pair.open_date ? new Date(pair.open_date) : new Date();
const startDate = new Date(openDate);
startDate.setDate(startDate.getDate() - 7);
const endDate = new Date(openDate);
endDate.setDate(endDate.getDate() + 7);
const params = new URLSearchParams({
start_date: startDate.toISOString().slice(0, 10),
end_date: endDate.toISOString().slice(0, 10),
period: period,
});
const url = `${TR_API_BASE}/kline-with-trades/${pair.symbol}?${params}`;
console.log('[K线加载] 请求URL:', url);
const res = await fetch(url);
const json = await res.json();
console.log('[K线加载] API响应:', {
success: json.success,
message: json.message,
hasData: !!json.data,
symbol: json.data?.symbol,
candlesCount: json.data?.candles?.length || 0,
markersCount: json.data?.trade_markers?.length || 0,
});
if (!json.success) {
console.warn('[K线加载] API返回失败:', json.message);
chartDom.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-tertiary);">暂无K线数据: ${json.message || '未知错误'}</div>`;
if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; }
return;
}
if (!json.data.candles || json.data.candles.length === 0) {
console.warn('[K线加载] K线数据为空');
chartDom.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-tertiary);">暂无K线数据</div>';
if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; }
return;
}
console.log('[K线加载] 开始渲染K线图');
trRenderDetailKline(json.data, pair);
console.log('[K线加载] K线图渲染完成');
} catch (e) {
console.error('[K线加载] 加载K线失败:', e);
chartDom.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-tertiary);">加载K线失败: ${e.message}</div>`;
}
}
function trRenderDetailKline(data, pair) {
if (trDetailChart) trDetailChart.dispose();
const chartDom = document.getElementById('tr-detail-kline');
trDetailChart = echarts.init(chartDom, 'dark');
const candles = data.candles;
const dates = candles.map(c => c[0]);
const values = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]);
const volumes = candles.map(c => [parseInt(c[5]), parseFloat(c[2]) >= parseFloat(c[1]) ? 1 : -1]);
// 构建买卖标记
const buyMarkers = [];
const sellMarkers = [];
const markers = data.trade_markers || [];
markers.forEach(m => {
const dateIdx = dates.indexOf(m.date);
if (dateIdx === -1) return;
const candle = candles[dateIdx];
const price = parseFloat(m.price);
const low = parseFloat(candle[3]);
const high = parseFloat(candle[4]);
const markerPrice = Math.max(low, Math.min(high, price));
if (m.direction === '买') {
buyMarkers.push({
name: `${m.offset || ''}`,
coord: [dateIdx, markerPrice],
value: `${m.offset || ''} ${price}`,
itemStyle: { color: '#34C759' },
});
} else {
sellMarkers.push({
name: `${m.offset || ''}`,
coord: [dateIdx, markerPrice],
value: `${m.offset || ''} ${price}`,
itemStyle: { color: '#FF3B30' },
});
}
});
const option = {
backgroundColor: 'transparent',
animation: false,
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(10, 15, 25, 0.95)',
borderColor: 'rgba(255,255,255,0.1)',
textStyle: { color: '#E5E5E7', fontSize: 12 },
},
grid: [
{ left: '8%', right: '3%', top: '5%', height: '55%' },
{ left: '8%', right: '3%', top: '68%', height: '22%' },
],
xAxis: [
{ type: 'category', data: dates, gridIndex: 0, axisLine: { lineStyle: { color: '#333' } }, axisLabel: { show: false } },
{ type: 'category', data: dates, gridIndex: 1, axisLine: { lineStyle: { color: '#333' } }, axisLabel: { color: '#999', fontSize: 10 } },
],
yAxis: [
{ scale: true, gridIndex: 0, splitLine: { lineStyle: { color: '#1a1a1a' } }, axisLabel: { color: '#999' } },
{ scale: true, gridIndex: 1, splitLine: { show: false }, axisLabel: { show: false } },
],
series: [
{
name: 'K线',
type: 'candlestick',
data: values,
xAxisIndex: 0, yAxisIndex: 0,
itemStyle: {
color: '#FF3B30', color0: '#34C759',
borderColor: '#FF3B30', borderColor0: '#34C759',
},
markPoint: {
symbol: 'triangle',
symbolSize: 12,
data: buyMarkers,
label: { show: true, formatter: '{b}', fontSize: 10, color: '#34C759' },
},
},
{
name: 'K线',
type: 'candlestick',
data: values,
xAxisIndex: 0, yAxisIndex: 0,
itemStyle: {
color: '#FF3B30', color0: '#34C759',
borderColor: '#FF3B30', borderColor0: '#34C759',
},
markPoint: {
symbol: 'triangle',
symbolSize: 12,
symbolRotate: 180,
data: sellMarkers,
label: { show: true, formatter: '{b}', fontSize: 10, color: '#FF3B30' },
},
},
{
name: '成交量',
type: 'bar',
xAxisIndex: 1, yAxisIndex: 1,
data: volumes.map(v => ({
value: v[0],
itemStyle: { color: v[1] > 0 ? 'rgba(255,59,48,0.5)' : 'rgba(52,199,89,0.5)' }
})),
},
],
dataZoom: [
{ type: 'inside', xAxisIndex: [0, 1], start: 0, end: 100 },
{ type: 'slider', xAxisIndex: [0, 1], bottom: '2%', height: 16, borderColor: '#333', fillerColor: 'rgba(212,167,71,0.15)' },
],
};
trDetailChart.setOption(option);
}
function trCloseDetailModal() {
document.getElementById('tr-detail-modal').classList.remove('show');
if (trDetailChart) {
trDetailChart.dispose();
trDetailChart = null;
}
trCurrentPair = null;
}
async function trAnalyzeInDetail(pair) {
const aiContent = document.getElementById('tr-detail-ai-content');
aiContent.innerHTML = '<div style="color:var(--text-tertiary);">正在分析中,请稍候...</div>';
try {
const res = await fetch(`${TR_API_BASE}/analyze-trade`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol: pair.symbol,
open_date: pair.open_date,
open_time: pair.open_time,
close_date: pair.close_date,
close_time: pair.close_time,
direction: pair.direction,
open_price: pair.open_price,
close_price: pair.close_price,
}),
});
const json = await res.json();
if (json.success) {
aiContent.innerHTML = `<div style="white-space:pre-wrap;">${json.data.analysis}</div>`;
} else {
aiContent.innerHTML = `<div style="color:var(--color-up);">${json.message || '分析失败'}</div>`;
}
} catch (e) {
aiContent.innerHTML = `<div style="color:var(--color-up);">分析请求失败: ${e.message}</div>`;
}
}
// 子页面切换时懒加载
document.addEventListener('click', function(e) {
if (!e.target.classList.contains('tr-sub-nav-item')) return;
const sub = e.target.dataset.sub;
setTimeout(() => {
if (sub === 'daily') trLoadDailySummary();
else if (sub === 'variety') trLoadVarietySummary();
else if (sub === 'pairs') trLoadTradePairs();
}, 50);
});
function formatNumber(num) {
if (num === 0 || num === undefined || num === null) return '--';
return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}
function calcPriceChangePercent(current, target) {
if (!current || !target || current === 0) return '--';
const pct = ((target - current) / current * 100).toFixed(2);
return (pct >= 0 ? '+' : '') + pct + '%';
}
function updateStats(data) {
const total = data.length;
// 根据AI分析结果统计趋势字符串包含判断
const upCount = data.filter(d =>
d.suggestion?.includes('做多') || d.suggestion?.includes('试多')
).length;
const downCount = data.filter(d =>
d.suggestion?.includes('做空') || d.suggestion?.includes('试空')
).length;
const neutralCount = data.filter(d =>
d.suggestion?.includes('观望')
).length;
// 安全检查确保元素存在再设置textContent
const totalCountEl = document.getElementById('total-count');
if (totalCountEl) totalCountEl.textContent = total;
const upCountEl = document.getElementById('up-count');
if (upCountEl) upCountEl.textContent = upCount;
const downCountEl = document.getElementById('down-count');
if (downCountEl) downCountEl.textContent = downCount;
const neutralCountEl = document.getElementById('neutral-count');
if (neutralCountEl) neutralCountEl.textContent = neutralCount;
const countAllEl = document.getElementById('count-all');
if (countAllEl) countAllEl.textContent = total;
const countWatchedEl = document.getElementById('count-watched');
if (countWatchedEl) countWatchedEl.textContent = watchedSymbols.length;
}
function filterByTrend(trend) {
console.log('按趋势筛选:', trend);
let filtered = allFuturesData;
if (trend === 'up') {
filtered = allFuturesData.filter(d =>
d.suggestion?.includes('做多') || d.suggestion?.includes('试多')
);
} else if (trend === 'down') {
filtered = allFuturesData.filter(d =>
d.suggestion?.includes('做空') || d.suggestion?.includes('试空')
);
} else if (trend === 'neutral') {
filtered = allFuturesData.filter(d =>
d.suggestion?.includes('观望')
);
}
renderFuturesGrid(filtered);
}
function filterFuturesList(keyword) {
keyword = keyword.toLowerCase();
const activePill = document.querySelector('.pill.active');
const category = activePill ? activePill.dataset.category : 'all';
let filtered = filterDataByCategory(allFuturesData, category);
if (keyword) {
filtered = filtered.filter(item =>
item.name.toLowerCase().includes(keyword) ||
item.symbol.toLowerCase().includes(keyword)
);
}
renderFuturesGrid(filtered);
}
function filterByCategory(category) {
let filtered = filterDataByCategory(allFuturesData, category);
renderFuturesGrid(filtered);
}
function sortFuturesList(sortBy) {
let sorted = [...getCurrentFilteredData()];
switch(sortBy) {
case 'success_rate':
sorted.sort((a, b) => b.successRate - a.successRate);
break;
case 'trend_score':
sorted.sort((a, b) => b.trendScore - a.trendScore);
break;
case 'change_pct':
sorted.sort((a, b) => b.changePct - a.changePct);
break;
case 'name':
sorted.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
break;
}
renderFuturesGrid(sorted);
}
async function loadFuturesDetail(symbol) {
try {
const response = await fetch(`${API_BASE}/detail/${symbol}`);
const data = await response.json();
if (data.success) {
currentDetailData = data.data;
updateDetailView(data.data);
}
} catch (error) {
console.error('加载详情失败:', error);
}
}
function updateDetailView(data) {
const nameEl = document.getElementById('detail-name');
if (nameEl) nameEl.textContent = data.name || '--';
const symbolEl = document.getElementById('detail-symbol');
if (symbolEl) symbolEl.textContent = data.symbol || '--';
const priceEl = document.getElementById('detail-price');
if (priceEl) {
priceEl.textContent = '¥' + formatNumber(data.price);
priceEl.className = 'price-value ' + (data.change >= 0 ? 'up' : 'down');
}
const changeEl = document.getElementById('detail-change');
if (changeEl) {
changeEl.className = 'price-change ' + (data.change >= 0 ? 'up' : 'down');
changeEl.innerHTML = `${data.change >= 0 ? '↑' : '↓'} ${data.change >= 0 ? '+' : ''}${formatNumber(data.change)} (${data.changePct >= 0 ? '+' : ''}${data.changePct.toFixed(2)}%)`;
}
const openEl = document.getElementById('detail-open');
if (openEl) openEl.textContent = formatNumber(data.open);
const highEl = document.getElementById('detail-high');
if (highEl) highEl.textContent = formatNumber(data.high);
const lowEl = document.getElementById('detail-low');
if (lowEl) lowEl.textContent = formatNumber(data.low);
const volumeEl = document.getElementById('detail-volume');
if (volumeEl) volumeEl.textContent = formatNumber(data.volume);
// 注意: detail-r1 和 detail-s1 在新HTML中不存在已移除
// 注意: suggestion-badge, suggestion-reason, entry-price, target-price, stop-loss, risk-level 在新HTML中不存在已移除
if (data.macd) {
const macdSignalEl = document.getElementById('macd-signal');
if (macdSignalEl) macdSignalEl.textContent = data.macd.signal;
const macdDetailEl = document.getElementById('macd-detail');
if (macdDetailEl) macdDetailEl.textContent = data.macd.detail;
}
if (data.rsi) {
const rsiValueEl = document.getElementById('rsi-value');
if (rsiValueEl) rsiValueEl.textContent = data.rsi.value;
const rsiStatusEl = document.getElementById('rsi-status');
if (rsiStatusEl) rsiStatusEl.textContent = data.rsi.status;
}
if (data.boll) {
const bollSignalEl = document.getElementById('boll-signal');
if (bollSignalEl) bollSignalEl.textContent = data.boll.signal;
const bollDetailEl = document.getElementById('boll-detail');
if (bollDetailEl) bollDetailEl.textContent = data.boll.detail;
}
if (data.kdj) {
const kdjSignalEl = document.getElementById('kdj-signal');
if (kdjSignalEl) kdjSignalEl.textContent = data.kdj.signal;
const kdjDetailEl = document.getElementById('kdj-detail');
if (kdjDetailEl) kdjDetailEl.textContent = data.kdj.detail;
}
if (data.resistances) {
for (let i = 0; i < 2; i++) {
const el = document.getElementById(`resistance-${i + 1}`);
if (el) {
el.textContent = formatNumber(data.resistances[i]);
}
}
}
if (data.supports) {
for (let i = 0; i < 2; i++) {
const el = document.getElementById(`support-${i + 1}`);
if (el) {
el.textContent = formatNumber(data.supports[i]);
}
}
}
if (data.pivotPoint) {
const ppEl = document.getElementById('pivot-point');
if (ppEl) ppEl.textContent = formatNumber(data.pivotPoint);
}
if (data.periodConsistency) {
const container = document.getElementById('period-trends');
if (container) {
const periodNames = { '5': '5分钟', '15': '15分钟', '30': '30分钟', '60': '60分钟' };
container.innerHTML = Object.entries(data.periodConsistency).map(([period, trend]) => `
<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>
`;
// 设置5分钟超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 300000); // 5分钟 = 300000毫秒
try {
const response = await fetch(`${API_BASE}/ai-analysis/${currentSymbol}?force_refresh=${forceRefresh}`, {
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await response.json();
if (data.success) {
currentAIAnalysis = data.data;
displayAIAnalysisResult(data.data);
showToast('success', '分析完成', `${currentSymbol} AI分析已完成`);
} else {
content.innerHTML = `
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
<p>${data.error || 'AI分析失败请稍后重试'}</p>
</div>
`;
showToast('error', '分析失败', data.error || 'AI分析失败');
}
} catch (error) {
clearTimeout(timeoutId);
console.error('AI分析请求失败:', error);
if (error.name === 'AbortError') {
content.innerHTML = `
<div style="text-align: center; padding: 28px; color: var(--text-tertiary);">
<p>分析超时,请稍后重试或联系管理员</p>
</div>
`;
showToast('error', '请求超时', 'AI分析耗时过长请稍后重试');
} else {
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');
}
// ==================== 复盘计划功能 V2 ====================
const RV_API_BASE = '/api/v1/review';
let rvCurrentPlanId = null;
let rvCurrentPlanData = null;
function initReviewPlan() {
document.getElementById('rv-date-selector').addEventListener('change', function (e) {
const selectedDate = e.target.value;
if (selectedDate) {
rvLoadPlanByDate(selectedDate);
}
});
document.getElementById('rv-btn-generate').addEventListener('click', function () {
rvHandleGenerate();
});
document.getElementById('rv-btn-clear').addEventListener('click', function () {
rvHandleClear();
});
rvSetupSortButtons();
rvLoadLatestPlan();
}
function rvHandleGenerate() {
const dateSelector = document.getElementById('rv-date-selector');
let reviewDate = dateSelector.value;
if (!reviewDate) {
reviewDate = new Date().toISOString().split('T')[0];
}
rvDoGenerate(reviewDate);
}
function rvDoGenerate(dateStr) {
rvShowLoading();
fetch(`${RV_API_BASE}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ review_date: dateStr })
})
.then(res => res.json())
.then(result => {
rvHideLoading();
if (result.success) {
const s = result.data.summary;
alert(
`复盘与计划生成成功!\n` +
`日期: ${s.review_date} ${s.week_day}\n` +
`品种数: ${s.symbol_count}\n` +
`交易机会: ${s.opportunity_count}`
);
rvLoadLatestPlan();
} else {
alert('生成失败: ' + (result.message || '未知错误'));
}
})
.catch(err => {
rvHideLoading();
console.error('生成复盘计划失败:', err);
alert('生成失败,请稍后重试');
});
}
function rvHandleClear() {
if (!confirm('确定要清除所有复盘与交易计划数据吗?此操作不可恢复。')) return;
fetch(`${RV_API_BASE}/v2/clear`, { method: 'DELETE' })
.then(res => res.json())
.then(data => {
if (data.success) {
alert('复盘数据已清除');
document.getElementById('rv-date-selector').value = '';
rvCurrentPlanId = null;
rvCurrentPlanData = null;
rvHideAllSections();
document.getElementById('rv-empty-state').style.display = 'flex';
} else {
alert(data.message || '清除失败');
}
})
.catch(err => {
console.error('清除数据失败:', err);
alert('清除失败,请稍后重试');
});
}
function rvLoadLatestPlan() {
fetch(`${RV_API_BASE}/v2/plans?limit=1`)
.then(res => res.json())
.then(data => {
if (data.success && data.data && data.data.length > 0) {
const plan = data.data[0];
const dateSelector = document.getElementById('rv-date-selector');
if (dateSelector && plan.review_date) {
dateSelector.value = plan.review_date;
}
rvLoadAndRender(plan.id);
} else {
rvHideAllSections();
document.getElementById('rv-empty-state').style.display = 'flex';
}
})
.catch(err => {
console.error('加载最新计划失败:', err);
});
}
function rvLoadPlanByDate(date) {
rvShowLoading();
fetch(`${RV_API_BASE}/v2/plan/by-date/${date}`)
.then(res => res.json())
.then(data => {
rvHideLoading();
if (data.success && data.data) {
rvCurrentPlanId = data.data.meta.id;
rvCurrentPlanData = data.data;
rvRenderAll(data.data);
} else {
rvHideAllSections();
document.getElementById('rv-empty-state').style.display = 'flex';
}
})
.catch(err => {
rvHideLoading();
console.error('按日期加载计划失败:', err);
});
}
function rvLoadAndRender(planId) {
rvShowLoading();
fetch(`${RV_API_BASE}/v2/plan/${planId}`)
.then(res => res.json())
.then(result => {
rvHideLoading();
if (result.success && result.data) {
rvCurrentPlanId = planId;
rvCurrentPlanData = result.data;
rvRenderAll(result.data);
} else {
alert('加载计划详情失败');
}
})
.catch(err => {
rvHideLoading();
console.error('加载计划详情失败:', err);
alert('加载失败,请稍后重试');
});
}
function rvRenderAll(data) {
const contentArea = document.getElementById('rv-content-area');
if (contentArea) contentArea.style.display = '';
document.getElementById('rv-empty-state').style.display = 'none';
const { meta, scores, plans, sectors } = data;
rvRenderHero(meta);
rvRenderStats(meta, scores);
rvRenderRisk(meta);
rvRenderGreenSection(plans, scores);
rvRenderYellowSection(scores);
rvRenderRedSection(scores);
rvRenderRankingTable(scores);
rvRenderDetails(scores);
rvRenderSectors(sectors);
}
function rvRenderHero(meta) {
const el = document.getElementById('rv-hero-section');
if (!el) return;
el.innerHTML = `
<h1>${meta.review_date} <span style="font-size: 18px; opacity: 0.8;">${meta.week_day || ''}</span></h1>
<div class="rv-hero-subtitle">${meta.data_basis || ''}</div>
<div class="rv-hero-conclusion">${meta.core_conclusion || ''}</div>
`;
el.style.display = '';
}
function rvRenderStats(meta, scores) {
const el = document.getElementById('rv-stats-grid');
if (!el) return;
const top3 = scores.slice(0, 3);
const top3Text = top3.map(s => `${s.symbol} ${s.name}(${s.composite_score}分)`).join('、');
el.innerHTML = `
<div class="rv-stat-card">
<div class="rv-stat-label">核心结论</div>
<div class="rv-stat-text">${meta.core_conclusion || '-'}</div>
</div>
<div class="rv-stat-card">
<div class="rv-stat-label">TOP 3 机会</div>
<div class="rv-stat-value" style="font-size: 14px;">${top3Text || '-'}</div>
</div>
<div class="rv-stat-card">
<div class="rv-stat-label">市场概览</div>
<div class="rv-stat-text">多头 ${meta.bull_count || 0} | 空头 ${meta.bear_count || 0} | 震荡 ${meta.neutral_count || 0}</div>
</div>
`;
el.style.display = '';
}
function rvRenderRisk(meta) {
const banner = document.getElementById('rv-risk-banner');
const list = document.getElementById('rv-risk-list');
if (!banner || !list) return;
const warnings = meta.risk_warnings || [];
if (warnings.length === 0) {
banner.style.display = 'none';
return;
}
list.innerHTML = warnings.map(w => `<li>${w}</li>`).join('');
banner.style.display = '';
}
function rvFormatPrice(value, refPrice) {
if (!value && value !== 0) return '-';
// 根据参考价格确定小数位数
let precision = 2;
if (refPrice >= 10000) precision = 0;
else if (refPrice >= 1000) precision = 1;
return Number(value).toFixed(precision);
}
function rvScoreBarColor(score) {
if (score > 70) return '#34C759'; // --color-down
if (score > 40) return '#007AFF'; // --color-brand
if (score > 20) return '#FF9F0A'; // --color-neutral
return '#FF3B30'; // --color-up
}
function rvScoreBar(score, label) {
const color = rvScoreBarColor(score);
return `
<div class="rv-score-bar-item">
<span class="rv-score-bar-label">${label}</span>
<div class="rv-score-bar-track">
<div class="rv-score-bar-fill" style="width: ${Math.min(100, score)}%; background: ${color};"></div>
</div>
<span class="rv-score-bar-value" style="color: ${color};">${Math.round(score)}</span>
</div>
`;
}
function rvDirectionTag(tag) {
let cls = 'rv-tag-neutral';
if (tag === '强多' || tag === '偏多') cls = 'rv-tag-bull';
else if (tag === '强空' || tag === '偏空') cls = 'rv-tag-bear';
return `<span class="rv-item-tag ${cls}">${tag || '震荡'}</span>`;
}
function rvRenderGreenSection(plans, scores) {
const section = document.getElementById('rv-green-section');
const grid = document.getElementById('rv-green-grid');
if (!section || !grid) return;
const greenPlans = plans.filter(p => p.category === 'green');
if (greenPlans.length === 0) {
section.style.display = 'none';
return;
}
grid.innerHTML = greenPlans.map(p => {
const score = scores.find(s => s.symbol === p.symbol) || {};
const dirText = p.direction === 'long' ? '做多' : '做空';
const dirColor = p.direction === 'long' ? 'var(--color-down)' : 'var(--color-up)';
const dataDateTag = score.data_date ? `<span class="rv-data-date-tag">${score.data_date}</span>` : '';
return `
<div class="rv-opp-card" onclick="rvShowDetail('${p.symbol}','${(p.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<div class="rv-opp-header">
<span class="rv-opp-symbol">${p.symbol} ${p.name}</span>
<span class="rv-opp-score">${p.composite_score}分</span>
</div>
<div class="rv-opp-info">
<div class="rv-opp-info-item"><span class="rv-opp-info-label">方向</span><span style="color: ${dirColor}; font-weight: 600;">${dirText}</span></div>
<div class="rv-opp-info-item"><span class="rv-opp-info-label">收盘价</span><span>${score.close_price || '-'} ${dataDateTag}</span></div>
<div class="rv-opp-info-item"><span class="rv-opp-info-label">涨跌幅</span><span>${score.change_pct || 0}%</span></div>
<div class="rv-opp-info-item"><span class="rv-opp-info-label">量比</span><span>${score.volume_ratio || '-'}</span></div>
</div>
<div class="rv-score-bars">
${rvScoreBar(p.amplitude_score || 0, '振幅')}
${rvScoreBar(p.volume_score || 0, '量能')}
${rvScoreBar(p.trend_score || 0, '趋势')}
${rvScoreBar(p.activity_score || 0, '活跃')}
</div>
<div class="rv-plan-box">
<div class="rv-plan-row"><span>入场区间</span><span>${rvFormatPrice(p.entry_low, score.close_price)} ~ ${rvFormatPrice(p.entry_high, score.close_price)}</span></div>
<div class="rv-plan-row"><span>止损位</span><span style="color: var(--color-up);">${rvFormatPrice(p.stop_loss, score.close_price)}</span></div>
<div class="rv-plan-row"><span>目标一</span><span style="color: var(--color-down);">${rvFormatPrice(p.target1, score.close_price)}</span></div>
<div class="rv-plan-row"><span>目标二</span><span style="color: var(--color-down);">${rvFormatPrice(p.target2, score.close_price)}</span></div>
<div class="rv-plan-trigger">触发条件: ${p.trigger || '-'}</div>
</div>
</div>
`;
}).join('');
section.style.display = '';
}
function rvRenderYellowSection(scores) {
const section = document.getElementById('rv-yellow-section');
const tbody = document.getElementById('rv-yellow-list');
if (!section || !tbody) return;
const yellowItems = scores.filter(s => s.category === 'yellow');
if (yellowItems.length === 0) {
section.style.display = 'none';
return;
}
tbody.innerHTML = yellowItems.map((s, i) => {
const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)';
return `
<tr onclick="rvShowDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td>${i + 1}</td>
<td><strong>${s.symbol}</strong> ${s.name}</td>
<td>${s.close_price || '-'}${s.data_date ? '<span class="rv-data-date-tag">' + s.data_date + '</span>' : ''}</td>
<td style="color: ${changeColor};">${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%</td>
<td><strong>${s.composite_score}</strong></td>
<td>${s.amplitude_pct || 0}%</td>
<td>${s.volume_ratio || '-'}</td>
<td>${rvDirectionTag(s.direction_tag)}</td>
<td style="font-size:11px;color:var(--text-tertiary);">${s.data_date || '-'}</td>
</tr>
`;
}).join('');
section.style.display = '';
}
function rvRenderRedSection(scores) {
const section = document.getElementById('rv-red-section');
const tbody = document.getElementById('rv-red-list');
if (!section || !tbody) return;
const redItems = scores.filter(s => s.category === 'red');
if (redItems.length === 0) {
section.style.display = 'none';
return;
}
tbody.innerHTML = redItems.map((s, i) => {
const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)';
return `
<tr onclick="rvShowDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td>${i + 1}</td>
<td><strong>${s.symbol}</strong> ${s.name}</td>
<td>${s.close_price || '-'}${s.data_date ? '<span class="rv-data-date-tag">' + s.data_date + '</span>' : ''}</td>
<td style="color: ${changeColor};">${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%</td>
<td><strong>${s.composite_score}</strong></td>
<td>${s.amplitude_pct || 0}%</td>
<td>${s.volume_ratio || '-'}</td>
<td>${rvDirectionTag(s.direction_tag)}</td>
<td style="font-size:11px;color:var(--text-tertiary);">${s.data_date || '-'}</td>
</tr>
`;
}).join('');
section.style.display = '';
}
let rvCurrentSortField = 'composite_score';
let rvCurrentSortOrder = 'desc';
function rvRenderRankingTable(scores) {
const section = document.getElementById('rv-table-section');
const table = document.getElementById('rv-ranking-table');
if (!section || !table) return;
if (!scores || scores.length === 0) {
section.style.display = 'none';
return;
}
// 排序
const sorted = [...scores].sort((a, b) => {
let va = a[rvCurrentSortField];
let vb = b[rvCurrentSortField];
// 方向排序特殊处理
if (rvCurrentSortField === 'direction_tag') {
const dirOrder = { '强多': 1, '偏多': 2, '多头共振': 1, '震荡': 3, '偏空': 4, '强空': 5, '空头共振': 5 };
va = dirOrder[va] || 3;
vb = dirOrder[vb] || 3;
}
if (va == null) va = -Infinity;
if (vb == null) vb = -Infinity;
if (rvCurrentSortOrder === 'asc') return va > vb ? 1 : va < vb ? -1 : 0;
return va < vb ? 1 : va > vb ? -1 : 0;
});
let html = `
<thead>
<tr>
<th>#</th>
<th>品种</th>
<th>收盘价</th>
<th>综合评分</th>
<th>振幅%</th>
<th>涨跌幅%</th>
<th>量比</th>
<th>趋势分</th>
<th>方向</th>
<th>活跃度</th>
<th>数据日期</th>
</tr>
</thead>
<tbody>
`;
sorted.forEach((s, i) => {
const scoreColor = rvScoreBarColor(s.composite_score);
const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)';
html += `
<tr onclick="rvShowDetail('${s.symbol}','${(s.name || '').replace(/'/g, "\\'")}')" style="cursor:pointer">
<td>${i + 1}</td>
<td><strong>${s.symbol}</strong> ${s.name}</td>
<td>${s.close_price || '-'}${s.data_date ? '<span class="rv-data-date-tag">' + s.data_date + '</span>' : ''}</td>
<td>
<strong>${s.composite_score}</strong>
<div class="rv-table-score-bar">
<div class="rv-table-score-fill" style="width: ${Math.min(100, s.composite_score)}%; background: ${scoreColor};"></div>
</div>
</td>
<td>${s.amplitude_pct || 0}</td>
<td style="color: ${changeColor};">${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}</td>
<td>${s.volume_ratio || '-'}</td>
<td>${s.trend_score || 0}</td>
<td>${rvDirectionTag(s.direction_tag)}</td>
<td>${s.activity_score || 0}</td>
<td style="font-size:11px;color:var(--text-tertiary);">${s.data_date || '-'}</td>
</tr>
`;
});
html += '</tbody>';
table.innerHTML = html;
section.style.display = '';
}
let rvSortButtonsInitialized = false;
function rvSetupSortButtons() {
if (rvSortButtonsInitialized) return;
const controls = document.getElementById('rv-sort-controls');
if (!controls) return;
controls.addEventListener('click', function (e) {
const btn = e.target.closest('.rv-sort-btn');
if (!btn) return;
const field = btn.dataset.sort;
const defaultOrder = btn.dataset.order || 'desc';
// 切换排序方向
if (rvCurrentSortField === field) {
rvCurrentSortOrder = rvCurrentSortOrder === 'desc' ? 'asc' : 'desc';
} else {
rvCurrentSortField = field;
rvCurrentSortOrder = defaultOrder;
}
// 更新按钮状态
controls.querySelectorAll('.rv-sort-btn').forEach(b => {
b.classList.remove('active');
b.textContent = b.textContent.replace(/ [▲▼]$/, '');
});
btn.classList.add('active');
const arrow = rvCurrentSortOrder === 'desc' ? ' ▼' : ' ▲';
btn.textContent = btn.textContent + arrow;
// 重新渲染
if (rvCurrentPlanData) {
rvRenderRankingTable(rvCurrentPlanData.scores);
}
});
rvSortButtonsInitialized = true;
}
function rvRenderDetails(scores) {
const section = document.getElementById('rv-details-section');
const grid = document.getElementById('rv-details-grid');
if (!section || !grid) return;
if (!scores || scores.length === 0) {
section.style.display = 'none';
return;
}
grid.innerHTML = scores.slice(0, 12).map(s => `
<div class="rv-detail-card">
<div class="rv-detail-title">${s.symbol} ${s.name}</div>
<div class="rv-detail-metrics">
<div class="rv-detail-metric"><span>收盘价</span><span>${s.close_price || '-'}</span></div>
<div class="rv-detail-metric"><span>涨跌幅</span><span style="color: ${s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)'};">${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%</span></div>
<div class="rv-detail-metric"><span>振幅</span><span>${s.amplitude_pct || 0}%</span></div>
<div class="rv-detail-metric"><span>量比</span><span>${s.volume_ratio || '-'}</span></div>
<div class="rv-detail-metric"><span>60m趋势</span><span>${s.trend_60m || 0}</span></div>
<div class="rv-detail-metric"><span>15m趋势</span><span>${s.trend_15m || 0}</span></div>
<div class="rv-detail-metric"><span>5m趋势</span><span>${s.trend_5m || 0}</span></div>
<div class="rv-detail-metric"><span>综合评分</span><span style="color: ${rvScoreBarColor(s.composite_score)};">${s.composite_score}</span></div>
</div>
<div class="rv-pivot-tags">
<span class="rv-pivot-tag resist">R2: ${rvFormatPrice(s.r2, s.close_price)}</span>
<span class="rv-pivot-tag resist">R1: ${rvFormatPrice(s.r1, s.close_price)}</span>
<span class="rv-pivot-tag pivot">P: ${rvFormatPrice(s.pivot, s.close_price)}</span>
<span class="rv-pivot-tag support">S1: ${rvFormatPrice(s.s1, s.close_price)}</span>
<span class="rv-pivot-tag support">S2: ${rvFormatPrice(s.s2, s.close_price)}</span>
</div>
</div>
`).join('');
section.style.display = '';
}
function rvRenderSectors(sectors) {
const section = document.getElementById('rv-sectors-section');
const grid = document.getElementById('rv-sectors-grid');
if (!section || !grid) return;
if (!sectors || sectors.length === 0) {
section.style.display = 'none';
return;
}
grid.innerHTML = sectors.map(sec => {
const heatIcon = sec.heat_level >= 2 ? '🔥' : sec.heat_level === 1 ? '🌡️' : '🧊';
const dirColor = sec.direction === '多头' ? 'var(--color-down)' : sec.direction === '空头' ? 'var(--color-up)' : 'var(--text-tertiary)';
return `
<div class="rv-sector-card">
<div class="rv-sector-header">
<span class="rv-sector-name">${sec.sector_name}</span>
<span class="rv-sector-heat">${heatIcon}</span>
</div>
<div class="rv-sector-info">
均分: <strong>${sec.avg_score}</strong> |
趋势: <span style="color: ${dirColor};">${sec.avg_trend}</span> |
方向: <span style="color: ${dirColor};">${sec.direction}</span>
</div>
<div class="rv-sector-info">龙头: ${sec.leader_symbol} (${sec.leader_score}分)</div>
<div class="rv-sector-members">
${(sec.members || []).map(m => `<span class="rv-sector-member">${m.name} ${m.score}</span>`).join('')}
</div>
</div>
`;
}).join('');
section.style.display = '';
}
function rvShowLoading() {
const el = document.getElementById('rv-loading');
if (el) el.classList.add('active');
}
function rvHideLoading() {
const el = document.getElementById('rv-loading');
if (el) el.classList.remove('active');
}
function rvHideAllSections() {
const contentArea = document.getElementById('rv-content-area');
if (contentArea) contentArea.style.display = 'none';
const sections = ['rv-hero-section', 'rv-stats-grid', 'rv-risk-banner', 'rv-green-section', 'rv-yellow-section', 'rv-red-section', 'rv-table-section', 'rv-details-section', 'rv-sectors-section'];
sections.forEach(id => {
const el = document.getElementById(id);
if (el) el.style.display = 'none';
});
}
// ==================== 复盘品种详情弹窗 ====================
function rvShowDetail(symbol, name) {
const scores = rvCurrentPlanData ? rvCurrentPlanData.scores : [];
const plans = rvCurrentPlanData ? rvCurrentPlanData.plans : [];
const s = scores.find(x => x.symbol === symbol);
if (!s) return;
const plan = plans.find(p => p.symbol === symbol);
const modal = document.getElementById('rv-detail-modal');
if (!modal) return;
document.getElementById('rv-modal-title').textContent = `${symbol} ${name || s.name || ''}`;
const body = document.getElementById('rv-modal-body');
// 交易计划部分
let planHtml = '';
if (plan) {
const dirText = plan.direction === 'long' ? '做多' : '做空';
const dirColor = plan.direction === 'long' ? 'var(--color-down)' : 'var(--color-up)';
planHtml = `
<div class="rv-modal-section">
<div class="rv-modal-section-title">交易计划</div>
<div class="rv-modal-plan-box">
<div class="rv-modal-plan-row"><span class="mp-label">方向</span><span class="mp-val" style="color:${dirColor}">${dirText}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">入场区间</span><span class="mp-val">${rvFormatPrice(plan.entry_low, s.close_price)} ~ ${rvFormatPrice(plan.entry_high, s.close_price)}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">止损位</span><span class="mp-val stop">${rvFormatPrice(plan.stop_loss, s.close_price)}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">目标一</span><span class="mp-val target">${rvFormatPrice(plan.target1, s.close_price)}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">目标二</span><span class="mp-val target">${rvFormatPrice(plan.target2, s.close_price)}</span></div>
<div class="rv-modal-plan-row"><span class="mp-label">触发条件</span><span class="mp-val">${plan.trigger || '-'}</span></div>
</div>
</div>
`;
}
const changeColor = s.change_pct >= 0 ? 'var(--color-down)' : 'var(--color-up)';
body.innerHTML = `
<div class="rv-modal-section">
<div class="rv-modal-section-title">基本信息</div>
<div class="rv-modal-metrics-grid">
<div class="rv-modal-metric"><span class="mm-label">收盘价</span><span class="mm-val">${s.close_price || '-'}${s.data_date ? '<span class="rv-data-date-tag">' + s.data_date + '</span>' : ''}</span></div>
<div class="rv-modal-metric"><span class="mm-label">涨跌幅</span><span class="mm-val" style="color:${changeColor}">${s.change_pct >= 0 ? '+' : ''}${s.change_pct || 0}%</span></div>
<div class="rv-modal-metric"><span class="mm-label">振幅</span><span class="mm-val">${s.amplitude_pct || 0}%</span></div>
<div class="rv-modal-metric"><span class="mm-label">量比</span><span class="mm-val">${s.volume_ratio || '-'}</span></div>
<div class="rv-modal-metric"><span class="mm-label">综合评分</span><span class="mm-val" style="color:${rvScoreBarColor(s.composite_score)}"><b>${s.composite_score}</b></span></div>
<div class="rv-modal-metric"><span class="mm-label">排名</span><span class="mm-val">#${s.rank || '-'}</span></div>
</div>
</div>
${planHtml}
<div class="rv-modal-section">
<div class="rv-modal-section-title">评分明细</div>
<div class="rv-modal-metrics-grid">
<div class="rv-modal-metric"><span class="mm-label">振幅评分</span><span class="mm-val">${s.amplitude_score || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">量能评分</span><span class="mm-val">${s.volume_score || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">趋势评分</span><span class="mm-val">${s.trend_score || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">活跃度</span><span class="mm-val">${s.activity_score || 0}</span></div>
</div>
</div>
<div class="rv-modal-section">
<div class="rv-modal-section-title">多周期趋势</div>
<div class="rv-modal-metrics-grid">
<div class="rv-modal-metric"><span class="mm-label">60分钟</span><span class="mm-val">${s.trend_60m || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">15分钟</span><span class="mm-val">${s.trend_15m || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">5分钟</span><span class="mm-val">${s.trend_5m || 0}</span></div>
<div class="rv-modal-metric"><span class="mm-label">方向</span><span class="mm-val">${rvDirectionTag(s.direction_tag)}</span></div>
</div>
</div>
<div class="rv-modal-section">
<div class="rv-modal-section-title">枢轴点位</div>
<div class="rv-modal-pivots">
<span class="rv-pivot-tag resist">R2: ${rvFormatPrice(s.r2, s.close_price)}</span>
<span class="rv-pivot-tag resist">R1: ${rvFormatPrice(s.r1, s.close_price)}</span>
<span class="rv-pivot-tag pivot">P: ${rvFormatPrice(s.pivot, s.close_price)}</span>
<span class="rv-pivot-tag support">S1: ${rvFormatPrice(s.s1, s.close_price)}</span>
<span class="rv-pivot-tag support">S2: ${rvFormatPrice(s.s2, s.close_price)}</span>
</div>
</div>
`;
modal.classList.add('show');
}
function rvCloseDetailModal() {
const modal = document.getElementById('rv-detail-modal');
if (modal) modal.classList.remove('show');
}
// ==================== 自选品种功能 ====================
function renderWatchedList() {
const grid = document.getElementById('watched-grid');
const emptyState = document.getElementById('watched-empty');
const totalCountEl = document.getElementById('watched-total-count');
if (watchedSymbols.length === 0) {
grid.style.display = 'none';
emptyState.style.display = 'flex';
totalCountEl.textContent = '0 个品种';
return;
}
grid.style.display = 'grid';
emptyState.style.display = 'none';
totalCountEl.textContent = `${watchedSymbols.length} 个品种`;
const watchedData = allFuturesData.filter(item => watchedSymbols.includes(item.symbol));
grid.innerHTML = watchedData.map(item => {
const code = item.symbol.replace(/[0-9]/g, '').substring(0, 2);
const changeIcon = item.change >= 0 ? '↑' : '↓';
const changeSign = item.change >= 0 ? '+' : '';
const pctSign = item.changePct >= 0 ? '+' : '';
let actionPillClass = 'watch';
let actionPillText = '观望';
if (item.suggestion?.includes('做多') || item.suggestion?.includes('试多')) {
actionPillClass = 'do-more';
actionPillText = '做多';
} else if (item.suggestion?.includes('做空') || item.suggestion?.includes('试空')) {
actionPillClass = 'do-more';
actionPillText = '做空';
}
const priceClass = item.change >= 0 ? 'up' : 'down';
const changeClass = item.change >= 0 ? 'up' : 'down';
const successRate = item.successRate || 0;
const trendScore = item.trendScore || 0;
const successColor = successRate >= 70 ? 'var(--color-down)' : successRate >= 60 ? 'var(--color-neutral)' : 'var(--color-up)';
const trendColor = trendScore >= 70 ? 'var(--color-down)' : trendScore >= 50 ? 'var(--color-neutral)' : 'var(--color-up)';
const periods = item.periods || {};
const resistance = item.resistance ? formatNumber(item.resistance) : '--';
const support = item.support ? formatNumber(item.support) : '--';
const isWatched = true;
return `
<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 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('');
}