/* 量化策略回测页 */ let curStrategy = 'ma_cross'; let rankData = []; let strategiesData = []; let chart = null; async function loadStrategies() { try { const d = await api('/api/strategies'); strategiesData = d.items || []; $('#strategyPills').innerHTML = strategiesData.map((s, i) => `` ).join(''); $$('#strategyPills .pill').forEach(p => p.onclick = () => { const cfg = strategiesData.find(s => s.key === p.dataset.k); selectStrategy(p.dataset.k, cfg); }); selectStrategy(strategiesData[0].key, strategiesData[0]); } catch (e) { $('#strategyPills').innerHTML = '
加载失败
'; } } function selectStrategy(key, cfg) { curStrategy = key; $$('#strategyPills .pill').forEach(p => p.classList.toggle('active', p.dataset.k === key)); if (cfg) renderDesc(cfg); loadMarket(key); } function renderDesc(cfg) { $('#stratDesc').innerHTML = `${cfg.icon} ${cfg.name}(${cfg.params}):${escapeHtml(cfg.desc)} ${cfg.tags.join(' / ')}`; const s = cfg.summary || {}; $('#stratSummary').innerHTML = `
全市场平均收益
${fmtPct(s.avg_return)}
平均夏普
${s.avg_sharpe ?? '--'}
平均胜率
${s.avg_win_rate ?? '--'}%
覆盖股票
${s.n ?? '--'}
最优标的
${s.best_name ? `${s.best_name} ${fmtPct(s.best_return)}` : '--'}
`; } async function loadMarket(key) { $('#rankTb').innerHTML = '加载中…'; try { const d = await api('/api/backtest/market?strategy=' + key); rankData = d.items || []; renderRank(); // 默认选中第一名股票 if (rankData.length) loadDetail(rankData[0].code); else $('#rankTb').innerHTML = '暂无回测数据,请到数据管理页重建'; } catch (e) { $('#rankTb').innerHTML = '加载失败'; } } function renderRank() { $('#rankHint').textContent = `(${rankData.length} 只股票)`; $('#rankTb').innerHTML = rankData.map((it, i) => ` ${i + 1} ${it.name}
${it.code}
${fmtPct(it.total_return)} ${fmtPct(it.benchmark)} ${fmtPct(it.excess)} ${fmtPct(it.max_drawdown)} ${it.sharpe} ${it.win_rate}% ${it.profit_factor} ${it.trades} `).join(''); // 同步下拉框 fillStockSelect(); } function fillStockSelect() { const sel = $('#btStock'); sel.innerHTML = rankData.map(it => ``).join(''); sel.onchange = () => loadDetail(sel.value); } async function loadDetail(code) { const sel = $('#btStock'); if (sel.value !== code) sel.value = code; try { const d = await api(`/api/backtest?strategy=${curStrategy}&code=${code}`); renderDetail(d); } catch (e) { $('#btHead').innerHTML = '
加载失败
'; } } function renderDetail(d) { const m = d.metrics, st = d.stock || {}; $('#btHead').innerHTML = `
${d.stock_name} ${d.code} · ${st.industry || ''} · ${st.board || ''}
${d.config.icon} ${d.config.name} · 回测 ${m.days} 个交易日
`; $('#btMetrics').innerHTML = `
策略收益
${fmtPct(m.total_return)}
年化收益
${fmtPct(m.annualized)}
最大回撤
${fmtPct(m.max_drawdown)}
夏普比率
${m.sharpe}
胜率
${m.win_rate}%
盈亏比
${m.profit_factor}
交易次数
${m.trades}
买入持有
${fmtPct(m.benchmark)}
超额收益
${fmtPct(m.excess)}
`; renderEquity(d.equity); renderTrades(d.trades); } function renderEquity(equity) { const el = $('#equityChart'); if (!window.echarts) { el.innerHTML = '
ECharts 加载失败
'; return; } if (!chart) chart = echarts.init(el); chart.setOption({ backgroundColor: 'transparent', animation: false, tooltip: { trigger: 'axis', valueFormatter: v => (v * 100).toFixed(1) + '%' }, legend: { data: ['策略净值', '买入持有'], textStyle: { color: '#9da7b3' }, top: 0 }, grid: { left: 50, right: 14, top: 28, bottom: 24 }, xAxis: { type: 'category', data: equity.map(e => e.date), axisLabel: { color: '#6e7681' }, axisLine: { lineStyle: { color: '#262d3a' } } }, yAxis: { type: 'value', name: '净值', scale: true, splitLine: { lineStyle: { color: '#1f2630' } }, axisLabel: { color: '#6e7681', formatter: v => (v * 100).toFixed(0) + '%' } }, series: [ { name: '策略净值', type: 'line', data: equity.map(e => e.value), showSymbol: false, lineStyle: { width: 2, color: '#f59e0b' }, areaStyle: { color: 'rgba(245,158,11,.08)' } }, { name: '买入持有', type: 'line', data: equity.map(e => e.bh), showSymbol: false, lineStyle: { width: 1.5, color: '#3b82f6', type: 'dashed' } }, ], }, true); } function renderTrades(trades) { if (!trades.length) { $('#tradeBox').innerHTML = '
该策略在回测期内无交易信号
'; return; } $('#tradeBox').innerHTML = ` ${trades.map(t => ``).join('')}
买入日期买入价卖出日期卖出价收益持有(天)信号
${t.entry_date}${t.entry_price} ${t.exit_date}${t.exit_price} ${fmtPct(t.return)} ${t.days} ${escapeHtml(t.reason)}
`; } window.addEventListener('resize', () => chart && chart.resize()); loadStrategies();