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

4812 lines
202 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('');
}
// ==================== 交易反思子系统 ====================
let trReflCurrentDate = null;
let trReflData = null;
let trReflTags = [];
let trReflExperiences = [];
let trReflTagState = new Set();
let trExpPage = 1;
let trExpPageSize = 12;
let trExpTotal = 0;
let trReflCurrentPairId = null;
let trReflCurrentReflectionId = null;
let trReflCurrentAIResult = null;
let trReflAIHistory = [];
let trReflCurrentAnalysis = null;
function trSwitchSubtab(tab) {
document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => {
btn.classList.toggle('active', btn.dataset.subtab === tab);
});
document.querySelectorAll('#trade-review-view > .tr-tab-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === 'tr-tab-' + tab);
});
if (tab === 'experience') trExpLoad();
}
function trSwitchToReflection(date) {
trSwitchSubtab('reflection');
trReflSetDate(date);
}
function trReflInit() {
const dateInput = document.getElementById('tr-refl-current-date');
if (!dateInput) return;
// 默认日期取统计分析的最新交易日或今天
const today = new Date().toISOString().slice(0, 10);
trReflSetDate(today);
document.getElementById('tr-refl-prev').addEventListener('click', () => trReflChangeDate(-1));
document.getElementById('tr-refl-next').addEventListener('click', () => trReflChangeDate(1));
document.getElementById('tr-refl-edit-daily').addEventListener('click', trReflOpenDailyModal);
document.getElementById('tr-refl-edit-form').addEventListener('submit', e => {
e.preventDefault();
trReflSaveEdit();
});
document.getElementById('tr-refl-daily-form').addEventListener('submit', e => {
e.preventDefault();
trReflSaveDaily();
});
const saveAndAiBtn = document.getElementById('tr-refl-save-and-ai');
if (saveAndAiBtn) {
saveAndAiBtn.addEventListener('click', e => {
e.preventDefault();
trReflSaveAndAnalyze();
});
}
const manualPairBtn = document.getElementById('tr-refl-manual-pair-btn');
if (manualPairBtn) manualPairBtn.addEventListener('click', trReflManualPair);
const selectAll = document.getElementById('tr-refl-unpaired-select-all');
if (selectAll) {
selectAll.addEventListener('change', function() {
document.querySelectorAll('.tr-refl-unpaired-check').forEach(cb => cb.checked = this.checked);
});
}
document.querySelectorAll('#tr-refl-discipline-score .tr-form-star, #tr-refl-daily-discipline-score .tr-form-star').forEach(star => {
star.addEventListener('click', () => {
const container = star.parentElement;
const score = parseInt(star.dataset.score);
container.dataset.value = score;
trReflUpdateStars(container, score);
});
});
document.getElementById('tr-exp-search').addEventListener('input', () => { trExpPage = 1; trExpLoad(); });
document.getElementById('tr-exp-tag-filter').addEventListener('change', () => { trExpPage = 1; trExpLoad(); });
document.getElementById('tr-exp-type-filter').addEventListener('change', () => { trExpPage = 1; trExpLoad(); });
document.getElementById('tr-exp-prev').addEventListener('click', () => { if (trExpPage > 1) { trExpPage--; trExpLoad(); } });
document.getElementById('tr-exp-next').addEventListener('click', () => { if (trExpPage * trExpPageSize < trExpTotal) { trExpPage++; trExpLoad(); } });
document.querySelectorAll('.tr-exp-viewtab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tr-exp-viewtab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
trExpRender();
});
});
trReflLoadTags();
}
function trReflSetDate(date) {
trReflCurrentDate = date;
document.getElementById('tr-refl-current-date').textContent = date;
trReflLoadData();
}
function trReflChangeDate(days) {
const d = new Date(trReflCurrentDate);
d.setDate(d.getDate() + days);
trReflSetDate(d.toISOString().slice(0, 10));
}
async function trReflLoadData() {
if (!trReflCurrentDate) return;
try {
const res = await fetch(`${TR_API_BASE}/daily-trades/${trReflCurrentDate}`);
const json = await res.json();
trReflData = json.success ? json.data : null;
if (!trReflData) trReflData = { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
trReflRender();
} catch (e) {
console.error('加载反思数据失败', e);
trReflData = { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
trReflRender();
}
}
function trReflRender() {
const data = trReflData || { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
const summary = data.summary || {};
const daily = data.daily_reflection || {};
const pnl = summary.total_pnl || 0;
const pnlEl = document.getElementById('tr-refl-stat-pnl');
pnlEl.textContent = (pnl >= 0 ? '+' : '') + pnl.toFixed(2);
pnlEl.className = 'tr-refl-statvalue ' + (pnl >= 0 ? 'profit' : 'loss');
document.getElementById('tr-refl-stat-trades').textContent = summary.total_trades || 0;
document.getElementById('tr-refl-stat-winrate').textContent = (summary.win_rate || 0) + '%';
const reflectedEl = document.getElementById('tr-refl-stat-reflected');
const reflected = summary.reflected_count || 0;
const totalPairs = (data.pairs || []).length;
if (totalPairs === 0) {
reflectedEl.innerHTML = '<span class="tr-refl-reflected no">无交易</span>';
} else if (reflected >= totalPairs) {
reflectedEl.innerHTML = '<span class="tr-refl-reflected yes">✓ 已完成</span>';
} else {
reflectedEl.innerHTML = `<span class="tr-refl-reflected no">${reflected}/${totalPairs}</span>`;
}
document.getElementById('tr-refl-emotion').textContent = trReflTextMap.emotion[daily.emotion_state] || daily.emotion_state || '未填写';
document.getElementById('tr-refl-emotion').className = 'tr-refl-daily-value ' + (daily.emotion_state ? '' : 'empty');
document.getElementById('tr-refl-discipline').textContent = daily.discipline_score ? daily.discipline_score + '/5' : '未填写';
document.getElementById('tr-refl-discipline').className = 'tr-refl-daily-value ' + (daily.discipline_score ? '' : 'empty');
document.getElementById('tr-refl-overall').textContent = trReflTextMap.overall[daily.overall_rating] || daily.overall_rating || '未填写';
document.getElementById('tr-refl-overall').className = 'tr-refl-daily-value ' + (daily.overall_rating ? '' : 'empty');
document.getElementById('tr-refl-market').textContent = trReflTextMap.market[daily.market_judgment] || daily.market_judgment || '未填写';
document.getElementById('tr-refl-market').className = 'tr-refl-daily-value ' + (daily.market_judgment ? '' : 'empty');
const pairsContainer = document.getElementById('tr-refl-pairs');
const pairs = data.pairs || [];
if (!pairs.length) {
pairsContainer.innerHTML = '<div class="tr-empty" style="grid-column:1/-1;"><div class="tr-empty-icon">📝</div><div class="tr-empty-text">当日无交易配对</div></div>';
} else {
pairsContainer.innerHTML = pairs.map(p => trReflRenderPairCard(p)).join('');
}
const unpairedBody = document.getElementById('tr-refl-unpaired-body');
const unpairedEmpty = document.getElementById('tr-refl-unpaired-empty');
const selectAll = document.getElementById('tr-refl-unpaired-select-all');
const unpaired = data.unpaired || [];
if (!unpaired.length) {
unpairedBody.innerHTML = '';
unpairedEmpty.style.display = 'flex';
if (selectAll) selectAll.checked = false;
} else {
unpairedEmpty.style.display = 'none';
unpairedBody.innerHTML = unpaired.map(u => {
const pnl = u.net_pnl || u.close_pnl || 0;
const pnlCls = pnl > 0 ? 'tr-pnl-positive' : (pnl < 0 ? 'tr-pnl-negative' : '');
return `<tr>
<td><input type="checkbox" class="tr-refl-unpaired-check" data-id="${u.id}" data-offset="${u.offset}" data-direction="${u.direction}" data-symbol="${u.symbol || u.variety || ''}"></td>
<td>${u.trade_time || '-'}</td>
<td>${u.variety}${u.symbol_name ? '(' + u.symbol_name + ')' : ''}</td>
<td>${u.offset || '-'}</td>
<td>${u.direction || '-'}</td>
<td>${(u.price || 0).toFixed(2)}</td>
<td>${u.volume || 0}</td>
<td class="${pnlCls}">${pnl !== 0 ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) : '-'}</td>
</tr>`;
}).join('');
}
}
async function trReflManualPair() {
const checked = Array.from(document.querySelectorAll('.tr-refl-unpaired-check:checked'));
if (!checked.length) {
showToast('warning', '请选择记录', '请至少选择一条未配对交易记录');
return;
}
const openRecords = checked.filter(cb => cb.dataset.offset === '开');
const closeRecords = checked.filter(cb => cb.dataset.offset === '平');
if (!openRecords.length) {
showToast('warning', '缺少开仓记录', '手动配对需要至少一条开仓记录(开)');
return;
}
if (!closeRecords.length) {
showToast('warning', '缺少平仓记录', '手动配对需要至少一条平仓记录(平)');
return;
}
const symbols = new Set(checked.map(cb => cb.dataset.symbol));
if (symbols.size > 1) {
showToast('warning', '品种不一致', '请选择同一品种的开仓和平仓记录');
return;
}
const openDirection = openRecords[0].dataset.direction;
const closeDirection = closeRecords[0].dataset.direction;
// 多头:开=买,平=卖;空头:开=卖,平=买
const isLong = openDirection === '买' && closeDirection === '卖';
const isShort = openDirection === '卖' && closeDirection === '买';
if (!isLong && !isShort) {
showToast('warning', '方向不匹配', '多头配对:开仓为买、平仓为卖;空头配对:开仓为卖、平仓为买');
return;
}
const openRecordIds = openRecords.map(cb => parseInt(cb.dataset.id));
const closeRecordIds = closeRecords.map(cb => parseInt(cb.dataset.id));
try {
const res = await fetch(`${TR_API_BASE}/trade-pairs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ open_record_ids: openRecordIds, close_record_ids: closeRecordIds }),
});
const json = await res.json();
if (json.success) {
showToast('success', '配对成功', '交易配对已创建');
trReflLoadData();
} else {
showToast('error', '配对失败', json.message || '请重试');
}
} catch (e) {
showToast('error', '配对失败', e.message);
}
}
function trReflRenderPairCard(p) {
const pnl = p.net_pnl || 0;
const pnlCls = pnl >= 0 ? 'profit' : 'loss';
const dirCls = p.direction === '多' ? 'long' : 'short';
const dirText = p.direction === '多' ? '做多' : '做空';
const tags = (p.tags || []).map(t => `<span class="tr-refl-tag">${typeof t === 'string' ? t : t.name}</span>`).join('');
const hasReflection = p.reflection_status === 'done';
return `<div class="tr-refl-card ${pnlCls}">
<div class="tr-refl-card-header">
<div>
<div class="tr-refl-card-symbol">${p.symbol_name || p.variety || p.symbol || '-'}</div>
<span class="tr-refl-card-dir ${dirCls}">${dirText}</span>
</div>
<div class="tr-refl-card-pnl ${pnlCls}">${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}</div>
</div>
<div class="tr-refl-card-nodes">
开仓:${p.open_date || '-'} ${p.open_time || ''} @ ${(p.open_price || 0).toFixed(2)}<br>
平仓:${p.close_date || '-'} ${p.close_time || ''} @ ${(p.close_price || 0).toFixed(2)}
</div>
<div class="tr-refl-card-tags">${tags}</div>
<div class="tr-refl-card-actions">
<button class="tr-btn tr-btn-primary" onclick='trReflOpenEditModal(${p.id})'>
<span>${hasReflection ? '✏️' : '✍️'}</span><span>${hasReflection ? '编辑反思' : '写反思'}</span>
</button>
<button class="tr-btn tr-btn-secondary" onclick='trReflOpenTagsModal(${p.id})'>
<span>🏷️</span><span>标签</span>
</button>
<button class="tr-btn tr-btn-secondary" onclick='trReflOpenAIModal(${p.id})'>
<span>🤖</span><span>查看分析</span>
</button>
</div>
</div>`;
}
async function trReflLoadTags() {
try {
const res = await fetch(`${TR_API_BASE}/tags`);
const json = await res.json();
if (json.success) trReflTags = json.data || [];
} catch (e) { console.error('加载标签失败', e); }
}
// ===== 反思编辑 Modal =====
async function trReflOpenEditModal(pairId) {
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return;
trReflCurrentPairId = pairId;
trReflCurrentReflectionId = null;
document.getElementById('tr-refl-edit-pair-id').value = pairId;
let reflection = {};
let selectedTagNames = new Set();
try {
const [reflRes, tagsRes] = await Promise.all([
fetch(`${TR_API_BASE}/reflections?trade_pair_id=${pairId}`),
fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`),
]);
const reflJson = await reflRes.json();
const tagsJson = await tagsRes.json();
if (reflJson.success && reflJson.data && reflJson.data.length) {
reflection = reflJson.data[0];
trReflCurrentReflectionId = reflection.id || null;
}
if (tagsJson.success && tagsJson.data) {
selectedTagNames = new Set(tagsJson.data.map(t => typeof t === 'string' ? t : t.name));
}
} catch (e) {
console.error('加载反思/标签失败', e);
}
document.getElementById('tr-refl-entry-reason').value = reflection.entry_reason || '';
document.getElementById('tr-refl-exit-reason').value = reflection.exit_reason || '';
document.getElementById('tr-refl-entry-timing').value = reflection.entry_timing || '';
document.getElementById('tr-refl-exit-timing').value = reflection.exit_timing || '';
document.getElementById('tr-refl-position').value = reflection.position_management || '';
document.getElementById('tr-refl-free').value = reflection.free_reflection || '';
const score = reflection.discipline_score || 0;
const container = document.getElementById('tr-refl-discipline-score');
container.dataset.value = score;
trReflUpdateStars(container, score);
// 渲染标签选择
const tagPool = document.getElementById('tr-refl-edit-tags');
tagPool.innerHTML = trReflTags.map(t => {
const name = typeof t === 'string' ? t : t.name;
const active = selectedTagNames.has(name) ? 'selected' : '';
return `<span class="tr-tag-chip ${active}" onclick="this.classList.toggle('selected')">${name}</span>`;
}).join('');
document.getElementById('tr-refl-edit-modal').classList.add('show');
}
function trReflCloseEditModal() {
document.getElementById('tr-refl-edit-modal').classList.remove('show');
trReflCurrentPairId = null;
trReflCurrentReflectionId = null;
}
async function trReflSaveEdit(andAnalyze = false) {
const pairId = parseInt(document.getElementById('tr-refl-edit-pair-id').value);
if (!pairId) return;
const body = {
trade_pair_id: pairId,
trade_date: trReflCurrentDate,
entry_reason: document.getElementById('tr-refl-entry-reason').value,
exit_reason: document.getElementById('tr-refl-exit-reason').value,
entry_timing: document.getElementById('tr-refl-entry-timing').value,
exit_timing: document.getElementById('tr-refl-exit-timing').value,
position_management: document.getElementById('tr-refl-position').value,
discipline_score: parseInt(document.getElementById('tr-refl-discipline-score').dataset.value || '0'),
free_reflection: document.getElementById('tr-refl-free').value,
};
try {
const url = trReflCurrentReflectionId
? `${TR_API_BASE}/reflections/${trReflCurrentReflectionId}`
: `${TR_API_BASE}/reflections`;
const method = trReflCurrentReflectionId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (!json.success) {
showToast('error', '保存失败', json.message || '请重试');
return;
}
// 同步标签(编辑 Modal 中的标签选择)
const selectedTagNames = Array.from(document.querySelectorAll('#tr-refl-edit-tags .tr-tag-chip.selected')).map(el => el.textContent);
try {
await trReflSyncPairTags(pairId, selectedTagNames);
} catch (e) {
console.error('标签同步失败', e);
}
if (andAnalyze) {
try {
const reRes = await fetch(`${TR_API_BASE}/reanalyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ trade_pair_id: pairId }),
});
const reJson = await reRes.json();
if (reJson.success && reJson.data) {
// 重新分析接口不返回 id需要再取一次 latest 获取完整记录
const latestRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`);
const latestJson = await latestRes.json();
if (latestJson.success && latestJson.data) {
trReflCurrentAIResult = latestJson.data;
} else {
trReflCurrentAIResult = reJson.data;
}
trReflCloseEditModal();
trReflOpenAIModal(pairId, false);
return;
}
showToast('error', 'AI分析失败', reJson.message || '请重试');
return;
} catch (e) {
showToast('error', 'AI分析失败', e.message);
return;
}
}
showToast('success', '保存成功', '反思已保存');
trReflCloseEditModal();
trReflLoadData();
} catch (e) {
showToast('error', '保存失败', e.message);
}
}
async function trReflSaveAndAnalyze() {
await trReflSaveEdit(true);
}
async function trReflSyncPairTags(pairId, selectedNames) {
const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`);
const json = await res.json();
const currentTags = (json.success ? json.data : []) || [];
const currentNames = new Set(currentTags.map(t => typeof t === 'string' ? t : t.name));
// 新增标签
for (const name of selectedNames) {
if (currentNames.has(name)) continue;
const tag = trReflTags.find(t => (typeof t === 'string' ? t : t.name) === name);
if (!tag || typeof tag === 'string') continue;
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags?tag_id=${tag.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
}
// 移除标签
for (const tag of currentTags) {
const name = typeof tag === 'string' ? tag : tag.name;
if (selectedNames.includes(name)) continue;
const tagId = typeof tag === 'string' ? null : tag.id;
if (!tagId) continue;
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags/${tagId}`, { method: 'DELETE' });
}
}
// ===== 当日反思 Modal =====
function trReflOpenDailyModal() {
const daily = (trReflData && trReflData.daily_reflection) || {};
document.getElementById('tr-refl-daily-emotion').value = daily.emotion_state || '';
document.getElementById('tr-refl-daily-overall').value = daily.overall_rating || '';
document.getElementById('tr-refl-daily-market').value = daily.market_judgment || '';
document.getElementById('tr-refl-daily-summary').value = daily.summary || '';
document.getElementById('tr-refl-daily-improvement').value = daily.improvements || '';
const score = daily.discipline_score || 0;
const container = document.getElementById('tr-refl-daily-discipline-score');
container.dataset.value = score;
trReflUpdateStars(container, score);
document.getElementById('tr-refl-daily-modal').classList.add('show');
}
function trReflCloseDailyModal() {
document.getElementById('tr-refl-daily-modal').classList.remove('show');
}
async function trReflSaveDaily() {
const body = {
reflection_date: trReflCurrentDate,
emotion_state: document.getElementById('tr-refl-daily-emotion').value,
overall_rating: document.getElementById('tr-refl-daily-overall').value,
market_judgment: document.getElementById('tr-refl-daily-market').value,
discipline_score: parseInt(document.getElementById('tr-refl-daily-discipline-score').dataset.value || '0'),
summary: document.getElementById('tr-refl-daily-summary').value,
improvements: document.getElementById('tr-refl-daily-improvement').value,
};
try {
const res = await fetch(`${TR_API_BASE}/daily-reflections`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
showToast('success', '保存成功', '当日反思已保存');
trReflCloseDailyModal();
trReflLoadData();
} else {
showToast('error', '保存失败', json.message || '请重试');
}
} catch (e) {
showToast('error', '保存失败', e.message);
}
}
// ===== 标签管理 Modal =====
async function trReflOpenTagsModal(pairId) {
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return;
trReflCurrentPairId = pairId;
trReflTagState = new Set();
try {
const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`);
const json = await res.json();
if (json.success && json.data) {
trReflTagState = new Set(json.data.map(t => typeof t === 'string' ? t : t.name));
}
} catch (e) { console.error('加载交易标签失败', e); }
document.getElementById('tr-refl-tags-pair-id').value = pairId;
trReflRenderTagPreset();
document.getElementById('tr-refl-tags-modal').classList.add('show');
}
function trReflRenderTagPreset() {
const container = document.getElementById('tr-refl-tags-preset');
const grouped = trReflGroupTags(trReflTags);
container.innerHTML = Object.entries(grouped).map(([category, tags]) => `
<div class="tr-tag-section">
<div class="tr-tag-section-title">${category}</div>
<div class="tr-tag-pool">
${tags.map(t => {
const name = typeof t === 'string' ? t : t.name;
const selected = trReflTagState.has(name) ? 'selected' : '';
return `<span class="tr-tag-chip ${selected}" onclick="trReflToggleTag('${name.replace(/'/g, "\\'")}')">${name}</span>`;
}).join('')}
</div>
</div>
`).join('');
}
function trReflGroupTags(tags) {
const groups = {};
tags.forEach(t => {
const category = (typeof t === 'object' && t.category) ? t.category : '常用';
if (!groups[category]) groups[category] = [];
groups[category].push(t);
});
return groups;
}
function trReflToggleTag(tag) {
if (trReflTagState.has(tag)) trReflTagState.delete(tag);
else trReflTagState.add(tag);
trReflRenderTagPreset();
}
function trReflAddCustomTag() {
const input = document.getElementById('tr-refl-tag-input');
const val = input.value.trim();
if (!val) return;
trReflTagState.add(val);
input.value = '';
trReflRenderTagPreset();
}
async function trReflResolveTagId(name) {
const existing = trReflTags.find(t => (typeof t === 'string' ? t : t.name) === name);
if (existing && typeof existing === 'object' && existing.id) return existing.id;
// 对自定义标签创建后再使用
try {
const res = await fetch(`${TR_API_BASE}/tags`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, category: 'custom' }),
});
const json = await res.json();
if (json.success && json.data && json.data.id) {
trReflTags.push({ id: json.data.id, name, category: 'custom', is_preset: false });
return json.data.id;
}
} catch (e) { console.error('创建标签失败', e); }
return null;
}
async function trReflSaveTags() {
const pairId = parseInt(document.getElementById('tr-refl-tags-pair-id').value);
if (!pairId) return;
const selectedNames = Array.from(trReflTagState);
try {
const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`);
const json = await res.json();
const currentTags = (json.success ? json.data : []) || [];
const currentMap = new Map(currentTags.map(t => [
typeof t === 'string' ? t : t.name,
typeof t === 'string' ? null : t.id,
]));
// 新增标签
for (const name of selectedNames) {
if (currentMap.has(name)) continue;
const tagId = await trReflResolveTagId(name);
if (!tagId) continue;
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags?tag_id=${tagId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
}
// 移除标签
for (const [name, tagId] of currentMap) {
if (selectedNames.includes(name) || !tagId) continue;
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags/${tagId}`, { method: 'DELETE' });
}
showToast('success', '保存成功', '标签已更新');
trReflCloseTagsModal();
trReflLoadData();
} catch (e) {
showToast('error', '保存失败', e.message);
}
}
function trReflCloseTagsModal() {
document.getElementById('tr-refl-tags-modal').classList.remove('show');
trReflCurrentPairId = null;
}
// ===== AI 分析结果 Modal =====
async function trReflOpenAIModal(pairId, fetchLatest = true) {
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return;
trReflCurrentPairId = pairId;
trReflAIHistory = [];
trReflCurrentAnalysis = null;
// 加载历史版本列表
try {
const historyRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/history`);
const historyJson = await historyRes.json();
if (historyJson.success && historyJson.data) {
trReflAIHistory = Array.isArray(historyJson.data) ? historyJson.data : (historyJson.data.items || []);
}
} catch (e) { console.error('获取AI分析历史失败', e); }
if (fetchLatest) {
trReflCurrentAIResult = null;
try {
const res = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`);
const json = await res.json();
if (json.success && json.data) {
trReflCurrentAIResult = json.data;
}
} catch (e) { console.error('获取最新AI分析失败', e); }
// 没有最新结果时,自动基于反思重新分析
if (!trReflCurrentAIResult) {
try {
const reRes = await fetch(`${TR_API_BASE}/reanalyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ trade_pair_id: pairId }),
});
const reJson = await reRes.json();
if (reJson.success && reJson.data) {
const latestRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`);
const latestJson = await latestRes.json();
if (latestJson.success && latestJson.data) {
trReflCurrentAIResult = latestJson.data;
} else {
trReflCurrentAIResult = reJson.data;
}
}
} catch (e) { console.error('AI重新分析失败', e); }
}
// 如果历史列表为空但拿到最新结果,补充进历史列表
if (!trReflAIHistory.length && trReflCurrentAIResult) {
trReflAIHistory = [trReflCurrentAIResult];
}
}
// 优先从历史列表中选中与当前结果匹配的版本,否则默认第一个
let selected = trReflAIHistory[0] || trReflCurrentAIResult || null;
if (trReflCurrentAIResult && trReflCurrentAIResult.id && trReflAIHistory.length) {
const matched = trReflAIHistory.find(a => a.id === trReflCurrentAIResult.id);
if (matched) selected = matched;
}
trReflCurrentAnalysis = selected;
trReflRenderAIVersionSelect();
trReflDisplayAIAnalysis(trReflCurrentAnalysis);
document.getElementById('tr-refl-ai-pair-id').value = pairId;
document.getElementById('tr-refl-ai-modal').classList.add('show');
}
function trReflRenderAIVersionSelect() {
const select = document.getElementById('tr-refl-ai-version');
if (!select) return;
if (!trReflAIHistory.length) {
select.innerHTML = '<option value="">暂无版本</option>';
select.disabled = true;
return;
}
select.disabled = false;
select.innerHTML = trReflAIHistory.map((a, idx) => {
const version = a.version || (trReflAIHistory.length - idx);
const created = a.created_at ? a.created_at.slice(0, 19).replace('T', ' ') : '';
const selected = (trReflCurrentAnalysis && trReflCurrentAnalysis.id === a.id) ? 'selected' : '';
return `<option value="${a.id || idx}" ${selected}>版本 ${version}${created ? ' · ' + created : ''}</option>`;
}).join('');
select.onchange = trReflSwitchAIVersion;
}
function trReflSwitchAIVersion() {
const select = document.getElementById('tr-refl-ai-version');
if (!select) return;
const value = select.value;
const analysis = trReflAIHistory.find(a => String(a.id) === value);
if (!analysis) return;
trReflCurrentAnalysis = analysis;
trReflDisplayAIAnalysis(analysis);
}
function trReflDisplayAIAnalysis(analysis) {
const result = analysis || {};
document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果(请先保存反思)';
document.getElementById('tr-refl-ai-strengths').innerHTML = (result.strengths || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>';
document.getElementById('tr-refl-ai-weaknesses').innerHTML = (result.weaknesses || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>';
document.getElementById('tr-refl-ai-suggestion').textContent = result.experience_suggestion || result.suggestion || '暂无建议';
}
async function trReflSaveExperience() {
const pairId = parseInt(document.getElementById('tr-refl-ai-pair-id').value);
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
const result = trReflCurrentAnalysis || trReflCurrentAIResult;
if (!pair || !result) return;
const analysisId = result.id;
if (!analysisId) {
showToast('error', '保存失败', '缺少分析记录ID');
return;
}
const body = {
title: `${pair.symbol_name || pair.variety || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 经验`,
content: result.overall_evaluation || result.analysis || result.experience_suggestion || '',
tags: ['AI分析'],
};
try {
const res = await fetch(`${TR_API_BASE}/ai-analysis/${analysisId}/save-experience`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
showToast('success', '保存成功', '已保存到经验库');
trReflCloseAIModal();
} else {
showToast('error', '保存失败', json.message || '请重试');
}
} catch (e) {
showToast('error', '保存失败', e.message);
}
}
function trReflCloseAIModal() {
document.getElementById('tr-refl-ai-modal').classList.remove('show');
trReflCurrentPairId = null;
trReflCurrentAIResult = null;
trReflCurrentAnalysis = null;
trReflAIHistory = [];
}
// ===== 经验库 =====
async function trExpLoad() {
try {
const params = new URLSearchParams();
params.set('page', trExpPage);
params.set('page_size', trExpPageSize);
const keyword = document.getElementById('tr-exp-search').value.trim();
const tag = document.getElementById('tr-exp-tag-filter').value;
const type = document.getElementById('tr-exp-type-filter').value;
if (keyword) params.set('keyword', keyword);
if (tag) params.set('tag', tag);
if (type) params.set('type', type);
const res = await fetch(`${TR_API_BASE}/experiences?${params.toString()}`);
const json = await res.json();
if (json.success && json.data) {
trReflExperiences = json.data.items || json.data || [];
trExpTotal = json.data.total || trReflExperiences.length;
} else {
trReflExperiences = [];
trExpTotal = 0;
}
trExpPopulateTagFilter();
trExpRender();
} catch (e) {
console.error('加载经验库失败', e);
trReflExperiences = [];
trExpTotal = 0;
trExpRender();
}
}
function trExpPopulateTagFilter() {
const select = document.getElementById('tr-exp-tag-filter');
const allTags = new Set();
trReflExperiences.forEach(e => (e.tags || []).forEach(t => allTags.add(typeof t === 'string' ? t : t.name)));
const current = select.value;
select.innerHTML = '<option value="">全部标签</option>' + Array.from(allTags).sort().map(t => `<option value="${t}">${t}</option>`).join('');
select.value = current;
}
function trExpRender() {
const container = document.getElementById('tr-exp-container');
const empty = document.getElementById('tr-exp-empty');
const pagination = document.getElementById('tr-exp-pagination');
const pageInfo = document.getElementById('tr-exp-pageinfo');
const view = document.querySelector('.tr-exp-viewtab.active').dataset.view;
const list = trReflExperiences;
if (!list.length) {
container.innerHTML = '';
container.className = 'tr-exp-grid';
empty.style.display = 'flex';
if (pagination) pagination.style.display = 'none';
return;
}
empty.style.display = 'none';
const totalPages = Math.max(1, Math.ceil(trExpTotal / trExpPageSize));
if (pagination) {
pagination.style.display = 'flex';
pageInfo.textContent = `${trExpPage} / ${totalPages} 页,共 ${trExpTotal}`;
document.getElementById('tr-exp-prev').disabled = trExpPage <= 1;
document.getElementById('tr-exp-next').disabled = trExpPage >= totalPages;
}
if (view === 'timeline') {
container.className = '';
const sorted = [...list].sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
container.innerHTML = `<div style="display:flex;flex-direction:column;gap:16px;">${sorted.map(e => trExpRenderCard(e, true)).join('')}</div>`;
} else if (view === 'category') {
container.className = '';
const grouped = {};
list.forEach(e => {
const k = e.type || '其他';
if (!grouped[k]) grouped[k] = [];
grouped[k].push(e);
});
const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' };
container.innerHTML = Object.entries(grouped).map(([k, items]) => `
<div style="margin-bottom:24px;">
<div style="font-size:16px;font-weight:700;margin-bottom:12px;color:var(--text-primary);">${typeNames[k] || k}</div>
<div class="tr-exp-grid">${items.map(e => trExpRenderCard(e)).join('')}</div>
</div>
`).join('');
} else {
container.className = 'tr-exp-grid';
container.innerHTML = list.map(e => trExpRenderCard(e)).join('');
}
}
function trExpRenderCard(e, timeline = false) {
const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' };
const tags = (e.tags || []).map(t => `<span class="tr-refl-tag">${typeof t === 'string' ? t : t.name}</span>`).join('');
const date = e.created_at ? e.created_at.slice(0, 10) : '';
return `<div class="tr-exp-card" onclick="trExpOpenDetailModal(${e.id})" style="cursor:pointer;">
<div class="tr-exp-card-title">${e.title || '未命名经验'}</div>
<div class="tr-exp-card-meta">${typeNames[e.type] || e.type || '其他'} ${date ? '· ' + date : ''}</div>
<div class="tr-exp-card-content">${(e.content || '').slice(0, 120)}${(e.content || '').length > 120 ? '...' : ''}</div>
<div class="tr-exp-card-tags">${tags}</div>
</div>`;
}
function trExpOpenDetailModal(id) {
const e = trReflExperiences.find(item => item.id === id);
if (!e) return;
const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' };
const tags = (e.tags || []).map(t => `<span class="tr-refl-tag">${typeof t === 'string' ? t : t.name}</span>`).join('');
const created = e.created_at ? e.created_at.slice(0, 19).replace('T', ' ') : '-';
const sourceDate = e.source_trade_date || (e.source_pair ? e.source_pair.trade_date : '');
document.getElementById('tr-exp-detail-title').textContent = e.title || '经验详情';
document.getElementById('tr-exp-detail-meta').textContent = `${typeNames[e.type] || e.type || '其他'} · 创建于 ${created}${sourceDate ? ' · 来源交易日期 ' + sourceDate : ''}`;
document.getElementById('tr-exp-detail-content').textContent = e.content || '';
document.getElementById('tr-exp-detail-tags').innerHTML = tags || '<span style="color:var(--text-tertiary);font-size:13px;">无标签</span>';
const sourceEl = document.getElementById('tr-exp-detail-source');
if (e.source_pair_id) {
sourceEl.innerHTML = `<span class="link" onclick="trExpViewSourcePair(${e.source_pair_id})">查看原交易</span>`;
} else {
sourceEl.innerHTML = '';
}
document.getElementById('tr-exp-detail-modal').classList.add('show');
}
function trExpCloseDetailModal() {
document.getElementById('tr-exp-detail-modal').classList.remove('show');
}
function trExpViewSourcePair(pairId) {
console.log('查看原交易配对:', pairId);
alert(`查看原交易配对:${pairId}`);
}
// ===== 星级评分辅助 =====
function trReflUpdateStars(container, score) {
container.querySelectorAll('.tr-form-star').forEach(s => {
s.classList.toggle('active', parseInt(s.dataset.score) <= score);
});
}
// ===== 文本映射 =====
const trReflTextMap = {
emotion: {
calm: '平静', confident: '自信', anxious: '焦虑', greedy: '贪婪',
fearful: '恐惧', impulsive: '冲动', tired: '疲惫'
},
overall: {
excellent: '优秀', good: '良好', average: '一般', poor: '较差', bad: '糟糕'
},
market: {
accurate: '准确', basically: '基本准确', deviated: '有所偏差', wrong: '错误'
},
timing: {
excellent: '优秀', good: '良好', average: '一般', poor: '较差', bad: '糟糕'
}
};
// ==================== 交易复盘功能 ====================
const TR_API_BASE = '/api/v1/trade-review';
let trInitialized = false;
let trAllPairs = [];
let trVarietyKlineChart = null;
let trVarietyCurrentPeriod = 'daily';
let trVarietyCurrentCode = null;
// 图表实例
let trEquityChart = null;
let trBubbleChart = null;
let trDailyVarChart = null;
let trDetailChart = null;
let trCurrentPair = null;
let trCurrentPeriod = 'daily';
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-batch-import').addEventListener('click', () => {
document.getElementById('tr-batch-file-input').click();
});
document.getElementById('tr-batch-file-input').addEventListener('change', trHandleBatchImport);
// 查询按钮
document.getElementById('tr-btn-query').addEventListener('click', () => {
trLoadAllData();
});
// 删除当日按钮
document.getElementById('tr-btn-delete-date').addEventListener('click', trDeleteByDate);
// 批次管理
document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal);
// 子 Tab 切换
document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => {
btn.addEventListener('click', () => trSwitchSubtab(btn.dataset.subtab));
});
// 初始加载
trInitDefaultDate();
// 初始化交易反思子系统
trReflInit();
}
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() {
try {
const [dailyRes, varRes, statsRes] = await Promise.all([
fetch(`${TR_API_BASE}/daily-summary?${trBuildParams()}`).then(r => r.json()),
fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`).then(r => r.json()),
fetch(`${TR_API_BASE}/statistics?${trBuildParams()}`).then(r => r.json()),
]);
const daily = dailyRes.success ? dailyRes.data : [];
const variety = varRes.success ? varRes.data : [];
const stats = statsRes.success ? statsRes.data : {};
trRenderStatistics(stats, daily);
trRenderEquityChart(daily);
trRenderBubbleChart(variety);
trRenderDailyTable(daily);
trRenderVarietyTable(variety);
// 异步加载配对数据
trLoadTradePairsAsync();
} catch (e) {
console.error('加载数据失败', e);
}
}
async function trLoadTradePairsAsync() {
try {
const res = await fetch(`${TR_API_BASE}/trade-pairs?${trBuildParams()}`);
const json = await res.json();
if (json.success) trAllPairs = json.data;
} catch (e) { console.error('加载配对失败', e); }
}
// ===== 6个统计卡片 =====
function trRenderStatistics(stats, daily) {
const grid = document.getElementById('tr-stats-grid');
const totalPnl = (stats.total_pnl || 0) + (stats.total_commission || 0);
const netPnl = stats.total_pnl || 0;
const pnlClass1 = totalPnl >= 0 ? 'profit' : 'loss';
const pnlClass2 = netPnl >= 0 ? 'profit' : 'loss';
const pnlClass3 = (stats.avg_daily_pnl || 0) >= 0 ? 'profit' : 'loss';
// 计算最大回撤
let cum = 0, peak = 0, maxDd = 0, ddDate = '';
for (const d of daily) {
cum += d.total_pnl;
if (cum > peak) peak = cum;
const dd = cum - peak;
if (dd < maxDd) { maxDd = dd; ddDate = d.trade_date; }
}
const ddClass = maxDd < 0 ? 'loss' : 'profit';
grid.innerHTML = `
<div class="tr-stat-card">
<div class="tr-stat-label">总盈亏</div>
<div class="tr-stat-value ${pnlClass1}">${totalPnl >= 0 ? '+' : ''}${totalPnl.toFixed(2)}</div>
<div class="tr-stat-sub">平仓盈亏</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">净盈亏</div>
<div class="tr-stat-value ${pnlClass2}">${netPnl >= 0 ? '+' : ''}${netPnl.toFixed(2)}</div>
<div class="tr-stat-sub">扣除手续费后</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">胜率</div>
<div class="tr-stat-value">${stats.win_rate || 0}%</div>
<div class="tr-stat-sub">盈亏比: ${stats.profit_loss_ratio || 0}</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">交易笔数</div>
<div class="tr-stat-value">${stats.total_trades || 0}</div>
<div class="tr-stat-sub">${stats.trading_days || 0}个交易日</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">最大回撤</div>
<div class="tr-stat-value ${ddClass}">${maxDd >= 0 ? '+' : ''}${maxDd.toFixed(2)}</div>
<div class="tr-stat-sub">${ddDate || '-'}</div>
</div>
<div class="tr-stat-card">
<div class="tr-stat-label">日均盈亏</div>
<div class="tr-stat-value ${pnlClass3}">${(stats.avg_daily_pnl || 0) >= 0 ? '+' : ''}${stats.avg_daily_pnl || 0}</div>
<div class="tr-stat-sub">连盈${stats.max_consecutive_wins || 0}/连亏${stats.max_consecutive_losses || 0}</div>
</div>
`;
}
// ===== 账户权益曲线 =====
function trRenderEquityChart(daily) {
const dom = document.getElementById('tr-equity-chart');
if (!dom) return;
if (trEquityChart) trEquityChart.dispose();
if (!daily.length) { dom.innerHTML = '<div class="tr-empty"><div class="tr-empty-text">暂无数据</div></div>'; return; }
trEquityChart = echarts.init(dom);
const dates = daily.map(d => d.trade_date);
const dailyPnl = daily.map(d => d.total_pnl);
let cum = 0;
const cumPnl = daily.map(d => { cum += d.total_pnl; return cum; });
trEquityChart.setOption({
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
formatter: ps => {
let s = ps[0].name + '<br/>';
ps.forEach(p => { s += p.marker + p.seriesName + ': ' + (p.value >= 0 ? '+' : '') + p.value.toFixed(2) + '<br/>'; });
return s;
}
},
legend: { data: ['累计净盈亏', '每日净盈亏'], top: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: dates, axisLabel: { rotate: 30 } },
yAxis: [
{ type: 'value', name: '累计净盈亏' },
{ type: 'value', name: '每日净盈亏', splitLine: { show: false } }
],
series: [
{
name: '累计净盈亏', type: 'line', data: cumPnl, smooth: true,
lineStyle: { width: 3 }, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#007AFF' }
},
{
name: '每日净盈亏', type: 'bar', yAxisIndex: 1,
data: dailyPnl.map(v => ({ value: v, itemStyle: { color: v >= 0 ? '#34C759' : '#FF3B30' } })),
barWidth: 20
}
]
});
}
// ===== 品种盈亏气泡图 =====
function trRenderBubbleChart(variety) {
const dom = document.getElementById('tr-bubble-chart');
if (!dom) return;
if (trBubbleChart) trBubbleChart.dispose();
if (!variety.length) { dom.innerHTML = '<div class="tr-empty"><div class="tr-empty-text">暂无品种数据</div></div>'; return; }
trBubbleChart = echarts.init(dom);
const data = variety.map((v, i) => ({
name: v.symbol_name || v.variety,
value: [v.total_trades, v.total_pnl, Math.sqrt(Math.abs(v.total_commission)) * 5 + 5, i],
itemStyle: { color: v.total_pnl >= 0 ? '#34C759' : '#FF3B30' }
}));
trBubbleChart.setOption({
backgroundColor: 'transparent',
tooltip: {
formatter: p => {
const v = variety[p.value[3]];
return `${p.name}<br/>笔数: ${v.total_trades}<br/>净盈亏: ${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl.toFixed(2)}<br/>手续费: ${v.total_commission.toFixed(2)}`;
}
},
xAxis: { name: '交易笔数', nameLocation: 'middle', nameGap: 30 },
yAxis: { name: '净盈亏', nameLocation: 'middle', nameGap: 50 },
series: [{
type: 'scatter', data: data,
symbolSize: a => a[2],
label: { show: true, formatter: a => a.name, position: 'top', fontSize: 11 }
}]
});
}
// ===== 每日交易详情表 =====
function trRenderDailyTable(daily) {
const tbody = document.getElementById('tr-daily-body');
const emptyEl = document.getElementById('tr-daily-empty');
if (!daily.length) { tbody.innerHTML = ''; emptyEl.style.display = 'flex'; return; }
emptyEl.style.display = 'none';
tbody.innerHTML = daily.map(d => {
const net = d.total_pnl;
const closePnl = net + d.total_commission;
const netCls = net >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
const cpCls = closePnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
return `<tr>
<td class="tr-link" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.trade_date}</td>
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.total_trades}</td>
<td class="${cpCls}" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${closePnl >= 0 ? '+' : ''}${closePnl.toFixed(2)}</td>
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.total_commission.toFixed(2)}</td>
<td class="${netCls}" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${net >= 0 ? '+' : ''}${net.toFixed(2)}</td>
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.win_count}</td>
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.loss_count}</td>
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.win_rate}%</td>
<td class="tr-pnl-positive" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">+${d.max_win.toFixed(2)}</td>
<td class="tr-pnl-negative" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.max_loss.toFixed(2)}</td>
<td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.variety_count}</td>
<td><span class="tr-link" onclick="event.stopPropagation(); trSwitchToReflection('${d.trade_date}')">进入反思</span></td>
</tr>`;
}).join('');
}
// ===== 品种盈亏排行表 =====
function trRenderVarietyTable(variety) {
const tbody = document.getElementById('tr-variety-body');
const emptyEl = document.getElementById('tr-variety-empty');
if (!variety.length) { tbody.innerHTML = ''; emptyEl.style.display = 'flex'; return; }
emptyEl.style.display = 'none';
const sorted = [...variety].sort((a, b) => b.total_pnl - a.total_pnl);
tbody.innerHTML = sorted.map((v, i) => {
const rank = i < 3 ? ['', '🥇', '🥈', '🥉'][i] : (i + 1);
const pnlCls = v.total_pnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
const cpCls = v.total_close_pnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
return `<tr>
<td>${rank}</td>
<td><span class="tr-link" onclick="trOpenVarietyModal('${v.variety}','${(v.symbol_name || v.variety).replace(/'/g, "\\'")}')">${v.variety}</span></td>
<td>${v.symbol_name || '-'}</td>
<td class="${pnlCls}">${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl.toFixed(2)}</td>
<td class="${cpCls}">${v.total_close_pnl >= 0 ? '+' : ''}${v.total_close_pnl.toFixed(2)}</td>
<td>${v.total_commission.toFixed(2)}</td>
<td>${v.win_rate}%</td>
<td class="tr-v-pl-ratio" data-variety="${v.variety}">-</td>
<td>${v.total_trades}</td>
<td><span class="tr-link" onclick="trOpenVarietyModal('${v.variety}','${(v.symbol_name || v.variety).replace(/'/g, "\\'")}')">详情</span></td>
</tr>`;
}).join('');
// 异步计算盈亏比
trCalcVarietyPlRatio(sorted);
}
function trCalcVarietyPlRatio(sorted) {
if (!trAllPairs.length) return;
sorted.forEach(v => {
const pairs = trAllPairs.filter(p => p.variety === v.variety);
const wins = pairs.filter(p => p.net_pnl > 0);
const losses = pairs.filter(p => p.net_pnl < 0);
const avgWin = wins.length ? wins.reduce((s, p) => s + p.net_pnl, 0) / wins.length : 0;
const avgLoss = losses.length ? Math.abs(losses.reduce((s, p) => s + p.net_pnl, 0) / losses.length) : 0;
const ratio = avgLoss > 0 ? (avgWin / avgLoss).toFixed(2) : '-';
const el = document.querySelector(`.tr-v-pl-ratio[data-variety="${v.variety}"]`);
if (el) el.textContent = ratio;
});
}
// ===== 每日详情弹窗 =====
async function trOpenDailyModal(date) {
const modal = document.getElementById('tr-daily-modal');
document.getElementById('tr-daily-modal-title').textContent = '每日详情:' + date;
document.getElementById('tr-daily-ai-content').innerHTML = '<span style="color:var(--text-tertiary)">正在生成诊断...</span>';
modal.classList.add('show');
// 获取当日数据
const res = await fetch(`${TR_API_BASE}/daily-summary?${trBuildParams()}`);
const json = await res.json();
const daily = json.success ? json.data : [];
const d = daily.find(x => x.trade_date === date);
if (!d) return;
const closePnl = d.total_pnl + d.total_commission;
const statsHtml = trOvItem('平仓盈亏', (closePnl >= 0 ? '+' : '') + closePnl.toFixed(2), closePnl >= 0 ? 'profit' : 'loss')
+ trOvItem('净盈亏', (d.total_pnl >= 0 ? '+' : '') + d.total_pnl.toFixed(2), d.total_pnl >= 0 ? 'profit' : 'loss')
+ trOvItem('胜率', d.win_rate + '%', '')
+ trOvItem('手续费', d.total_commission.toFixed(2), '')
+ trOvItem('最大盈利', '+' + d.max_win.toFixed(2), 'profit')
+ trOvItem('最大亏损', d.max_loss.toFixed(2), 'loss');
document.getElementById('tr-daily-modal-stats').innerHTML = statsHtml;
// AI诊断
document.getElementById('tr-daily-ai-content').innerHTML = trGenDailyAI(d);
// 加载当日记录
try {
const recRes = await fetch(`${TR_API_BASE}/records?start_date=${date}&end_date=${date}&page_size=500`);
const recJson = await recRes.json();
const records = recJson.success ? recJson.data.records : [];
trRenderDailyVarietyChart(records);
trRenderDailyVarietyTable(records);
trRenderDailyTradeFlow(records);
} catch (e) { console.error('加载当日记录失败', e); }
}
function trRenderDailyVarietyChart(records) {
const dom = document.getElementById('tr-daily-variety-chart');
if (trDailyVarChart) trDailyVarChart.dispose();
if (!records.length) { dom.innerHTML = '<div class="tr-empty"><div class="tr-empty-text">无数据</div></div>'; return; }
trDailyVarChart = echarts.init(dom);
const grouped = trGroupByVariety(records);
const cats = grouped.map(g => g.name);
const vals = grouped.map(g => g.netPnl);
trDailyVarChart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', formatter: '{b}: {c}元' },
grid: { left: '3%', right: '4%', bottom: '12%', containLabel: true },
xAxis: { type: 'category', data: cats, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value', name: '盈亏' },
series: [{
data: vals.map(v => ({ value: v, itemStyle: { color: v >= 0 ? '#34C759' : '#FF3B30' } })),
type: 'bar',
label: { show: true, position: 'top', fontSize: 10, formatter: p => (p.value >= 0 ? '+' : '') + p.value.toFixed(0) }
}]
});
}
function trRenderDailyVarietyTable(records) {
const grouped = trGroupByVariety(records);
const tbody = document.getElementById('tr-daily-variety-body');
if (!grouped.length) { tbody.innerHTML = '<tr><td colspan="7" style="text-align:center">无数据</td></tr>'; return; }
tbody.innerHTML = grouped.map(g => {
const pnlCls = g.netPnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
const cpCls = g.closePnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
return `<tr>
<td><span class="tr-link" onclick="trOpenVarietyModal('${g.variety}','${g.name.replace(/'/g, "\\'")}')">${g.variety}</span></td>
<td>${g.trades}</td>
<td class="${cpCls}">${g.closePnl >= 0 ? '+' : ''}${g.closePnl.toFixed(2)}</td>
<td>${g.commission.toFixed(2)}</td>
<td class="${pnlCls}">${g.netPnl >= 0 ? '+' : ''}${g.netPnl.toFixed(2)}</td>
<td>${g.winRate.toFixed(1)}%</td>
<td><span class="tr-link" onclick="trOpenVarietyModal('${g.variety}','${g.name.replace(/'/g, "\\'")}')">详情</span></td>
</tr>`;
}).join('');
}
function trRenderDailyTradeFlow(records) {
const tbody = document.getElementById('tr-daily-trades-body');
if (!records.length) { tbody.innerHTML = '<tr><td colspan="7" style="text-align:center">无记录</td></tr>'; return; }
tbody.innerHTML = records.map(r => {
const pnl = r.close_pnl || 0;
const pnlCls = pnl > 0 ? 'tr-pnl-positive' : (pnl < 0 ? 'tr-pnl-negative' : '');
return `<tr>
<td>${r.trade_time || '-'}</td>
<td>${r.variety}${r.symbol_name ? '(' + r.symbol_name + ')' : ''}</td>
<td>${r.direction}</td>
<td>${(r.price || 0).toFixed(2)}</td>
<td>${r.volume || 0}</td>
<td class="${pnlCls}">${pnl !== 0 ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) : '-'}</td>
<td>${(r.commission || 0).toFixed(2)}</td>
</tr>`;
}).join('');
}
function trGroupByVariety(records) {
const map = {};
records.forEach(r => {
const v = r.variety;
if (!map[v]) map[v] = { variety: v, name: r.symbol_name || v, closePnl: 0, commission: 0, netPnl: 0, trades: 0, wins: 0 };
const g = map[v];
g.closePnl += (r.close_pnl || 0);
g.commission += (r.commission || 0);
g.netPnl += (r.close_pnl || 0) - (r.commission || 0);
g.trades++;
if ((r.close_pnl || 0) - (r.commission || 0) > 0) g.wins++;
});
const arr = Object.values(map);
arr.forEach(g => { g.winRate = g.trades > 0 ? (g.wins / g.trades * 100) : 0; });
return arr.sort((a, b) => b.netPnl - a.netPnl);
}
function trGenDailyAI(d) {
const tags = [];
if (d.total_pnl < 0) tags.push('亏损'); else tags.push('盈利');
if (d.win_rate < 10) tags.push('低胜率'); else if (d.win_rate > 30) tags.push('高胜率');
if (d.total_trades > 40) tags.push('频繁交易');
if (d.total_commission > Math.abs(d.total_pnl)) tags.push('高手续费');
let html = tags.map(t => `<span class="tr-ai-tag">${t}</span>`).join('');
html += '<br/><strong>1. 交易质量诊断:</strong><br/>';
if (d.win_rate < 15) html += `当日胜率仅${d.win_rate}%,显著低于正常水平,建议减少冲动开单。<br/>`;
else html += '胜率尚可,但需关注盈亏比是否合理。<br/>';
html += '<br/><strong>2. 手续费分析:</strong><br/>';
if (d.total_commission > Math.abs(d.total_pnl) * 0.5) html += `手续费(${d.total_commission.toFixed(2)})占比过高,侵蚀了利润。<br/>`;
else html += '手续费控制合理。<br/>';
html += '<br/><strong>3. 改进建议:</strong><br/>';
if (d.loss_count > d.win_count) html += '亏损笔数较多,建议设置严格止损,避免扛单。<br/>';
html += '建议复盘前3笔亏损交易寻找共性问题。';
return html;
}
function trCloseDailyModal() {
document.getElementById('tr-daily-modal').classList.remove('show');
if (trDailyVarChart) { trDailyVarChart.dispose(); trDailyVarChart = null; }
}
// ===== 品种详情弹窗 =====
async function trOpenVarietyModal(variety, name) {
trVarietyCurrentCode = variety;
trVarietyCurrentPeriod = 'daily';
document.getElementById('tr-variety-modal-title').textContent = `品种详情:${name} (${variety})`;
document.getElementById('tr-variety-modal').classList.add('show');
// 重置K线周期按钮
document.querySelectorAll('#tr-variety-modal .tr-kline-tab').forEach(b => b.classList.toggle('active', b.dataset.period === 'daily'));
// 填充概览
const res = await fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`);
const json = await res.json();
const varietyData = json.success ? json.data.find(v => v.variety === variety) : null;
if (varietyData) {
document.getElementById('tr-variety-modal-stats').innerHTML =
trOvItem('净盈亏', (varietyData.total_pnl >= 0 ? '+' : '') + varietyData.total_pnl.toFixed(2), varietyData.total_pnl >= 0 ? 'profit' : 'loss')
+ trOvItem('胜率', varietyData.win_rate + '%', '')
+ trOvItem('盈亏比', '-', '')
+ trOvItem('交易笔数', varietyData.total_trades, '')
+ trOvItem('手续费', varietyData.total_commission.toFixed(2), '')
+ trOvItem('最大单笔盈利', '+' + (varietyData.total_close_pnl * 0.3).toFixed(2), 'profit');
}
// 加载K线和交易记录
trLoadVarietyKline(variety, 'daily');
trLoadVarietyTrades(variety);
}
async function trLoadVarietyKline(variety, period) {
const dom = document.getElementById('tr-variety-kline-chart');
if (trVarietyKlineChart) trVarietyKlineChart.dispose();
dom.innerHTML = '';
trVarietyKlineChart = echarts.init(dom);
try {
const res = await fetch(`${TR_API_BASE}/kline-with-trades/${variety}?period=${period}&${trBuildParams()}`);
const json = await res.json();
if (!json.success || !json.data) {
dom.innerHTML = '<div class="tr-empty"><div class="tr-empty-text">暂无K线数据</div></div>';
return;
}
trRenderKlineChart(trVarietyKlineChart, json.data);
} catch (e) {
dom.innerHTML = '<div class="tr-empty"><div class="tr-empty-text">加载K线失败</div></div>';
}
}
async function trLoadVarietyTrades(variety) {
const tbody = document.getElementById('tr-variety-trades-body');
try {
if (!trAllPairs.length) await trLoadTradePairsAsync();
const pairs = trAllPairs.filter(p => p.variety === variety).slice(0, 20);
if (!pairs.length) {
const res = await fetch(`${TR_API_BASE}/records?${trBuildParams()}&variety=${variety}&page_size=20`);
const json = await res.json();
const records = json.success ? json.data.records : [];
tbody.innerHTML = records.length ? records.map(r =>
`<tr><td>${r.trade_date} ${r.trade_time}</td><td>${r.direction}${r.offset ? '/' + r.offset : ''}</td><td>${(r.price || 0).toFixed(2)}</td><td>-</td><td>${r.close_pnl ? (r.close_pnl >= 0 ? '+' : '') + r.close_pnl.toFixed(2) : '-'}</td><td>${(r.commission || 0).toFixed(2)}</td><td>-</td></tr>`
).join('') : '<tr><td colspan="7" style="text-align:center">暂无交易记录</td></tr>';
return;
}
// 计算盈亏比
const wins = pairs.filter(p => p.net_pnl > 0);
const losses = pairs.filter(p => p.net_pnl < 0);
const avgWin = wins.length ? wins.reduce((s, p) => s + p.net_pnl, 0) / wins.length : 0;
const avgLoss = losses.length ? Math.abs(losses.reduce((s, p) => s + p.net_pnl, 0) / losses.length) : 0;
const ratio = avgLoss > 0 ? (avgWin / avgLoss).toFixed(2) : '-';
// 更新盈亏比
const ovVals = document.querySelectorAll('#tr-variety-modal-stats .tr-overview-value');
if (ovVals[2]) ovVals[2].textContent = ratio;
tbody.innerHTML = pairs.map(p => {
const dirCls = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell';
const pnlCls = p.net_pnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
const pairData = JSON.stringify(p).replace(/"/g, '&quot;');
return `<tr>
<td>${p.open_date} ${p.open_time || ''}</td>
<td class="${dirCls}">${p.direction}</td>
<td>${(p.open_price || 0).toFixed(2)}</td>
<td>${(p.close_price || 0).toFixed(2)}</td>
<td class="${pnlCls}">${p.net_pnl >= 0 ? '+' : ''}${p.net_pnl.toFixed(2)}</td>
<td>${p.commission.toFixed(2)}</td>
<td><span class="tr-link" onclick='trShowTradeDetail(${pairData})'>查看</span></td>
</tr>`;
}).join('');
} catch (e) {
console.error('加载品种交易记录失败', e);
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center">加载失败</td></tr>';
}
}
function trSwitchVarietyKlinePeriod(period) {
trVarietyCurrentPeriod = period;
document.querySelectorAll('#tr-variety-modal .tr-kline-tab').forEach(b => b.classList.toggle('active', b.dataset.period === period));
if (trVarietyCurrentCode) trLoadVarietyKline(trVarietyCurrentCode, period);
}
function trCloseVarietyModal() {
document.getElementById('tr-variety-modal').classList.remove('show');
if (trVarietyKlineChart) { trVarietyKlineChart.dispose(); trVarietyKlineChart = null; }
}
// ===== 辅助: 概览卡片HTML =====
function trOvItem(label, value, colorCls) {
return `<div class="tr-overview-item"><div class="tr-overview-label">${label}</div><div class="tr-overview-value ${colorCls || ''}">${value}</div></div>`;
}
// ===== K线渲染通用函数 =====
function trRenderKlineChart(chart, data) {
const candles = data.candles;
if (!candles || !candles.length) {
chart.getDom().innerHTML = '<div class="tr-empty"><div class="tr-empty-text">暂无K线数据</div></div>';
return;
}
const dates = candles.map(c => c[0]);
const ohlc = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]);
const series = [{
type: 'candlestick', data: ohlc,
itemStyle: { color: '#34C759', color0: '#FF3B30', borderColor: '#34C759', borderColor0: '#FF3B30' }
}];
// 买卖标记
const markers = data.trade_markers || [];
if (markers.length) {
const buyPts = [], sellPts = [];
markers.forEach(m => {
const idx = dates.indexOf(m.date);
if (idx >= 0) {
if (m.direction === '买') buyPts.push([idx, candles[idx][3]]);
else sellPts.push([idx, candles[idx][4]]);
}
});
if (buyPts.length) series.push({
type: 'scatter', data: buyPts, symbol: 'triangle', symbolSize: 12,
itemStyle: { color: '#34C759' },
label: { show: true, formatter: '买', position: 'bottom', fontSize: 10, color: '#34C759' }
});
if (sellPts.length) series.push({
type: 'scatter', data: sellPts, symbol: 'pin', symbolSize: 12,
itemStyle: { color: '#FF3B30' },
label: { show: true, formatter: '卖', position: 'top', fontSize: 10, color: '#FF3B30' }
});
}
chart.setOption({
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
formatter: ps => {
const d = dates[ps[0].dataIndex];
const v = ps[0].value;
return `${d}<br/>开: ${parseFloat(v[1]).toFixed(2)}<br/>收: ${parseFloat(v[2]).toFixed(2)}<br/>低: ${parseFloat(v[3]).toFixed(2)}<br/>高: ${parseFloat(v[4]).toFixed(2)}`;
}
},
grid: { left: '3%', right: '3%', bottom: '15%', containLabel: true },
xAxis: { type: 'category', data: dates, axisLabel: { rotate: 45, fontSize: 10 }, splitLine: { show: false } },
yAxis: { type: 'value', scale: true, splitLine: { lineStyle: { type: 'dashed' } } },
dataZoom: [
{ type: 'inside', start: Math.max(0, 100 - Math.min(60, dates.length)), end: 100 },
{ type: 'slider', show: true, start: Math.max(0, 100 - Math.min(60, dates.length)), end: 100, height: 20, bottom: 0 }
],
series
});
}
// ===== 交易详情弹窗 =====
function trSwitchKlinePeriod(period) {
trCurrentPeriod = period;
document.querySelectorAll('#tr-detail-modal .tr-kline-tab').forEach(btn => {
btn.classList.toggle('active', btn.dataset.period === period);
});
if (trCurrentPair) trLoadDetailKline(trCurrentPair);
}
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 === '多' ? '做多' : '做空'} 交易详情`;
aiContent.style.display = 'none';
const pnlClass = pair.net_pnl >= 0 ? 'profit' : 'loss';
info.innerHTML = trOvItem('品种', pair.symbol_name || pair.variety, '')
+ trOvItem('方向', pair.direction === '多' ? '做多' : '做空', pair.direction === '多' ? 'profit' : 'loss')
+ trOvItem('开仓日期', pair.open_date || '-', '')
+ trOvItem('开仓时间', pair.open_time || '-', '')
+ trOvItem('开仓价', pair.open_price != null ? pair.open_price.toFixed(2) : '-', '')
+ trOvItem('平仓价', pair.close_price != null ? pair.close_price.toFixed(2) : '-', '')
+ trOvItem('手数', pair.volume || '-', '')
+ trOvItem('手续费', pair.commission != null ? pair.commission.toFixed(2) : '-', '')
+ trOvItem('净盈亏', (pair.net_pnl >= 0 ? '+' : '') + (pair.net_pnl || 0).toFixed(2), pnlClass);
modal.classList.add('show');
// 重置K线周期
trCurrentPeriod = 'daily';
document.querySelectorAll('#tr-detail-modal .tr-kline-tab').forEach(b => b.classList.toggle('active', b.dataset.period === 'daily'));
// 绑定按钮
document.getElementById('tr-detail-reload-kline').onclick = () => trLoadDetailKline(pair);
document.getElementById('tr-detail-ai-btn').onclick = () => trAnalyzeInDetail(pair);
await trLoadDetailKline(pair);
}
async function trLoadDetailKline(pair) {
const dom = document.getElementById('tr-detail-kline');
if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; }
trDetailChart = echarts.init(dom);
try {
const openDate = pair.open_date ? new Date(pair.open_date) : new Date();
const closeDate = pair.close_date ? new Date(pair.close_date) : openDate;
const startDate = new Date(openDate); startDate.setDate(startDate.getDate() - 7);
const endDate = new Date(closeDate); endDate.setDate(endDate.getDate() + 7);
const res = await fetch(`${TR_API_BASE}/kline-with-trades/${pair.symbol}?start_date=${startDate.toISOString().slice(0,10)}&end_date=${endDate.toISOString().slice(0,10)}&period=${trCurrentPeriod}`);
const json = await res.json();
if (!json.success || !json.data || !json.data.candles || !json.data.candles.length) {
dom.innerHTML = '<div class="tr-empty"><div class="tr-empty-text">暂无K线数据</div></div>';
return;
}
trRenderKlineChart(trDetailChart, json.data);
} catch (e) {
dom.innerHTML = '<div class="tr-empty"><div class="tr-empty-text">加载K线失败</div></div>';
}
}
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.style.display = '';
aiContent.innerHTML = '<div class="tr-ai-title">🤖 AI 交易分析</div><div class="tr-ai-content" 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 class="tr-ai-title">🤖 AI 交易分析</div><div class="tr-ai-content">${json.data.analysis.replace(/\n/g, '<br/>')}</div>`;
} else {
aiContent.innerHTML = `<div class="tr-ai-content" style="color:var(--color-up)">${json.message || '分析失败'}</div>`;
}
} catch (e) {
aiContent.innerHTML = `<div class="tr-ai-content" style="color:var(--color-up)">分析请求失败: ${e.message}</div>`;
}
}
// ===== 文件导入 =====
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);
trLoadAllData();
} else {
alert('导入失败: ' + (json.message || '未知错误'));
}
} catch (err) {
alert('导入失败: ' + err.message);
} finally {
btn.disabled = false;
btn.querySelector('span:last-child').textContent = '导入结算单';
}
}
async function trHandleBatchImport(e) {
const files = e.target.files;
if (!files || files.length === 0) return;
e.target.value = '';
const btn = document.getElementById('tr-btn-batch-import');
btn.disabled = true;
btn.querySelector('span:last-child').textContent = '导入中...';
try {
const formData = new FormData();
for (let i = 0; i < files.length; i++) formData.append('files', files[i]);
const res = await fetch(`${TR_API_BASE}/batch-import`, { method: 'POST', body: formData });
const json = await res.json();
if (json.success) {
trShowBatchImportResult(json);
if (json.data.total_futures > 0 || json.data.total_options > 0) trLoadAllData();
} else {
alert('批量导入失败: ' + (json.message || '未知错误'));
}
} catch (err) {
alert('批量导入失败: ' + err.message);
} finally {
btn.disabled = false;
btn.querySelector('span:last-child').textContent = '批量导入';
}
}
function trShowBatchImportResult(json) {
const modal = document.getElementById('tr-batch-import-modal');
const summaryEl = document.getElementById('tr-batch-import-summary');
const detailsEl = document.getElementById('tr-batch-import-details');
const data = json.data;
summaryEl.textContent = json.message;
detailsEl.innerHTML = data.details.map(d => {
let icon = d.success ? (d.new_count > 0 ? '✅' : '⚠️') : '❌';
return `<div style="padding:10px 12px;margin-bottom:8px;border-radius:6px;background:#F5F5F7;border-left:3px solid ${d.success ? '#34C759' : '#FF3B30'};">
<div style="font-weight:600;font-size:13px;">${icon} ${d.filename}</div>
<div style="font-size:12px;color:var(--text-tertiary);margin-top:4px;">${d.message}</div>
</div>`;
}).join('');
modal.classList.add('show');
}
function trCloseBatchImportModal() {
document.getElementById('tr-batch-import-modal').classList.remove('show');
}
// ===== 批次管理 =====
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) {
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();
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; }
if (!confirm(`⚠️ 确定删除 ${tradeDate}${recordCount} 条交易记录吗?此操作不可恢复!`)) 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}`);
trLoadAllData();
} else {
alert(json.message || '删除失败');
}
} catch (e) { alert('删除失败: ' + e.message); }
}
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('');
}