-
🔍
-
暂无配对交易数据
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
该品种交易记录
+
+ | 开仓时间 | 方向 | 开仓价 | 平仓价 | 盈亏 | 手续费 | 操作 |
+
+
+
-
+
-
-
-
-
-
K线走势与买卖点
-
+
+
+
K线走势与买卖点
+
-
+
-
-
-
AI 交易分析
-
-
-
+
+
+
@@ -1146,19 +1172,6 @@
-
-
-
diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js
index 447b229..8359b70 100644
--- a/app/static/futures_analysis.js
+++ b/app/static/futures_analysis.js
@@ -630,8 +630,19 @@ function renderFuturesGrid(data) {
// ==================== 交易复盘功能 ====================
const TR_API_BASE = '/api/v1/trade-review';
-let trCurrentPage = 1;
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;
@@ -651,7 +662,6 @@ function initTradeReview() {
// 查询按钮
document.getElementById('tr-btn-query').addEventListener('click', () => {
- trCurrentPage = 1;
trLoadAllData();
});
@@ -661,19 +671,7 @@ function initTradeReview() {
// 批次管理
document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal);
- // 子导航切换
- document.querySelectorAll('.tr-sub-nav-item').forEach(item => {
- item.addEventListener('click', function(e) {
- e.preventDefault();
- document.querySelectorAll('.tr-sub-nav-item').forEach(i => i.classList.remove('active'));
- this.classList.add('active');
- const sub = this.dataset.sub;
- document.querySelectorAll('.tr-sub-page').forEach(p => p.classList.remove('active'));
- document.getElementById('tr-sub-' + sub).classList.add('active');
- });
- });
-
- // 初始加载:先获取最后交易日,设为默认日期,再加载数据
+ // 初始加载
trInitDefaultDate();
}
@@ -709,105 +707,654 @@ function trBuildParams(extra = {}) {
return params.toString();
}
+// ===== 加载所有数据 =====
async function trLoadAllData() {
- trLoadStatistics();
- trLoadRecords();
+ 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 trLoadStatistics() {
+async function trLoadTradePairsAsync() {
try {
- const res = await fetch(`${TR_API_BASE}/statistics?${trBuildParams()}`);
+ const res = await fetch(`${TR_API_BASE}/trade-pairs?${trBuildParams()}`);
const json = await res.json();
- if (json.success) trRenderStatistics(json.data);
- } catch (e) {
- console.error('加载统计失败', e);
- }
+ if (json.success) trAllPairs = json.data;
+ } catch (e) { console.error('加载配对失败', e); }
}
-function trRenderStatistics(stats) {
+// ===== 6个统计卡片 =====
+function trRenderStatistics(stats, daily) {
const grid = document.getElementById('tr-stats-grid');
- const pnlClass = stats.total_pnl >= 0 ? 'profit' : 'loss';
+ 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 = `
总盈亏
-
${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl.toFixed(2)}
-
手续费: ${stats.total_commission.toFixed(2)}
+
${totalPnl >= 0 ? '+' : ''}${totalPnl.toFixed(2)}
+
平仓盈亏
+
+
+
净盈亏
+
${netPnl >= 0 ? '+' : ''}${netPnl.toFixed(2)}
+
扣除手续费后
胜率
-
${stats.win_rate}%
-
盈亏比: ${stats.profit_loss_ratio}
+
${stats.win_rate || 0}%
+
盈亏比: ${stats.profit_loss_ratio || 0}
-
交易统计
-
${stats.total_trades}
-
${stats.trading_days} 个交易日
+
交易笔数
+
${stats.total_trades || 0}
+
${stats.trading_days || 0}个交易日
+
+
+
最大回撤
+
${maxDd >= 0 ? '+' : ''}${maxDd.toFixed(2)}
+
${ddDate || '-'}
日均盈亏
-
${stats.avg_daily_pnl >= 0 ? '+' : ''}${stats.avg_daily_pnl}
-
最大连盈 ${stats.max_consecutive_wins} / 连亏 ${stats.max_consecutive_losses}
+
${(stats.avg_daily_pnl || 0) >= 0 ? '+' : ''}${stats.avg_daily_pnl || 0}
+
连盈${stats.max_consecutive_wins || 0}/连亏${stats.max_consecutive_losses || 0}
`;
}
-async function trLoadRecords() {
+// ===== 账户权益曲线 =====
+function trRenderEquityChart(daily) {
+ const dom = document.getElementById('tr-equity-chart');
+ if (!dom) return;
+ if (trEquityChart) trEquityChart.dispose();
+ if (!daily.length) { dom.innerHTML = '
'; 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 + '
';
+ ps.forEach(p => { s += p.marker + p.seriesName + ': ' + (p.value >= 0 ? '+' : '') + p.value.toFixed(2) + '
'; });
+ 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 = '
'; 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}
笔数: ${v.total_trades}
净盈亏: ${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl.toFixed(2)}
手续费: ${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 `
+ | ${d.trade_date} |
+ ${d.total_trades} |
+ ${closePnl >= 0 ? '+' : ''}${closePnl.toFixed(2)} |
+ ${d.total_commission.toFixed(2)} |
+ ${net >= 0 ? '+' : ''}${net.toFixed(2)} |
+ ${d.win_count} |
+ ${d.loss_count} |
+ ${d.win_rate}% |
+ +${d.max_win.toFixed(2)} |
+ ${d.max_loss.toFixed(2)} |
+ ${d.variety_count} |
+
`;
+ }).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 `
+ | ${rank} |
+ ${v.variety} |
+ ${v.symbol_name || '-'} |
+ ${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl.toFixed(2)} |
+ ${v.total_close_pnl >= 0 ? '+' : ''}${v.total_close_pnl.toFixed(2)} |
+ ${v.total_commission.toFixed(2)} |
+ ${v.win_rate}% |
+ - |
+ ${v.total_trades} |
+ 详情 |
+
`;
+ }).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 = '
正在生成诊断...';
+ 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 = '
'; 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 = '
| 无数据 |
'; 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 `
+ | ${g.variety} |
+ ${g.trades} |
+ ${g.closePnl >= 0 ? '+' : ''}${g.closePnl.toFixed(2)} |
+ ${g.commission.toFixed(2)} |
+ ${g.netPnl >= 0 ? '+' : ''}${g.netPnl.toFixed(2)} |
+ ${g.winRate.toFixed(1)}% |
+ 详情 |
+
`;
+ }).join('');
+}
+
+function trRenderDailyTradeFlow(records) {
+ const tbody = document.getElementById('tr-daily-trades-body');
+ if (!records.length) { tbody.innerHTML = '
| 无记录 |
'; 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 `
+ | ${r.trade_time || '-'} |
+ ${r.variety}${r.symbol_name ? '(' + r.symbol_name + ')' : ''} |
+ ${r.direction} |
+ ${(r.price || 0).toFixed(2)} |
+ ${r.volume || 0} |
+ ${pnl !== 0 ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) : '-'} |
+ ${(r.commission || 0).toFixed(2)} |
+
`;
+ }).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 => `
${t}`).join('');
+ html += '
1. 交易质量诊断:';
+ if (d.win_rate < 15) html += `当日胜率仅${d.win_rate}%,显著低于正常水平,建议减少冲动开单。
`;
+ else html += '胜率尚可,但需关注盈亏比是否合理。
';
+ html += '
2. 手续费分析:';
+ if (d.total_commission > Math.abs(d.total_pnl) * 0.5) html += `手续费(${d.total_commission.toFixed(2)})占比过高,侵蚀了利润。
`;
+ else html += '手续费控制合理。
';
+ html += '
3. 改进建议:';
+ if (d.loss_count > d.win_count) html += '亏损笔数较多,建议设置严格止损,避免扛单。
';
+ 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 params = trBuildParams({ page: trCurrentPage, page_size: 50 });
- const res = await fetch(`${TR_API_BASE}/records?${params}`);
+ const res = await fetch(`${TR_API_BASE}/kline-with-trades/${variety}?period=${period}&${trBuildParams()}`);
const json = await res.json();
- if (json.success) trRenderRecords(json.data);
+ if (!json.success || !json.data) {
+ dom.innerHTML = '
';
+ return;
+ }
+ trRenderKlineChart(trVarietyKlineChart, json.data);
+ } catch (e) {
+ dom.innerHTML = '
';
+ }
+}
+
+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 =>
+ `
| ${r.trade_date} ${r.trade_time} | ${r.direction}${r.offset ? '/' + r.offset : ''} | ${(r.price || 0).toFixed(2)} | - | ${r.close_pnl ? (r.close_pnl >= 0 ? '+' : '') + r.close_pnl.toFixed(2) : '-'} | ${(r.commission || 0).toFixed(2)} | - |
`
+ ).join('') : '
| 暂无交易记录 |
';
+ 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, '"');
+ return `
+ | ${p.open_date} ${p.open_time || ''} |
+ ${p.direction} |
+ ${(p.open_price || 0).toFixed(2)} |
+ ${(p.close_price || 0).toFixed(2)} |
+ ${p.net_pnl >= 0 ? '+' : ''}${p.net_pnl.toFixed(2)} |
+ ${p.commission.toFixed(2)} |
+ 查看 |
+
`;
+ }).join('');
} catch (e) {
- console.error('加载记录失败', e);
+ console.error('加载品种交易记录失败', e);
+ tbody.innerHTML = '
| 加载失败 |
';
}
}
-function trRenderRecords(data) {
- const tbody = document.getElementById('tr-records-body');
- const emptyEl = document.getElementById('tr-records-empty');
- const tableWrap = document.getElementById('tr-records-table-wrap');
- const pagination = document.getElementById('tr-records-pagination');
+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 `
`;
+}
- if (!data.records || data.records.length === 0) {
- tableWrap.style.display = 'none';
- pagination.style.display = 'none';
- emptyEl.style.display = 'flex';
+// ===== K线渲染通用函数 =====
+function trRenderKlineChart(chart, data) {
+ const candles = data.candles;
+ if (!candles || !candles.length) {
+ chart.getDom().innerHTML = '
';
return;
}
- tableWrap.style.display = '';
- emptyEl.style.display = 'none';
+ const dates = candles.map(c => c[0]);
+ const ohlc = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]);
- tbody.innerHTML = data.records.map(r => {
- const pnlClass = r.close_pnl > 0 ? 'tr-pnl-positive' : (r.close_pnl < 0 ? 'tr-pnl-negative' : '');
- const dirClass = r.direction === '买' ? 'tr-dir-buy' : 'tr-dir-sell';
- return `
- | ${r.trade_date || '-'} |
- ${r.trade_time || '-'} |
- ${r.trade_type} |
- ${r.symbol} |
- ${r.symbol_name || r.variety} |
- ${r.direction} |
- ${r.offset || '-'} |
- ${r.price != null ? r.price.toFixed(2) : '-'} |
- ${r.volume || '-'} |
- ${r.amount != null ? r.amount.toFixed(2) : '-'} |
- ${r.close_pnl != null ? r.close_pnl.toFixed(2) : '-'} |
- ${r.commission != null ? r.commission.toFixed(2) : '-'} |
- ${(r.source_file || '').substring(0, 15)} |
-
`;
- }).join('');
+ const series = [{
+ type: 'candlestick', data: ohlc,
+ itemStyle: { color: '#34C759', color0: '#FF3B30', borderColor: '#34C759', borderColor0: '#FF3B30' }
+ }];
- // 分页
- const totalPages = Math.ceil(data.total / data.page_size);
- pagination.style.display = totalPages > 1 ? 'flex' : 'none';
- pagination.innerHTML = `
-
-
${trCurrentPage} / ${totalPages} (共 ${data.total} 条)
-
- `;
+ // 买卖标记
+ 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}
开: ${parseFloat(v[1]).toFixed(2)}
收: ${parseFloat(v[2]).toFixed(2)}
低: ${parseFloat(v[3]).toFixed(2)}
高: ${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 = '
';
+ return;
+ }
+ trRenderKlineChart(trDetailChart, json.data);
+ } catch (e) {
+ dom.innerHTML = '
';
+ }
+}
+
+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 = '
🤖 AI 交易分析
正在分析中,请稍候...
';
+
+ 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 = `
🤖 AI 交易分析
${json.data.analysis.replace(/\n/g, '
')}
`;
+ } else {
+ aiContent.innerHTML = `
${json.message || '分析失败'}
`;
+ }
+ } catch (e) {
+ aiContent.innerHTML = `
分析请求失败: ${e.message}
`;
+ }
}
+// ===== 文件导入 =====
async function trHandleFileImport(e) {
const file = e.target.files[0];
if (!file) return;
@@ -824,7 +1371,6 @@ async function trHandleFileImport(e) {
const json = await res.json();
if (json.success) {
alert(json.message);
- trCurrentPage = 1;
trLoadAllData();
} else {
alert('导入失败: ' + (json.message || '未知错误'));
@@ -848,19 +1394,12 @@ async function trHandleBatchImport(e) {
try {
const formData = new FormData();
- for (let i = 0; i < files.length; i++) {
- formData.append('files', files[i]);
- }
+ 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);
- const data = json.data;
- if (data.total_futures > 0 || data.total_options > 0) {
- trCurrentPage = 1;
- trLoadAllData();
- }
+ if (json.data.total_futures > 0 || json.data.total_options > 0) trLoadAllData();
} else {
alert('批量导入失败: ' + (json.message || '未知错误'));
}
@@ -877,32 +1416,14 @@ function trShowBatchImportResult(json) {
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 statusIcon, statusColor;
- if (!d.success) {
- statusIcon = '❌';
- statusColor = 'var(--color-error, #ef4444)';
- } else if (d.new_count > 0 && d.skipped_count > 0) {
- statusIcon = '⚡';
- statusColor = 'var(--color-warning, #f59e0b)';
- } else if (d.new_count > 0) {
- statusIcon = '✅';
- statusColor = 'var(--color-success, #22c55e)';
- } else {
- statusIcon = '⚠️';
- statusColor = 'var(--color-warning, #f59e0b)';
- }
- return `
-
- ${statusIcon} ${d.filename}
-
-
${d.message}
+ let icon = d.success ? (d.new_count > 0 ? '✅' : '⚠️') : '❌';
+ return `
+
${icon} ${d.filename}
+
${d.message}
`;
}).join('');
-
modal.classList.add('show');
}
@@ -910,8 +1431,7 @@ 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');
@@ -919,14 +1439,12 @@ async function trShowBatchModal() {
const res = await fetch(`${TR_API_BASE}/batches`);
const json = await res.json();
if (json.success) trRenderBatchList(json.data);
- } catch (e) {
- console.error('加载批次失败', e);
- }
+ } catch (e) { console.error('加载批次失败', e); }
}
function trRenderBatchList(batches) {
const list = document.getElementById('tr-batch-list');
- if (!batches || batches.length === 0) {
+ if (!batches || !batches.length) {
list.innerHTML = '
';
return;
}
@@ -949,753 +1467,42 @@ async function trDeleteBatch(batchId) {
if (json.success) {
alert(json.message);
trShowBatchModal();
- trCurrentPage = 1;
trLoadAllData();
}
- } catch (e) {
- alert('删除失败: ' + e.message);
- }
+ } 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;
- }
+ if (!startDate || !endDate) { alert('请先选择要删除的日期范围'); return; }
+ if (startDate !== endDate) { alert('删除功能仅支持单个日期,请确保开始日期和结束日期相同'); return; }
const tradeDate = startDate;
-
- // 第一次确认:查询该日期的记录数
+
try {
const queryRes = await fetch(`${TR_API_BASE}/records?start_date=${tradeDate}&end_date=${tradeDate}&page=1&page_size=1`);
const queryJson = await queryRes.json();
const recordCount = queryJson.success ? queryJson.data.total : 0;
-
- if (recordCount === 0) {
- alert(`${tradeDate} 没有交易记录,无需删除`);
- return;
- }
-
- // 第一次确认弹窗
- const firstConfirm = confirm(
- `⚠️ 警告:即将删除交易数据\n\n` +
- `交易日:${tradeDate}\n` +
- `记录数量:${recordCount} 条\n\n` +
- `此操作将永久删除该日期的所有交易记录,不可恢复!\n\n` +
- `点击"确定"继续,点击"取消"返回`
- );
-
- if (!firstConfirm) return;
-
- // 第二次确认弹窗(更强烈的警告)
- const secondConfirm = confirm(
- `🔴 最终确认\n\n` +
- `您确定要删除 ${tradeDate} 的 ${recordCount} 条交易记录吗?\n\n` +
- `删除后无法恢复,请确认!`
- );
-
- if (!secondConfirm) return;
-
- // 执行删除
+ 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}`);
- trCurrentPage = 1;
trLoadAllData();
} else {
alert(json.message || '删除失败');
}
- } catch (e) {
- alert('删除失败: ' + e.message);
- }
+ } catch (e) { alert('删除失败: ' + e.message); }
}
-// ==================== 每日分析 ====================
-
-async function trLoadDailySummary() {
- try {
- const res = await fetch(`${TR_API_BASE}/daily-summary?${trBuildParams()}`);
- const json = await res.json();
- if (json.success) trRenderDailySummary(json.data);
- } catch (e) {
- console.error('加载每日汇总失败', e);
- }
-}
-
-function trRenderDailySummary(data) {
- const tbody = document.getElementById('tr-daily-body');
- const emptyEl = document.getElementById('tr-daily-empty');
- const chartDom = document.getElementById('tr-daily-chart');
-
- if (!data || data.length === 0) {
- tbody.innerHTML = '';
- emptyEl.style.display = 'flex';
- chartDom.style.display = 'none';
- return;
- }
- emptyEl.style.display = 'none';
- chartDom.style.display = '';
-
- tbody.innerHTML = data.map(d => {
- const pnlClass = d.total_pnl > 0 ? 'tr-pnl-positive' : (d.total_pnl < 0 ? 'tr-pnl-negative' : '');
- return `
- | ${d.trade_date} |
- ${d.total_trades} |
- ${d.total_pnl >= 0 ? '+' : ''}${d.total_pnl} |
- ${d.total_commission} |
- ${d.win_count} |
- ${d.loss_count} |
- ${d.win_rate}% |
- ${d.max_win > 0 ? '+' + d.max_win : '-'} |
- ${d.max_loss < 0 ? d.max_loss : '-'} |
- ${d.variety_count} |
-
`;
- }).join('');
-
- // 渲染柱状图
- trRenderDailyChart(data);
-}
-
-function trRenderDailyChart(data) {
- const chartDom = document.getElementById('tr-daily-chart');
- const chart = echarts.init(chartDom);
- chart.setOption({
- backgroundColor: 'transparent',
- tooltip: { trigger: 'axis' },
- grid: { left: 60, right: 20, top: 20, bottom: 40 },
- xAxis: { type: 'category', data: data.map(d => d.trade_date), axisLabel: { fontSize: 10, rotate: 30 } },
- yAxis: { type: 'value', axisLabel: { fontSize: 11 } },
- series: [{
- type: 'bar',
- data: data.map(d => ({
- value: d.total_pnl,
- itemStyle: { color: d.total_pnl >= 0 ? '#34C759' : '#FF3B30' },
- })),
- }],
- });
-}
-
-// ==================== 品种汇总 ====================
-
-async function trLoadVarietySummary() {
- try {
- const res = await fetch(`${TR_API_BASE}/variety-summary?${trBuildParams()}`);
- const json = await res.json();
- if (json.success) trRenderVarietySummary(json.data);
- } catch (e) {
- console.error('加载品种汇总失败', e);
- }
-}
-
-function trRenderVarietySummary(data) {
- const tbody = document.getElementById('tr-variety-body');
- const emptyEl = document.getElementById('tr-variety-empty');
-
- if (!data || data.length === 0) {
- tbody.innerHTML = '';
- emptyEl.style.display = 'flex';
- return;
- }
- emptyEl.style.display = 'none';
-
- tbody.innerHTML = data.map(v => {
- const pnlClass = v.total_pnl > 0 ? 'tr-pnl-positive' : (v.total_pnl < 0 ? 'tr-pnl-negative' : '');
- return `
- | ${v.variety} |
- ${v.symbol_name || '-'} |
- ${v.total_trades} |
- ${v.total_pnl >= 0 ? '+' : ''}${v.total_pnl} |
- ${v.total_close_pnl} |
- ${v.total_commission} |
- ${v.total_volume} |
- ${v.buy_count} |
- ${v.sell_count} |
- ${v.win_rate}% |
-
`;
- }).join('');
-}
-
-// ==================== 逐笔分析 ====================
-
-async function trLoadTradePairs() {
- try {
- const res = await fetch(`${TR_API_BASE}/trade-pairs?${trBuildParams()}`);
- const json = await res.json();
- if (json.success) trRenderTradePairs(json.data);
- } catch (e) {
- console.error('加载逐笔数据失败', e);
- }
-}
-
-function trRenderTradePairs(pairs) {
- const grid = document.getElementById('tr-pairs-grid');
- const emptyEl = document.getElementById('tr-pairs-empty');
-
- if (!pairs || pairs.length === 0) {
- grid.innerHTML = '';
- emptyEl.style.display = 'flex';
- return;
- }
- emptyEl.style.display = 'none';
-
- grid.innerHTML = pairs.map(p => {
- const pnlClass = p.net_pnl >= 0 ? 'profit' : 'loss';
- const dirText = p.direction === '多' ? '做多' : '做空';
- const dirClass = p.direction === '多' ? 'tr-dir-buy' : 'tr-dir-sell';
- const pairData = JSON.stringify(p).replace(/"/g, '"');
- return `
-
-
-
-
开仓${p.open_date} ${p.open_time || ''}
-
平仓${p.close_date || '-'} ${p.close_time || ''}
-
开仓价${p.open_price != null ? p.open_price.toFixed(2) : '-'}
-
平仓价${p.close_price != null ? p.close_price.toFixed(2) : '-'}
-
手数${p.volume || '-'}
-
手续费${p.commission}
-
-
-
-
-
`;
- }).join('');
-}
-
-async function trAnalyzeTrade(pair) {
- const modal = document.getElementById('tr-analysis-modal');
- const title = document.getElementById('tr-analysis-title');
- const content = document.getElementById('tr-analysis-content');
-
- title.textContent = `AI 分析 - ${pair.symbol_name || pair.symbol} ${pair.direction}`;
- content.textContent = '正在分析中,请稍候...';
- modal.classList.add('show');
-
- try {
- const res = await fetch(`${TR_API_BASE}/analyze-trade`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- symbol: pair.symbol,
- open_date: pair.open_date,
- open_time: pair.open_time,
- close_date: pair.close_date,
- close_time: pair.close_time,
- direction: pair.direction,
- open_price: pair.open_price,
- close_price: pair.close_price,
- }),
- });
- const json = await res.json();
- if (json.success) {
- content.textContent = json.data.analysis;
- } else {
- content.textContent = json.message || '分析失败';
- }
- } catch (e) {
- content.textContent = '分析请求失败: ' + e.message;
- }
-}
-
-function trCloseAnalysisModal() {
- document.getElementById('tr-analysis-modal').classList.remove('show');
-}
-
-// ==================== 交易详情弹窗 ====================
-let trDetailChart = null;
-let trCurrentPair = null;
-let trCurrentPeriod = 'daily'; // 当前K线周期
-
-function trSwitchKlinePeriod(period) {
- trCurrentPeriod = period;
- // 更新标签页样式
- document.querySelectorAll('.tr-kline-tab').forEach(btn => {
- btn.classList.toggle('active', btn.dataset.period === period);
- });
- // 重新加载K线
- 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 === '多' ? '做多' : '做空'} 交易详情`;
-
- // 渲染交易信息
- const pnlClass = pair.net_pnl >= 0 ? 'profit' : 'loss';
- info.innerHTML = `
-
- 品种
- ${pair.symbol_name || pair.symbol}
-
-
- 方向
- ${pair.direction === '多' ? '做多' : '做空'}
-
-
- 开仓日期
- ${pair.open_date || '-'}
-
-
- 开仓时间
- ${pair.open_time || '-'}
-
-
- 开仓价
- ${pair.open_price != null ? pair.open_price.toFixed(2) : '-'}
-
-
- 平仓日期
- ${pair.close_date || '-'}
-
-
- 平仓时间
- ${pair.close_time || '-'}
-
-
- 平仓价
- ${pair.close_price != null ? pair.close_price.toFixed(2) : '-'}
-
-
- 手数
- ${pair.volume || '-'}
-
-
- 手续费
- ${pair.commission || '-'}
-
-
- 净盈亏
- ${pair.net_pnl >= 0 ? '+' : ''}${pair.net_pnl || 0}
-
- `;
-
- // 重置AI分析区域
- aiContent.innerHTML = `
`;
- document.getElementById('tr-detail-ai-btn').addEventListener('click', () => trAnalyzeInDetail(pair));
-
- modal.classList.add('show');
-
- // 绑定刷新K线按钮事件
- document.getElementById('tr-detail-reload-kline').onclick = () => trLoadDetailKline(pair);
-
- // 加载K线
- await trLoadDetailKline(pair);
-}
-
-async function trLoadDetailKline(pair) {
- const period = trCurrentPeriod;
- const chartDom = document.getElementById('tr-detail-kline');
-
- console.log('[K线加载] 开始加载K线数据', {
- symbol: pair.symbol,
- variety: pair.variety,
- period: period,
- open_date: pair.open_date,
- close_date: pair.close_date,
- });
-
- try {
- // 构建日期范围:开仓日前7天到平仓日后7天,确保交易前后都有K线数据
- 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 params = new URLSearchParams({
- start_date: startDate.toISOString().slice(0, 10),
- end_date: endDate.toISOString().slice(0, 10),
- period: period,
- });
-
- const url = `${TR_API_BASE}/kline-with-trades/${pair.symbol}?${params}`;
- console.log('[K线加载] 请求URL:', url);
-
- const res = await fetch(url);
- const json = await res.json();
-
- console.log('[K线加载] API响应:', {
- success: json.success,
- message: json.message,
- hasData: !!json.data,
- symbol: json.data?.symbol,
- candlesCount: json.data?.candles?.length || 0,
- markersCount: json.data?.trade_markers?.length || 0,
- });
-
- if (!json.success) {
- console.warn('[K线加载] API返回失败:', json.message);
- chartDom.innerHTML = `
暂无K线数据: ${json.message || '未知错误'}
`;
- if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; }
- return;
- }
-
- if (!json.data.candles || json.data.candles.length === 0) {
- console.warn('[K线加载] K线数据为空');
- chartDom.innerHTML = '
暂无K线数据
';
- if (trDetailChart) { trDetailChart.dispose(); trDetailChart = null; }
- return;
- }
-
- console.log('[K线加载] 开始渲染K线图');
- trRenderDetailKline(json.data, pair);
- console.log('[K线加载] K线图渲染完成');
- } catch (e) {
- console.error('[K线加载] 加载K线失败:', e);
- chartDom.innerHTML = `
加载K线失败: ${e.message}
`;
- }
-}
-
-function trRenderDetailKline(data, pair) {
- if (trDetailChart) trDetailChart.dispose();
- const chartDom = document.getElementById('tr-detail-kline');
- trDetailChart = echarts.init(chartDom, 'dark');
-
- const candles = data.candles;
- const dates = candles.map(c => c[0]);
- const values = candles.map(c => [parseFloat(c[1]), parseFloat(c[2]), parseFloat(c[3]), parseFloat(c[4])]);
- const volumes = candles.map(c => [parseInt(c[5]), parseFloat(c[2]) >= parseFloat(c[1]) ? 1 : -1]);
-
- // 计算MA均线
- const ma5 = calculateMA(candles, 5);
- const ma10 = calculateMA(candles, 10);
- const ma20 = calculateMA(candles, 20);
-
- // 构建买卖标记(每根K线柱只保留一对买/卖标记)
- const buyMarkers = [];
- const sellMarkers = [];
- const markers = data.trade_markers || [];
- const period = data.period || 'daily';
-
- // 按K线索引分组,每根K线只保留第一个买和第一个卖
- const buyByIndex = {}; // { candleIndex: marker }
- const sellByIndex = {};
-
- markers.forEach(m => {
- let dateIdx = -1;
-
- if (period === 'daily') {
- // 日线:按日期匹配
- dateIdx = dates.indexOf(m.date);
- } else {
- // 分钟线:按时间戳匹配(找到最接近的K线)
- const tradeTime = new Date(`${m.date} ${m.time || '00:00:00'}`).getTime();
- let minDiff = Infinity;
- for (let i = 0; i < candles.length; i++) {
- const candleTime = new Date(candles[i][0]).getTime();
- const diff = Math.abs(candleTime - tradeTime);
- if (diff < minDiff) {
- minDiff = diff;
- dateIdx = i;
- }
- }
- }
-
- if (dateIdx === -1) return;
-
- // 按K线索引去重,只保留第一个
- if (m.direction === '买') {
- if (buyByIndex[dateIdx] === undefined) {
- buyByIndex[dateIdx] = m;
- }
- } else {
- if (sellByIndex[dateIdx] === undefined) {
- sellByIndex[dateIdx] = m;
- }
- }
- });
-
- // 计算全局价格范围,用于确定标记偏移量
- let globalLow = Infinity, globalHigh = -Infinity;
- candles.forEach(c => {
- const lo = parseFloat(c[3]);
- const hi = parseFloat(c[4]);
- if (lo < globalLow) globalLow = lo;
- if (hi > globalHigh) globalHigh = hi;
- });
- const priceRange = globalHigh - globalLow;
- const markerOffset = priceRange * 0.06; // 标记偏移量:价格范围的6%
-
- // 生成标记数据
- let minMarkerIdx = Infinity;
- let maxMarkerIdx = -Infinity;
-
- Object.entries(buyByIndex).forEach(([idx, m]) => {
- const dateIdx = parseInt(idx);
- const candle = candles[dateIdx];
- const price = parseFloat(m.price);
- const low = parseFloat(candle[3]);
- // 买标记放在K线下方,远离柱体
- const markerPrice = low - markerOffset;
-
- buyMarkers.push({
- name: `买${m.offset || ''}`,
- coord: [dateIdx, markerPrice],
- value: `买${m.offset || ''} ${price}`,
- itemStyle: { color: '#10b981' },
- });
-
- minMarkerIdx = Math.min(minMarkerIdx, dateIdx);
- maxMarkerIdx = Math.max(maxMarkerIdx, dateIdx);
- });
-
- Object.entries(sellByIndex).forEach(([idx, m]) => {
- const dateIdx = parseInt(idx);
- const candle = candles[dateIdx];
- const price = parseFloat(m.price);
- const high = parseFloat(candle[4]);
- // 卖标记放在K线上方,远离柱体
- const markerPrice = high + markerOffset;
-
- sellMarkers.push({
- name: `卖${m.offset || ''}`,
- coord: [dateIdx, markerPrice],
- value: `卖${m.offset || ''} ${price}`,
- itemStyle: { color: '#ef4444' },
- });
-
- minMarkerIdx = Math.min(minMarkerIdx, dateIdx);
- maxMarkerIdx = Math.max(maxMarkerIdx, dateIdx);
- });
-
- // 计算dataZoom范围:确保至少显示50根K线,并自动定位到买卖点区域
- let zoomStart = 0;
- let zoomEnd = 100;
- const totalLen = candles.length;
- const MIN_CANDLES = 50; // 最少显示50根K线
-
- if (minMarkerIdx !== Infinity && maxMarkerIdx !== -Infinity) {
- const markerRange = maxMarkerIdx - minMarkerIdx;
-
- // 计算需要显示的范围:至少50根,或者买卖点范围+前后各20根
- let displayCount = Math.max(MIN_CANDLES, markerRange + 40);
- displayCount = Math.min(displayCount, totalLen); // 不超过总长度
-
- // 以买卖点中心为基准,前后扩展
- const centerIdx = Math.floor((minMarkerIdx + maxMarkerIdx) / 2);
- let startIdx = centerIdx - Math.floor(displayCount / 2);
- let endIdx = startIdx + displayCount - 1;
-
- // 边界调整
- if (startIdx < 0) {
- startIdx = 0;
- endIdx = Math.min(displayCount - 1, totalLen - 1);
- }
- if (endIdx >= totalLen) {
- endIdx = totalLen - 1;
- startIdx = Math.max(0, endIdx - displayCount + 1);
- }
-
- zoomStart = Math.floor((startIdx / totalLen) * 100);
- zoomEnd = Math.ceil(((endIdx + 1) / totalLen) * 100);
- } else {
- // 没有买卖点,显示全部数据
- zoomStart = 0;
- zoomEnd = 100;
- }
-
- 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: '66%', height: '24%' },
- ],
- xAxis: [
- {
- type: 'category', data: dates, gridIndex: 0, boundaryGap: true,
- axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
- axisLabel: { show: false },
- splitLine: { show: false },
- },
- {
- type: 'category', data: dates, gridIndex: 1, boundaryGap: true,
- axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
- axisLabel: { color: '#64748b', fontSize: 10 },
- splitLine: { show: false },
- },
- ],
- yAxis: [
- {
- scale: true, gridIndex: 0,
- axisLine: { show: false },
- axisLabel: { color: '#64748b' },
- splitLine: { show: false },
- },
- {
- scale: true, gridIndex: 1,
- axisLine: { show: false },
- axisLabel: { show: false },
- splitLine: { show: false },
- },
- ],
- series: [
- {
- name: 'K线',
- type: 'candlestick',
- data: values,
- xAxisIndex: 0, yAxisIndex: 0,
- itemStyle: {
- color: '#10b981', color0: '#ef4444',
- borderColor: '#10b981', borderColor0: '#ef4444',
- },
- markPoint: {
- symbol: 'triangle',
- symbolSize: 12,
- data: buyMarkers,
- label: { show: true, formatter: '{b}', fontSize: 10, color: '#10b981', position: 'bottom', distance: 5 },
- },
- },
- {
- name: 'K线',
- type: 'candlestick',
- data: values,
- xAxisIndex: 0, yAxisIndex: 0,
- itemStyle: {
- color: '#10b981', color0: '#ef4444',
- borderColor: '#10b981', borderColor0: '#ef4444',
- },
- markPoint: {
- symbol: 'triangle',
- symbolSize: 12,
- symbolRotate: 180,
- data: sellMarkers,
- label: { show: true, formatter: '{b}', fontSize: 10, color: '#ef4444', position: 'top', distance: 5 },
- },
- },
- {
- name: 'MA5',
- type: 'line',
- data: ma5,
- xAxisIndex: 0, yAxisIndex: 0,
- lineStyle: { width: 1, color: '#f59e0b' },
- symbol: 'none',
- },
- {
- name: 'MA10',
- type: 'line',
- data: ma10,
- xAxisIndex: 0, yAxisIndex: 0,
- lineStyle: { width: 1, color: '#3b82f6' },
- symbol: 'none',
- },
- {
- name: 'MA20',
- type: 'line',
- data: ma20,
- xAxisIndex: 0, yAxisIndex: 0,
- 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)' }
- })),
- },
- ],
- dataZoom: [
- { type: 'inside', xAxisIndex: [0, 1], start: zoomStart, end: zoomEnd },
- {
- show: true, type: 'slider', xAxisIndex: [0, 1], bottom: 5, height: 16,
- start: zoomStart, end: zoomEnd,
- borderColor: 'transparent',
- backgroundColor: 'rgba(15, 20, 30, 0.5)',
- fillerColor: 'rgba(6, 182, 212, 0.15)',
- handleStyle: { color: '#06b6d4' },
- textStyle: { color: '#64748b' },
- },
- ],
- };
-
- trDetailChart.setOption(option);
-}
-
-function trCloseDetailModal() {
- document.getElementById('tr-detail-modal').classList.remove('show');
- if (trDetailChart) {
- trDetailChart.dispose();
- trDetailChart = null;
- }
- trCurrentPair = null;
-}
-
-async function trAnalyzeInDetail(pair) {
- const aiContent = document.getElementById('tr-detail-ai-content');
- aiContent.innerHTML = '
正在分析中,请稍候...
';
-
- 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 = `
${json.data.analysis}
`;
- } else {
- aiContent.innerHTML = `
${json.message || '分析失败'}
`;
- }
- } catch (e) {
- aiContent.innerHTML = `
分析请求失败: ${e.message}
`;
- }
-}
-
-// 子页面切换时懒加载
-document.addEventListener('click', function(e) {
- if (!e.target.classList.contains('tr-sub-nav-item')) return;
- const sub = e.target.dataset.sub;
- setTimeout(() => {
- if (sub === 'daily') trLoadDailySummary();
- else if (sub === 'variety') trLoadVarietySummary();
- else if (sub === 'pairs') trLoadTradePairs();
- }, 50);
-});
-
function formatNumber(num) {
if (num === 0 || num === undefined || num === null) return '--';
return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
diff --git a/data/trade_review.html b/data/trade_review.html
new file mode 100644
index 0000000..523aa92
--- /dev/null
+++ b/data/trade_review.html
@@ -0,0 +1,409 @@
+
+
+
+
+
+
期货智析 - 交易复盘
+
+
+
+
+
+
+
+
+
+
品种盈亏分布(气泡大小=手续费占比,颜色=净盈亏)
+
每日交易详情(点击日期查看详情)
+
| 日期 | 笔数 | 平仓盈亏 | 手续费 | 净盈亏 | 盈利笔 | 亏损笔 | 胜率 | 最大盈利 | 最大亏损 | 品种数 |
|---|
+
品种盈亏排行(点击查看品种详情)
+
| 排名 | 代码 | 名称 | 净盈亏 | 平仓盈亏 | 手续费 | 胜率 | 盈亏比 | 笔数 | 操作 |
|---|
+
+
+
+
+
+
+
+
+
+
+
当日品种盈亏统计(点击品种名查看详情)
+
+
+
+
+
+
+
+