alphax/static/review_center.html
2026-05-23 14:59:57 +08:00

68 lines
17 KiB
HTML

{% extends "base.html" %}
{% block title %}复盘中心 — AlphaX Agent{% endblock %}
{% block extra_head_css %}
<style>
.shell{width:min(100% - 40px,1320px);margin:0 auto;padding:24px 0 48px}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:16px}.page-head h1{font-size:28px;font-weight:950;color:var(--ink);letter-spacing:0}.page-head p{margin-top:5px;color:var(--stone);font-size:13px;line-height:1.55;max-width:840px}.actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.select,.btn{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:850;color:var(--ink)}.btn{cursor:pointer}.strategy-digest{border:1px solid rgba(28,28,30,.1);background:linear-gradient(135deg,#fffdf7 0%,#f7fbff 56%,#f8fff8 100%);border-radius:18px;padding:16px;margin-bottom:14px;box-shadow:0 18px 50px rgba(15,23,42,.06)}.digest-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:12px}.digest-title{font-size:18px;font-weight:950;color:var(--ink)}.digest-sub{margin-top:4px;font-size:12px;color:var(--stone);line-height:1.55}.digest-grid{display:grid;grid-template-columns:1.2fr 1fr 1fr 1fr;gap:10px}.digest-card{border:1px solid var(--hairline-soft);background:rgba(255,255,255,.72);border-radius:14px;padding:12px;min-width:0}.digest-card h3{font-size:12px;font-weight:950;color:var(--ink);margin-bottom:8px}.digest-item{border-top:1px solid var(--hairline-soft);padding:8px 0}.digest-item:first-of-type{border-top:0}.digest-item b{display:block;font-size:12px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.digest-item span{display:block;margin-top:3px;font-size:11px;color:var(--stone);line-height:1.45}.digest-empty{font-size:12px;color:var(--stone);padding:10px 0}.principles{display:none}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden;min-width:0}.panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:15px;font-weight:950;color:var(--ink)}.panel-desc{margin-top:4px;color:var(--stone);font-size:11px;line-height:1.45;max-width:720px}.panel-body{padding:14px}.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:12px}.kpi{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px;min-width:0}.kpi span{display:block;color:var(--stone);font-size:10px;font-weight:950}.kpi b{display:block;margin-top:5px;color:var(--ink);font-size:20px;line-height:1;font-weight:950;letter-spacing:0}.kpi b.green{color:var(--green)}.kpi b.red{color:var(--red)}.kpi b.blue{color:var(--blue)}.list{display:grid;gap:8px}.row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:9px 10px}.row-main{min-width:0}.row-title{font-size:12px;font-weight:950;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.row-sub{margin-top:3px;color:var(--stone);font-size:11px;line-height:1.45;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.value{font-size:12px;font-weight:950;color:var(--slate);white-space:nowrap}.value.green{color:var(--green)}.value.red{color:var(--red)}.split{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}.mini-title{font-size:12px;font-weight:950;color:var(--ink);margin-bottom:8px}.empty,.loading{padding:30px 14px;text-align:center;color:var(--stone);font-size:13px}.badge{display:inline-flex;align-items:center;height:24px;border-radius:999px;border:1px solid var(--hairline-soft);background:var(--surface);padding:0 8px;font-size:11px;font-weight:900;color:var(--slate);white-space:nowrap}.badge.ok{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.warn{background:var(--yellow-light);border-color:rgba(252,185,0,.22);color:var(--yellow-dark)}.badge.bad{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}.full{grid-column:1/-1}@media(max-width:1100px){.grid,.principles,.digest-grid{grid-template-columns:1fr}.split{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.shell{width:min(100% - 24px,1320px)}.page-head h1{font-size:22px}.kpis{grid-template-columns:1fr}.digest-head{display:block}.digest-head .badge{margin-top:8px}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="page-head">
<div>
<h1>复盘中心</h1>
<p>把机会发现、策略交易收益、多源证据和策略迭代拆开看。收益只看策略交易;机会归档只看发现和确认质量。</p>
</div>
<div class="actions">
<select class="select" id="daysSel" onchange="loadAll()">
<option value="7">近 7 天</option>
<option value="30" selected>近 30 天</option>
<option value="60">近 60 天</option>
<option value="120">近 120 天</option>
</select>
<button class="btn" onclick="loadAll()">刷新</button>
</div>
</div>
<section class="strategy-digest" id="strategyDigest"><div class="loading">加载中...</div></section>
<div class="principles" id="principles"><div class="loading">加载中...</div></div>
<div class="grid">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">机会复盘</div><div class="panel-desc" id="oppDef">--</div></div><span class="badge warn">不算收益</span></div>
<div class="panel-body" id="opportunityPanel"><div class="loading">加载中...</div></div>
</section>
<section class="panel">
<div class="panel-head"><div><div class="panel-title">策略交易复盘</div><div class="panel-desc" id="paperDef">--</div></div><span class="badge ok">唯一收益口径</span></div>
<div class="panel-body" id="paperPanel"><div class="loading">加载中...</div></div>
</section>
<section class="panel">
<div class="panel-head"><div><div class="panel-title">多源证据归因</div><div class="panel-desc" id="evidenceDef">--</div></div><span class="badge warn">只做证据</span></div>
<div class="panel-body" id="evidencePanel"><div class="loading">加载中...</div></div>
</section>
<section class="panel">
<div class="panel-head"><div><div class="panel-title">策略迭代闸门</div><div class="panel-desc" id="iterationDef">--</div></div><span class="badge warn">候选假设</span></div>
<div class="panel-body" id="iterationPanel"><div class="loading">加载中...</div></div>
</section>
<section class="panel full">
<div class="panel-head"><div><div class="panel-title">最近关键样本</div><div class="panel-desc">方便快速定位是机会问题、执行问题,还是证据源问题。</div></div></div>
<div class="panel-body" id="recentPanel"><div class="loading">加载中...</div></div>
</section>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
var API='';function $(id){return document.getElementById(id)}function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]})}function num(v,d){return Number(v||0).toFixed(d==null?0:d)}function pct(v){return (Number(v||0)>=0?'+':'')+Number(v||0).toFixed(2)+'%'}function usd(v){v=Number(v||0);return (v>=0?'+':'-')+'$'+Math.abs(v).toFixed(2)}function time(t){if(!t)return '--';var d=new Date(t);if(isNaN(d.getTime()))return t;return (d.getMonth()+1)+'/'+d.getDate()+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)}
function kpis(items){return '<div class="kpis">'+items.map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('')+'</div>'}
function rows(items,label,value,sub){items=items||[];if(!items.length)return '<div class="empty">暂无数据</div>';return '<div class="list">'+items.map(function(x){return '<div class="row"><div class="row-main"><div class="row-title">'+esc(label(x))+'</div><div class="row-sub">'+esc(sub?sub(x):'')+'</div></div><div class="value">'+esc(value?value(x):'')+'</div></div>'}).join('')+'</div>'}
function digestItems(items,label,sub){items=items||[];if(!items.length)return '<div class="digest-empty">暂无动作</div>';return items.slice(0,4).map(function(x){return '<div class="digest-item"><b>'+esc(label(x))+'</b><span>'+esc(sub?sub(x):'')+'</span></div>'}).join('')}
function renderStrategyDigest(d){var it=d.iteration||{},dig=it.digest||{},latest=dig.latest||{},m=latest.metrics||{},decision=latest.decision||'hold';var badgeCls=decision==='release'?'ok':decision==='gray'?'warn':'warn';$('strategyDigest').innerHTML='<div class="digest-head"><div><div class="digest-title">策略迭代摘要</div><div class="digest-sub">'+esc(latest.title||'暂无复盘')+' · '+time(latest.time)+' · 版本 '+esc(latest.strategy_version||'--')+'</div></div><span class="badge '+badgeCls+'">'+esc(decision)+'</span></div><div class="kpis">'+[['因子生效调整',m.factor_weight_updates||0,'blue'],['候选 / 灰度',(it.summary&&it.summary.candidate_count||0)+' / '+(it.summary&&it.summary.gray_count||0),''],['本轮有效复盘',m.effective_review_count||0,''],['发布状态',latest.reason||'继续观察','']].map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('')+'</div><div class="digest-grid"><div class="digest-card"><h3>升权了什么</h3>'+digestItems(dig.upgraded,function(x){return x.signal||'--'},function(x){return '权重 '+x.old_weight+' → '+x.new_weight+' · 样本 '+(x.sample_size||0)+' · 命中 '+(x.hit_rate||0)+'%'})+'</div><div class="digest-card"><h3>降权 / 淘汰</h3>'+digestItems(dig.downgraded,function(x){return x.signal||'--'},function(x){return (x.action||'调整')+' · '+(x.old_weight!=null?'权重 '+x.old_weight+' → '+x.new_weight:x.detail||'')})+'</div><div class="digest-card"><h3>灰度观察</h3>'+digestItems(dig.gray,function(x){return x.signal||('候选 #'+x.id)},function(x){return '样本 '+(x.sample_size||0)+' · 置信 '+(x.confidence||0)+'% · '+(x.description||'')})+'</div><div class="digest-card"><h3>最近迭代</h3>'+digestItems(dig.timeline,function(x){return time(x.time)+' · '+(x.decision||'hold')},function(x){return x.reason||x.title||''})+'</div></div>'}
function renderOpportunity(o){var s=o.summary||{};$('oppDef').textContent=o.definition||'';$('opportunityPanel').innerHTML=kpis([['机会样本',s.total_opportunities||0,'blue'],['可买/等回踩',(s.buy_now_count||0)+' / '+(s.wait_pullback_count||0),''],['策略执行',s.paper_executed_count||0,'green'],['漏选爆发',s.missed_explosion_count||0,'red'],['有效复盘',s.effective_review_count||0,''],['机会命中',num(s.opportunity_hit_rate,1)+'%','green'],['观察样本',s.observe_count||0,''],['失效样本',s.invalid_count||0,'red']])+'<div class="split"><div><div class="mini-title">状态分布</div>'+rows(o.status_distribution,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">复盘结果</div>'+rows(o.outcome_distribution,function(x){return x.name||'--'},function(x){return x.count})+'</div></div>'}
function renderPaper(p){var s=p.summary||{},ta=p.trade_attribution||{},wo=p.watch_order_attribution||{},tf=ta.factor||[],te=ta.entry_path||[],tg=ta.factor_group||[],wr=wo.watch_pool||[],orows=wo.paper_orders||[];$('paperDef').textContent=p.definition||'';$('paperPanel').innerHTML=kpis([['当前余额','$'+num(s.current_balance_usdt,2),'blue'],['总收益',usd(s.total_pnl_usdt),Number(s.total_pnl_usdt||0)>=0?'green':'red'],['账户收益率',pct(s.account_total_return_pct),Number(s.account_total_return_pct||0)>=0?'green':'red'],['胜率',num(s.win_rate,1)+'%',''],['开仓/平仓',(s.open_count||0)+' / '+(s.closed_count||0),''],['已实现',usd(s.realized_pnl_usdt),Number(s.realized_pnl_usdt||0)>=0?'green':'red'],['未实现',usd(s.open_unrealized_pnl_usdt),Number(s.open_unrealized_pnl_usdt||0)>=0?'green':'red'],['累计杠杆',num(s.cumulative_leverage,2)+'x','']])+'<div class="split"><div><div class="mini-title">退出原因</div>'+rows(p.exit_reasons,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">执行事件</div>'+rows(p.event_types,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">真实交易因子</div>'+rows(tf.slice(0,6),function(x){return x.factor||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'% · 均值 '+pct(x.avg_realized_pnl_pct)})+'</div><div><div class="mini-title">因子组表现</div>'+rows(tg.slice(0,6),function(x){return x.factor_group||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div><div><div class="mini-title">入场路径表现</div>'+rows(te.slice(0,6),function(x){return x.entry_path||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div><div><div class="mini-title">观察/挂单推进</div>'+rows(wr.concat(orows).slice(0,6),function(x){return x.watch_bucket||x.order_bucket||'--'},function(x){return (x.executed_pct!=null?num(x.executed_pct,1):num(x.fill_pct,1))+'%'},function(x){return '样本 '+(x.opportunity_count||x.order_count||0)+' · 执行/成交 '+(x.executed_count||x.filled_count||0)})+'</div></div>'}
function renderEvidence(e){var s=e.summary||{};$('evidenceDef').textContent=e.definition||'';$('evidencePanel').innerHTML=kpis([['新闻事件',s.news_count||0,'blue'],['有效舆情',s.actionable_news_count||0,''],['链上信号',s.onchain_signal_count||0,'blue'],['高置信链上',s.high_confidence_onchain_count||0,'green'],['原始链上',s.raw_onchain_count||0,''],['已映射原始',s.mapped_raw_onchain_count||0,'green'],['LLM 调用',s.llm_runs||0,''],['LLM 成功',s.llm_success_count||0,'green']])+'<div class="split"><div><div class="mini-title">链上信号</div>'+rows(e.onchain_signals,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">舆情决策</div>'+rows(e.news_decisions,function(x){return x.name||'未处理'},function(x){return x.count})+'</div></div>'}
function renderIteration(i){var s=i.summary||{};$('iterationDef').textContent=i.definition||'';$('iterationPanel').innerHTML=kpis([['迭代记录',s.iteration_count||0,'blue'],['候选规则',s.candidate_count||0,''],['灰度规则',s.gray_count||0,'green'],['生效规则',s.active_count||0,'green']])+'<div class="row" style="margin-bottom:10px"><div class="row-main"><div class="row-title">最新发布结论:'+esc(s.latest_release_decision||'hold')+'</div><div class="row-sub">'+esc(s.latest_release_reason||'暂无发布说明')+'</div></div><div class="value">闸门</div></div><div class="split"><div><div class="mini-title">发布决策</div>'+rows(i.release_decisions,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">候选状态</div>'+rows(i.candidate_status,function(x){return x.name||'--'},function(x){return x.count})+'</div></div>'}
function renderRecent(d){var opp=(d.opportunity&&d.opportunity.missed_explosions)||[], trades=(d.paper_trading&&d.paper_trading.recent_trades)||[], news=(d.evidence&&d.evidence.recent_news)||[], chain=(d.evidence&&d.evidence.recent_onchain)||[];$('recentPanel').innerHTML='<div class="split"><div><div class="mini-title">最近策略交易</div>'+rows(trades.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.status||'--')},function(x){return x.status==='closed'?pct(x.realized_pnl_pct):pct(x.pnl_pct)},function(x){return time(x.opened_at)+' · '+(x.exit_reason||x.source_status||'')})+'</div><div><div class="mini-title">漏选爆发</div>'+rows(opp.slice(0,8),function(x){return x.symbol||'--'},function(x){return pct(x.gain_pct)},function(x){return time(x.detect_time)+' · '+(x.reason_missed||'')})+'</div><div><div class="mini-title">舆情事件</div>'+rows(news.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.title||'--')},function(x){return x.importance||'--'},function(x){return time(x.detected_at)+' · '+(x.source||'')+' · '+(x.decision||'未处理')})+'</div><div><div class="mini-title">链上信号</div>'+rows(chain.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.signal_label||x.signal_code||'--')},function(x){return x.severity||x.confidence||'--'},function(x){return time(x.detected_at)+' · '+(x.source||'')+' · '+(x.direction||'')})+'</div></div>'}
function render(d){renderStrategyDigest(d);$('principles').innerHTML=(d.principles||[]).map(function(x){return '<div class="principle">'+esc(x)+'</div>'}).join('');renderOpportunity(d.opportunity||{});renderPaper(d.paper_trading||{});renderEvidence(d.evidence||{});renderIteration(d.iteration||{});renderRecent(d)}
async function loadAll(){try{var days=$('daysSel').value;var d=await (await fetch(API+'/api/review-center/dashboard?days='+days+'&_ts='+Date.now(),{cache:'no-store'})).json();render(d)}catch(e){['strategyDigest','principles','opportunityPanel','paperPanel','evidencePanel','iterationPanel','recentPanel'].forEach(function(id){$(id).innerHTML='<div class="empty">加载失败</div>'})}}
loadAll();
</script>
{% endblock %}