Files
stock-advisor/static/js/strategies.js
T

149 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 量化策略回测页 */
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) =>
`<button class="pill ${i === 0 ? 'active' : ''}" data-k="${s.key}">${s.icon} ${s.name} <span style="color:var(--muted);font-size:11px">${s.params}</span></button>`
).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 = '<div class="empty">加载失败</div>';
}
}
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 = `<b style="color:var(--text)">${cfg.icon} ${cfg.name}${cfg.params}</b>${escapeHtml(cfg.desc)} <span class="tag">${cfg.tags.join(' / ')}</span>`;
const s = cfg.summary || {};
$('#stratSummary').innerHTML = `
<div class="kpi"><div class="kpi-label">全市场平均收益</div><div class="kpi-value num ${s.avg_return >= 0 ? 'up' : 'down'}">${fmtPct(s.avg_return)}</div></div>
<div class="kpi"><div class="kpi-label">平均夏普</div><div class="kpi-value num">${s.avg_sharpe ?? '--'}</div></div>
<div class="kpi"><div class="kpi-label">平均胜率</div><div class="kpi-value num">${s.avg_win_rate ?? '--'}%</div></div>
<div class="kpi"><div class="kpi-label">覆盖股票</div><div class="kpi-value num">${s.n ?? '--'}</div></div>
<div class="kpi"><div class="kpi-label">最优标的</div><div class="kpi-value" style="font-size:15px">${s.best_name ? `<a href="/stock/${s.best_code}" target="_blank">${s.best_name}</a> <span class="num up">${fmtPct(s.best_return)}</span>` : '--'}</div></div>`;
}
async function loadMarket(key) {
$('#rankTb').innerHTML = '<tr><td colspan="10" class="loading">加载中…</td></tr>';
try {
const d = await api('/api/backtest/market?strategy=' + key);
rankData = d.items || [];
renderRank();
// 默认选中第一名股票
if (rankData.length) loadDetail(rankData[0].code);
else $('#rankTb').innerHTML = '<tr><td colspan="10" class="empty">暂无回测数据,请到数据管理页重建</td></tr>';
} catch (e) {
$('#rankTb').innerHTML = '<tr><td colspan="10" class="empty">加载失败</td></tr>';
}
}
function renderRank() {
$('#rankHint').textContent = `${rankData.length} 只股票)`;
$('#rankTb').innerHTML = rankData.map((it, i) => `
<tr class="row-link" onclick="loadDetail('${it.code}')">
<td class="num">${i + 1}</td>
<td><b>${it.name}</b><div style="color:var(--muted);font-size:12px">${it.code}</div></td>
<td class="num ${pctClass(it.total_return)}"><b>${fmtPct(it.total_return)}</b></td>
<td class="num ${pctClass(it.benchmark)}">${fmtPct(it.benchmark)}</td>
<td class="num ${pctClass(it.excess)}">${fmtPct(it.excess)}</td>
<td class="num down">${fmtPct(it.max_drawdown)}</td>
<td class="num ${it.sharpe >= 0 ? 'up' : 'down'}">${it.sharpe}</td>
<td class="num">${it.win_rate}%</td>
<td class="num">${it.profit_factor}</td>
<td class="num">${it.trades}</td>
</tr>`).join('');
// 同步下拉框
fillStockSelect();
}
function fillStockSelect() {
const sel = $('#btStock');
sel.innerHTML = rankData.map(it => `<option value="${it.code}">${it.name}${it.code}</option>`).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 = '<div class="empty">加载失败</div>';
}
}
function renderDetail(d) {
const m = d.metrics, st = d.stock || {};
$('#btHead').innerHTML = `
<div class="flex between wrap">
<div>
<b style="font-size:16px">${d.stock_name}</b> <span style="color:var(--muted)">${d.code} · ${st.industry || ''} · ${st.board || ''}</span>
</div>
<span style="color:var(--muted);font-size:12px">${d.config.icon} ${d.config.name} · 回测 ${m.days} 个交易日</span>
</div>`;
$('#btMetrics').innerHTML = `
<div class="kpi"><div class="kpi-label">策略收益</div><div class="kpi-value num ${pctClass(m.total_return)}">${fmtPct(m.total_return)}</div></div>
<div class="kpi"><div class="kpi-label">年化收益</div><div class="kpi-value num ${pctClass(m.annualized)}">${fmtPct(m.annualized)}</div></div>
<div class="kpi"><div class="kpi-label">最大回撤</div><div class="kpi-value num down">${fmtPct(m.max_drawdown)}</div></div>
<div class="kpi"><div class="kpi-label">夏普比率</div><div class="kpi-value num ${m.sharpe >= 0 ? 'up' : 'down'}">${m.sharpe}</div></div>
<div class="kpi"><div class="kpi-label">胜率</div><div class="kpi-value num">${m.win_rate}%</div></div>
<div class="kpi"><div class="kpi-label">盈亏比</div><div class="kpi-value num">${m.profit_factor}</div></div>
<div class="kpi"><div class="kpi-label">交易次数</div><div class="kpi-value num">${m.trades}</div></div>
<div class="kpi"><div class="kpi-label">买入持有</div><div class="kpi-value num ${pctClass(m.benchmark)}">${fmtPct(m.benchmark)}</div></div>
<div class="kpi"><div class="kpi-label">超额收益</div><div class="kpi-value num ${pctClass(m.excess)}">${fmtPct(m.excess)}</div></div>`;
renderEquity(d.equity);
renderTrades(d.trades);
}
function renderEquity(equity) {
const el = $('#equityChart');
if (!window.echarts) { el.innerHTML = '<div class="empty">ECharts 加载失败</div>'; 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 = '<div class="empty">该策略在回测期内无交易信号</div>'; return; }
$('#tradeBox').innerHTML = `<table>
<tr><th>买入日期</th><th>买入价</th><th>卖出日期</th><th>卖出价</th><th>收益</th><th>持有(天)</th><th>信号</th></tr>
${trades.map(t => `<tr>
<td>${t.entry_date}</td><td class="num">${t.entry_price}</td>
<td>${t.exit_date}</td><td class="num">${t.exit_price}</td>
<td class="num ${pctClass(t.return)}"><b>${fmtPct(t.return)}</b></td>
<td class="num">${t.days}</td>
<td style="max-width:200px;white-space:normal;font-size:12px;color:var(--text2)">${escapeHtml(t.reason)}</td>
</tr>`).join('')}
</table>`;
}
window.addEventListener('resize', () => chart && chart.resize());
loadStrategies();