feat: 策略研究Tab全流程回测框架 + API端点 + 前端渲染 + 盘中大跌分析日志
This commit is contained in:
@@ -70,6 +70,7 @@ body { background: #0f0f13; color: #e2e8f0; }
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="signals">🔍 信号</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="health">🏥 健康</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="principles">📐 开发原则</button>
|
||||
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="research">📋 研究</button>
|
||||
<a href="/upload" class="tab-btn px-4 py-2 text-sm rounded-lg" style="text-decoration:none;color:#fbbf24;border-color:#fbbf24">📸 上传</a>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +83,7 @@ body { background: #0f0f13; color: #e2e8f0; }
|
||||
<div id="tab-market" class="tab-content hidden"></div>
|
||||
<div id="tab-signals" class="tab-content hidden"></div>
|
||||
<div id="tab-health" class="tab-content hidden"></div>
|
||||
<div id="tab-research" class="tab-content hidden"></div>
|
||||
<div id="tab-principles" class="tab-content hidden">
|
||||
<div class="flex gap-1 mb-4 bg-slate-900/50 rounded-xl p-1 border border-slate-800/50" id="princBar">
|
||||
<button class="princ-btn active px-4 py-1.5 text-sm rounded-lg" data-princ="spec">G 规范</button>
|
||||
@@ -258,6 +260,7 @@ function renderTab(name) {
|
||||
else if (name === 'health') renderHealth();
|
||||
else if (name === 'principles') renderPrinciples();
|
||||
else if (name === 'watch') renderWatch();
|
||||
else if (name === 'research') renderResearch();
|
||||
}
|
||||
|
||||
// ── Data Fetching ──
|
||||
@@ -1917,6 +1920,88 @@ function inMd(t) {
|
||||
.replace(/`([^`]+)`/g, '<code class="bg-slate-900 px-1 rounded text-xs font-mono">$1</code>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-[#58a6ff]" target="_blank">$1</a>');
|
||||
}
|
||||
|
||||
function renderResearch() {
|
||||
const el = document.getElementById('tab-research');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="flex gap-2 items-center mb-4"><h2 class="text-lg font-bold">📋 策略研究 — 全流程回测</h2>' +
|
||||
'<select id="btPeriod" class="bg-slate-800 text-sm rounded-lg px-2 py-1 border border-slate-700" onchange="runBacktest()">' +
|
||||
'<option value="1m">近 1 个月</option>' +
|
||||
'<option value="6m" selected>近 6 个月</option>' +
|
||||
'<option value="1y">近 1 年</option>' +
|
||||
'<option value="2y">近 2 年</option>' +
|
||||
'</select>' +
|
||||
'<button onclick="runBacktest()" class="px-3 py-1 bg-blue-600 hover:bg-blue-500 rounded-lg text-sm">▶ 运行</button></div>' +
|
||||
'<div id="btResults" class="text-slate-400 text-sm">点击「运行」开始回测验证</div>';
|
||||
window.btEl = el;
|
||||
}
|
||||
|
||||
async function runBacktest() {
|
||||
const period = document.getElementById('btPeriod')?.value || '6m';
|
||||
const resultsEl = document.getElementById('btResults');
|
||||
if (!resultsEl) return;
|
||||
resultsEl.innerHTML = '<div class="text-blue-400 animate-pulse">⏳ 回测运行中(约 15-30 秒)...</div>';
|
||||
try {
|
||||
const resp = await fetch('/api/research/backtest?period=' + period);
|
||||
const data = await resp.json();
|
||||
if (data.error) { resultsEl.innerHTML = '<div class="text-red-400">错误: ' + data.error + '</div>'; return; }
|
||||
displayBacktestResults(data);
|
||||
} catch(e) {
|
||||
resultsEl.innerHTML = '<div class="text-red-400">请求失败: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayBacktestResults(data) {
|
||||
const el = document.getElementById('btResults');
|
||||
if (!el) return;
|
||||
const s = data.summary || {};
|
||||
const winRate = s.win_rate || 0;
|
||||
const winRateClass = winRate >= 50 ? 'text-green-400' : winRate >= 40 ? 'text-yellow-400' : 'text-red-400';
|
||||
const sharpeClass = (s.sharpe_ratio || 0) >= 1 ? 'text-green-400' : (s.sharpe_ratio || 0) >= 0 ? 'text-yellow-400' : 'text-red-400';
|
||||
|
||||
let html = '<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">' +
|
||||
'<div class="bg-slate-800/50 rounded-lg p-3"><div class="text-xs text-slate-500">测试周期</div><div class="text-sm font-mono">' + (data.period || '') + '</div></div>' +
|
||||
'<div class="bg-slate-800/50 rounded-lg p-3"><div class="text-xs text-slate-500">初始资金</div><div class="text-sm font-mono">¥' + (s.capital_end ? (data.capital || 1000000).toLocaleString() : '100万') + '</div></div>' +
|
||||
'<div class="bg-slate-800/50 rounded-lg p-3"><div class="text-xs text-slate-500">最终资金</div><div class="text-sm font-mono">¥' + (s.capital_end ? s.capital_end.toLocaleString() : '-') + '</div></div>' +
|
||||
'<div class="bg-slate-800/50 rounded-lg p-3"><div class="text-xs text-slate-500">总交易</div><div class="text-sm font-mono">' + (s.total_trades || 0) + ' 笔</div></div>' +
|
||||
'<div class="bg-slate-800/50 rounded-lg p-3"><div class="text-xs text-slate-500">胜率</div><div class="text-lg font-bold ' + winRateClass + '">' + s.win_rate + '%</div></div>' +
|
||||
'<div class="bg-slate-800/50 rounded-lg p-3"><div class="text-xs text-slate-500">平均收益</div><div class="text-lg font-bold ' + ((s.avg_profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400') + '">' + (s.avg_profit_pct || 0) + '%</div></div>' +
|
||||
'<div class="bg-slate-800/50 rounded-lg p-3"><div class="text-xs text-slate-500">夏普比率</div><div class="text-lg font-bold ' + sharpeClass + '">' + (s.sharpe_ratio || 0).toFixed(2) + '</div></div>' +
|
||||
'<div class="bg-slate-800/50 rounded-lg p-3"><div class="text-xs text-slate-500">最大回撤</div><div class="text-lg font-bold text-red-400">' + (s.max_drawdown_pct || 0).toFixed(1) + '%</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="grid grid-cols-4 gap-2 mb-4 text-xs">' +
|
||||
'<div class="bg-green-900/20 rounded p-2"><span class="text-green-400">✅ 盈利: ' + (s.wins || 0) + '</span> <span class="text-slate-500">均+' + (s.avg_win_pct || 0).toFixed(1) + '%</span></div>' +
|
||||
'<div class="bg-red-900/20 rounded p-2"><span class="text-red-400">❌ 亏损: ' + (s.losses || 0) + '</span> <span class="text-slate-500">均' + (s.avg_loss_pct || 0).toFixed(1) + '%</span></div>' +
|
||||
'<div class="bg-slate-800/30 rounded p-2"><span class="text-yellow-400">📊 盈亏比: ' + (s.profit_factor || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="bg-slate-800/30 rounded p-2"><span class="text-slate-300">📈 筛选: ' + (data.total_stocks_screened || 0) + ' 只</span></div>' +
|
||||
'</div>';
|
||||
|
||||
// Trade list
|
||||
const trades = data.trades || [];
|
||||
html += '<div class="mb-2 text-sm font-bold text-slate-300">交易明细(前 50 笔)</div>' +
|
||||
'<div class="overflow-x-auto"><table class="w-full text-xs"><thead><tr class="text-slate-500 border-b border-slate-700">' +
|
||||
'<th class="text-left px-2 py-1">代码</th><th class="text-left px-2 py-1">名称</th><th class="text-left px-2 py-1">买入日</th>' +
|
||||
'<th class="text-right px-2 py-1">买入价</th><th class="text-right px-2 py-1">卖出价</th><th class="text-right px-2 py-1">收益</th>' +
|
||||
'<th class="text-center px-2 py-1">结果</th><th class="text-right px-2 py-1">持仓</th><th class="text-right px-2 py-1">评分</th></tr></thead><tbody>';
|
||||
for (const t of trades) {
|
||||
const pnlClass = (t.profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400';
|
||||
const reasonMap = { 'target': '🎯 止盈', 'stop': '🛑 止损', 'keep': '⏳ 持仓中' };
|
||||
html += '<tr class="border-b border-slate-800/50 hover:bg-slate-800/30">' +
|
||||
'<td class="px-2 py-1 font-mono">' + (t.code || '') + '</td>' +
|
||||
'<td class="px-2 py-1">' + (t.name || '') + '</td>' +
|
||||
'<td class="px-2 py-1 font-mono">' + (t.entry_date || '') + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono">' + (t.entry_price || 0).toFixed(2) + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono">' + (t.exit_price || 0).toFixed(2) + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono ' + pnlClass + '">' + (t.profit_pct || 0).toFixed(1) + '%</td>' +
|
||||
'<td class="text-center px-2 py-1">' + (reasonMap[t.exit_reason] || t.exit_reason || '') + '</td>' +
|
||||
'<td class="text-right px-2 py-1">' + (t.hold_days || 0) + 'd</td>' +
|
||||
'<td class="text-right px-2 py-1">' + (t.score || 0) + '</td></tr>';
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user