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

4622 lines
193 KiB

const API_BASE = '/api/v1/futures';
let klineChart = null;
let currentSymbol = null;
let currentPeriod = '15';
let allFuturesData = [];
let watchedSymbols = [];
let currentDetailData = null;
document.addEventListener('DOMContentLoaded', function() {
addScoreGradient();
updateTime();
setInterval(updateTime, 1000);
initEventListeners();
loadWatchedSymbols();
loadFuturesList();
});
function addScoreGradient() {
const svg = document.querySelector('.score-ring svg');
if (svg && !svg.querySelector('defs')) {
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gradient.setAttribute('id', 'scoreGradient');
gradient.setAttribute('x1', '0%');
gradient.setAttribute('y1', '0%');
gradient.setAttribute('x2', '100%');
gradient.setAttribute('y2', '0%');
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', '#ef4444');
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop2.setAttribute('offset', '50%');
stop2.setAttribute('stop-color', '#f59e0b');
const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop3.setAttribute('offset', '100%');
stop3.setAttribute('stop-color', '#10b981');
gradient.appendChild(stop1);
gradient.appendChild(stop2);
gradient.appendChild(stop3);
defs.appendChild(gradient);
svg.insertBefore(defs, svg.firstChild);
}
}
function updateTime() {
const now = new Date();
const timeStr = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0') + ' ' +
String(now.getHours()).padStart(2, '0') + ':' +
String(now.getMinutes()).padStart(2, '0') + ':' +
String(now.getSeconds()).padStart(2, '0');
document.getElementById('current-time').textContent = timeStr;
}
function initEventListeners() {
const backBtn = document.getElementById('back-btn');
if (backBtn) backBtn.addEventListener('click', showListView);
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) themeToggle.addEventListener('click', toggleTheme);
const refreshAllBtn = document.getElementById('refresh-all-btn');
if (refreshAllBtn) refreshAllBtn.addEventListener('click', refreshAllSymbols);
const aiAnalyzeAllBtn = document.getElementById('ai-analyze-all-btn');
if (aiAnalyzeAllBtn) aiAnalyzeAllBtn.addEventListener('click', analyzeAllSymbols);
const refreshSymbolBtn = document.getElementById('refresh-symbol-btn');
if (refreshSymbolBtn) refreshSymbolBtn.addEventListener('click', function() {
if (currentSymbol) {
refreshSingleSymbol(currentSymbol);
}
});
const savedTheme = localStorage.getItem('futures-theme');
if (savedTheme === 'dark') {
document.body.classList.remove('theme-minimal');
updateThemeIcon(false);
} else {
document.body.classList.add('theme-minimal');
updateThemeIcon(true);
}
document.querySelectorAll('.period-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentPeriod = this.dataset.period;
loadKlineData(currentSymbol, currentPeriod);
});
});
const searchInput = document.getElementById('search-input');
if (searchInput) searchInput.addEventListener('input', function() {
filterFuturesList(this.value);
});
document.querySelectorAll('.pill').forEach(tab => {
if (tab.dataset.category) {
tab.addEventListener('click', function() {
document.querySelectorAll('.pill').forEach(t => t.classList.remove('active'));
this.classList.add('active');
filterByCategory(this.dataset.category);
});
}
});
const sortSelect = document.getElementById('sort-select');
if (sortSelect) sortSelect.addEventListener('change', function() {
sortFuturesList(this.value);
});
document.querySelectorAll('.modal-overlay').forEach(modal => {
modal.addEventListener('click', function(e) {
if (e.target === this) {
this.classList.remove('active');
}
});
});
// 导航项点击事件
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 trReflCurrentPairId = null;
let trReflCurrentReflectionId = null;
let trReflCurrentAIResult = 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();
});
}
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', trExpRender);
document.getElementById('tr-exp-tag-filter').addEventListener('change', trExpRender);
document.getElementById('tr-exp-type-filter').addEventListener('change', trExpRender);
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 unpaired = data.unpaired || [];
if (!unpaired.length) {
unpairedBody.innerHTML = '';
unpairedEmpty.style.display = 'flex';
} 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>${u.trade_time || '-'}</td>
<td>${u.variety}${u.symbol_name ? '(' + u.symbol_name + ')' : ''}</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('');
}
}
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;
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); }
}
}
const result = trReflCurrentAIResult || {};
document.getElementById('tr-refl-ai-pair-id').value = pairId;
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 || '暂无建议';
document.getElementById('tr-refl-ai-modal').classList.add('show');
}
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);
if (!pair || !trReflCurrentAIResult) return;
const result = trReflCurrentAIResult;
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;
}
// ===== 经验库 =====
async function trExpLoad() {
try {
const res = await fetch(`${TR_API_BASE}/experiences`);
const json = await res.json();
trReflExperiences = json.success ? (json.data || []) : [];
trExpPopulateTagFilter();
trExpRender();
} catch (e) {
console.error('加载经验库失败', e);
trReflExperiences = [];
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 keyword = document.getElementById('tr-exp-search').value.toLowerCase();
const tag = document.getElementById('tr-exp-tag-filter').value;
const type = document.getElementById('tr-exp-type-filter').value;
const view = document.querySelector('.tr-exp-viewtab.active').dataset.view;
let list = trReflExperiences.filter(e => {
const matchKeyword = !keyword || (e.title || '').toLowerCase().includes(keyword) || (e.content || '').toLowerCase().includes(keyword);
const matchTag = !tag || (e.tags || []).some(t => (typeof t === 'string' ? t : t.name) === tag);
const matchType = !type || e.type === type;
return matchKeyword && matchTag && matchType;
});
if (!list.length) {
container.innerHTML = '';
container.className = 'tr-exp-grid';
empty.style.display = 'flex';
return;
}
empty.style.display = 'none';
if (view === 'timeline') {
container.className = '';
list = [...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;">${list.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">
<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 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('');
}