alphax/static/pipeline.html
2026-05-25 08:53:21 +08:00

81 lines
21 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 44px}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin-bottom:16px;flex-wrap:wrap}.page-head h1{font-size:26px;font-weight:900;letter-spacing:-.6px;color:var(--ink)}.page-head p{margin-top:4px;color:var(--stone);font-size:13px}.head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.select,.btn-lite,.page-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:800;color:var(--ink)}.btn-lite,.page-btn{cursor:pointer}.page-btn:disabled{opacity:.45;cursor:default}.pager{display:flex;align-items:center;gap:8px}.pager-info{font-size:12px;color:var(--stone);font-weight:800}.kpis{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:10px;margin-bottom:14px}.kpi{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);padding:12px;min-width:0}.kpi span{display:block;color:var(--stone);font-size:11px;font-weight:900}.kpi b{display:block;margin-top:6px;color:var(--ink);font-size:24px;line-height:1;font-weight:900;letter-spacing:-.5px}.kpi b.green{color:var(--green)}.kpi b.red{color:var(--red)}.kpi b.blue{color:var(--blue)}.layout{display:grid;grid-template-columns:420px minmax(0,1fr);gap:14px;align-items:start}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);min-width:0}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:900;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:800}.shorttf{margin:-2px 0 14px}.shorttf-body{display:grid;grid-template-columns:260px minmax(0,1fr);gap:12px;padding:14px}.shorttf-score{border:1px solid rgba(66,98,255,.16);background:linear-gradient(135deg,rgba(66,98,255,.08),rgba(0,180,115,.05));border-radius:var(--radius-md);padding:14px}.shorttf-score span{display:block;color:var(--stone);font-size:11px;font-weight:900}.shorttf-score b{display:block;margin-top:5px;color:var(--ink);font-size:28px;line-height:1;font-weight:900}.shorttf-score em{display:block;margin-top:7px;color:var(--slate);font-style:normal;font-size:12px;font-weight:800}.shorttf-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.shorttf-signal{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px}.shorttf-signal h3{font-size:12px;color:var(--ink);font-weight:900;margin-bottom:6px}.shorttf-signal p{font-size:11px;color:var(--stone);line-height:1.5}.shorttf-signal strong{color:var(--ink)}.run-list{display:flex;flex-direction:column;max-height:calc(100vh - 246px);overflow:auto}.run-row{display:grid;grid-template-columns:1fr auto;gap:10px;padding:12px 14px;border-bottom:1px solid var(--hairline-soft);cursor:pointer;background:var(--canvas);transition:.12s}.run-row:hover{background:var(--surface)}.run-row.active{background:rgba(66,98,255,.06);box-shadow:inset 3px 0 0 var(--blue)}.run-main{min-width:0}.run-time{font-size:13px;font-weight:900;color:var(--ink)}.run-sub{margin-top:4px;color:var(--stone);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.status{display:inline-flex;align-items:center;height:22px;border-radius:var(--radius-full);padding:0 8px;font-size:11px;font-weight:900;background:var(--surface);color:var(--slate);border:1px solid var(--hairline-soft)}.status.ok{background:var(--green-light);color:var(--green);border-color:rgba(0,180,115,.18)}.status.err{background:var(--red-light);color:var(--red);border-color:rgba(229,62,62,.18)}.funnel{grid-column:1/-1;display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:6px;margin-top:8px}.funnel div{background:var(--surface);border:1px solid var(--hairline-soft);border-radius:var(--radius-sm);padding:6px 7px;min-width:0}.funnel span{display:block;color:var(--stone);font-size:10px;font-weight:900}.funnel b{display:block;margin-top:2px;color:var(--ink);font-size:14px;font-weight:900}.detail{min-height:520px}.detail-body{padding:14px}.timeline{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin-bottom:14px}.stage{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px;min-width:0}.stage b{display:block;font-size:12px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stage span{display:block;margin-top:4px;color:var(--stone);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stage.ok{border-color:rgba(0,180,115,.18);background:rgba(0,180,115,.045)}.stage.err{border-color:rgba(229,62,62,.18);background:rgba(229,62,62,.045)}.chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}.chip{height:32px;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-full);padding:0 10px;font-size:12px;font-weight:900;color:var(--slate);cursor:pointer}.chip.active{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}.summary-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:12px}.mini{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:9px}.mini span{display:block;color:var(--stone);font-size:10px;font-weight:900}.mini b{display:block;margin-top:3px;font-size:16px;color:var(--ink)}.table-wrap{overflow:auto;border:1px solid var(--hairline-soft);border-radius:var(--radius-md)}.table{width:100%;border-collapse:collapse;min-width:760px}.table th,.table td{padding:9px 10px;border-bottom:1px solid var(--hairline-soft);text-align:left;font-size:12px;vertical-align:top}.table th{color:var(--stone);font-size:11px;font-weight:900;background:var(--surface);position:sticky;top:0}.table td{color:var(--slate)}.table tr:last-child td{border-bottom:0}.sym{font-weight:900;color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.num{font-weight:900;color:var(--ink)}.label{display:inline-flex;border-radius:var(--radius-full);padding:3px 8px;font-size:11px;font-weight:900;background:var(--surface);border:1px solid var(--hairline-soft);color:var(--slate);white-space:nowrap}.label.rec{background:rgba(66,98,255,.06);color:var(--blue);border-color:rgba(66,98,255,.15)}.label.win{background:var(--green-light);color:var(--green);border-color:rgba(0,180,115,.18)}.label.fail{background:var(--red-light);color:var(--red);border-color:rgba(229,62,62,.18)}.signals{max-width:320px;line-height:1.45}.empty{padding:38px 20px;text-align:center;color:var(--stone);font-size:13px}.loading{padding:22px;color:var(--stone);font-size:13px}.issue{color:var(--red);font-weight:800}.muted{color:var(--stone)}@media(max-width:1120px){.kpis{grid-template-columns:repeat(3,minmax(0,1fr))}.layout{grid-template-columns:1fr}.run-list{max-height:none}.timeline{grid-template-columns:repeat(3,minmax(0,1fr))}.shorttf-body{grid-template-columns:1fr}.shorttf-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.shell{width:min(100% - 24px,1320px);padding-top:18px}.kpis,.summary-grid,.shorttf-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.timeline{grid-template-columns:1fr}.funnel{grid-template-columns:repeat(2,minmax(0,1fr))}.page-head h1{font-size:22px}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="page-head">
<div><h1>链路日志</h1><p>按粗筛批次还原事件、粗筛、确认、推荐、跟踪与复盘结果。</p></div>
<div class="head-actions">
<select class="select" id="hoursSel" onchange="setHoursAndReload()"><option value="24">近 24h</option><option value="72">近 3 天</option><option value="168">近 7 天</option></select>
<button class="btn-lite" onclick="reloadRuns()">刷新</button>
</div>
</div>
<div class="kpis" id="kpis"><div class="loading">加载中...</div></div>
<section class="panel shorttf">
<div class="panel-head">
<div>
<div class="panel-title">短周期验证</div>
<div class="panel-note">5m / 15m 只做早期发现采样,不直接触发交易动作</div>
</div>
<div class="panel-note" id="shortTfWindow">--</div>
</div>
<div class="shorttf-body" id="shortTfBody"><div class="loading">加载短周期样本...</div></div>
</section>
<div class="layout">
<section class="panel">
<div class="panel-head">
<div>
<div class="panel-title">批次</div>
<div class="panel-note" id="runCount">--</div>
</div>
<div class="pager">
<button class="page-btn" id="prevPageBtn" onclick="changePage(-1)">上一页</button>
<button class="page-btn" id="nextPageBtn" onclick="changePage(1)">下一页</button>
<span class="pager-info" id="pageInfo">--</span>
</div>
</div>
<div class="run-list" id="runList"><div class="loading">加载批次...</div></div>
</section>
<section class="panel detail">
<div class="panel-head"><div class="panel-title" id="detailTitle">批次详情</div><div class="panel-note" id="detailWindow">--</div></div>
<div class="detail-body" id="detailBody"><div class="empty">请选择一个批次</div></div>
</section>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
var state = { runs: [], selected: null, detail: null, filter: 'all', page: 1, limit: 12, totalPages: 0, totalCount: 0, offset: 0, hours: 24 };
function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
function $(id){return document.getElementById(id);}
function uniq(arr){var seen={};return (arr||[]).filter(function(x){x=String(x||'').trim();if(!x||seen[x])return false;seen[x]=true;return true;});}
function friendlyReason(text){text=String(text||'').trim();if(text==='行情数据过旧')return 'Binance 近 36 小时没有有效更新,通常是停牌、下架遗留或极低活跃交易对';if(text==='交易对已停用/不可现货交易')return 'Binance 标记为非交易状态,已从交易宇宙剔除';return text;}
function fmtTime(t){if(!t)return'--';var d=new Date(t);if(isNaN(d.getTime()))return t;return (d.getMonth()+1)+'/'+d.getDate()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');}
function fmtDur(ms){ms=Number(ms||0);if(ms>=60000)return Math.round(ms/60000)+'m';if(ms>=1000)return Math.round(ms/100)/10+'s';return ms+'ms';}
function pct(a,b){return b?((Number(a||0)/Number(b||1))*100).toFixed(1)+'%':'--';}
function statusCls(s){return s==='success'?'ok':'err';}
function label(text,cls){return '<span class="label '+(cls||'')+'">'+esc(text)+'</span>';}
function updatePager(){var info=$('pageInfo');var prev=$('prevPageBtn');var next=$('nextPageBtn');if(!info||!prev||!next)return;var totalPages=state.totalPages||0;info.textContent=totalPages?('第 '+state.page+' / '+totalPages+' 页,共 '+state.totalCount+' 批'):'暂无数据';prev.disabled=state.page<=1;next.disabled=!totalPages||state.page>=totalPages;}
function renderKpis(k){var items=[['批次数',k.run_count||0,''],['Binance USDT',k.usdt_pair_count||0,'blue'],['可交易宇宙',k.tradable_universe_count||0,'blue'],['缓存过滤',k.cached_exclusion_count||0,''],['宇宙过滤',k.universe_gate_count||0,''],['K线成功',((k.kline_h1_success_rate||0)+'%'),'green'],['质量通过',k.quality_pass_count||0,'blue'],['推荐生成',k.recommendations||0,'green'],['复盘成功/失败',(k.perf_success||0)+' / '+(k.perf_failed||0),'red']];$('kpis').innerHTML=items.map(function(x){return '<div class="kpi"><span>'+x[0]+'</span><b class="'+x[2]+'">'+x[1]+'</b></div>';}).join('');}
function fmtPct(v){v=Number(v||0);return (v>0?'+':'')+v.toFixed(2)+'%';}
function renderShortTfReview(d){$('shortTfWindow').textContent='近 '+(d.hours||state.hours)+' 小时 · 样本 '+(d.total_samples||0);var summary=d.summary||[];if(!summary.length){$('shortTfBody').innerHTML='<div class="empty">还没有短周期样本。等调度跑几轮后,这里会显示 5m/15m 启动信号后续是否真的有价值。</div>';return;}var cards=summary.slice(0,3).map(function(x){return '<div class="shorttf-signal"><h3>'+esc(x.signal_label||x.signal_code)+'</h3><p>样本 <strong>'+esc(x.count||0)+'</strong> · 转推荐 <strong>'+esc(x.conversion_rate||0)+'%</strong> · 当前均值 <strong>'+fmtPct(x.avg_return_pct||0)+'</strong> · 胜率 <strong>'+esc(x.win_rate||0)+'%</strong></p></div>';}).join('');$('shortTfBody').innerHTML='<div class="shorttf-score"><span>证据池</span><b>'+esc(d.total_samples||0)+'</b><em>转推荐 '+esc(d.converted_count||0)+' 个 · 当前均值 '+fmtPct(d.avg_return_pct||0)+'</em><em>只有复盘表现稳定,短周期才允许被提权。</em></div><div class="shorttf-grid">'+cards+'</div>';}
async function loadShortTfReview(){try{var d=await (await fetch('/api/screening/short-tf-review?hours='+state.hours+'&limit=200')).json();renderShortTfReview(d||{});}catch(e){$('shortTfBody').innerHTML='<div class="empty">短周期验证加载失败</div>';}}
function renderRuns(){var list=state.runs||[];$('runCount').textContent='本页 '+list.length+' / 全部 '+state.totalCount+' 批';updatePager();if(!list.length){$('runList').innerHTML='<div class="empty">暂无粗筛批次</div>';return;}$('runList').innerHTML=list.map(function(r){var active=state.selected===r.run_id?' active':'';var notes=(r.issue_notes||[]).join(' / ')||'链路正常';return '<div class="run-row'+active+'" onclick="selectRun('+r.run_id+')"><div class="run-main"><div class="run-time">'+fmtTime(r.started_at)+'</div><div class="run-sub '+(r.issue_notes&&r.issue_notes.length?'issue':'')+'">'+esc(notes)+'</div></div><span class="status '+statusCls(r.run_status)+'">'+esc(r.result_status||r.run_status)+'</span><div class="funnel"><div><span>宇宙过滤</span><b>'+(r.universe_gate_count||0)+'</b></div><div><span>异动发现</span><b>'+(r.discovery_count||0)+'/'+(r.rough_candidates||0)+'</b></div><div><span>质量通过</span><b>'+(r.quality_pass_count||0)+'</b></div><div><span>质量淘汰</span><b>'+(r.quality_reject_count||0)+'</b></div><div><span>交易确认</span><b>'+(r.trade_confirm_count||0)+'/'+(r.confirm_processed||0)+'</b></div><div><span>推荐</span><b>'+(r.recommendations||0)+'</b></div></div></div>';}).join('');}
function resetSelection(){state.selected=null;state.detail=null;}
async function loadRuns(){try{$('runList').innerHTML='<div class="loading">加载批次...</div>';state.hours=Number($('hoursSel').value||24);var d=await (await fetch('/api/pipeline/runs?hours='+state.hours+'&limit='+state.limit+'&offset='+state.offset)).json();state.runs=d.runs||[];state.totalPages=(d.pagination&&d.pagination.total_pages)||0;state.totalCount=(d.pagination&&d.pagination.total_count)||0;state.page=(d.pagination&&d.pagination.page)||1;renderKpis(d.kpi||{});loadShortTfReview();if(state.selected&&!state.runs.some(function(r){return r.run_id===state.selected;}))resetSelection();if(!state.selected&&state.runs[0])state.selected=state.runs[0].run_id;renderRuns();if(state.selected)selectRun(state.selected,true);else $('detailBody').innerHTML='<div class="empty">暂无可展示批次</div>';}catch(e){$('runList').innerHTML='<div class="empty">加载失败</div>';updatePager();}}
async function selectRun(id,quiet){state.selected=id;renderRuns();$('detailBody').innerHTML='<div class="loading">加载详情...</div>';try{var d=await (await fetch('/api/pipeline/runs/'+id)).json();state.detail=d;state.filter='all';renderDetail();}catch(e){$('detailBody').innerHTML='<div class="empty">详情加载失败</div>';}}
function changePage(step){if(!state.totalPages)return;var next=state.page+step;if(next<1||next>state.totalPages)return;state.offset=(next-1)*state.limit;resetSelection();loadRuns();}
function setHoursAndReload(){state.offset=0;state.page=1;resetSelection();loadRuns();}
function renderDetail(){var d=state.detail||{},s=d.summary||{};$('detailTitle').textContent='批次 #'+(s.run_id||s.id||'--')+' · '+fmtTime(s.started_at);$('detailWindow').textContent=fmtTime(s.window_start)+' - '+fmtTime(s.window_end);var timeline=(d.timeline||[]).map(function(t){return '<div class="stage '+statusCls(t.run_status)+'"><b>'+esc(t.stage)+'</b><span>'+fmtTime(t.started_at)+' · '+fmtDur(t.duration_ms)+'</span><span>'+esc(t.result_status||t.run_status)+'</span></div>';}).join('')||'<div class="empty">暂无阶段日志</div>';var counts=d.stage_counts||{};var chips=[['all','全部',0],['universe_gate','宇宙过滤',counts.universe_gate||0],['discovery','异动发现',counts.discovery||0],['quality_filter','质量验证',(counts.quality_pass||0)+(counts.quality_reject||0)],['trade_confirm','交易确认',counts.trade_confirm||0],['tracking','跟踪',counts.tracking||0],['review','复盘',counts.review||0],['missed','漏选',counts.missed||0]];var chipHtml=chips.map(function(c){return '<button class="chip '+(state.filter===c[0]?'active':'')+'" onclick="setFilter(\''+c[0]+'\')">'+c[1]+(c[2]?(' '+c[2]):'')+'</button>';}).join('');var minis=[['覆盖漏斗',(s.usdt_pair_count||0)+' / '+(s.tradable_universe_count||0)+' / '+(s.kline_attempt_count||0)+' / '+(s.rough_candidates||0)],['缓存过滤',s.cached_exclusion_count||0],['K线成功',pct(s.kline_h1_success_count,s.kline_attempt_count)+' / '+pct(s.kline_h4_success_count,s.kline_attempt_count)],['流程漏斗',(s.rough_candidates||0)+' / '+(s.fine_qualified||0)+' / '+(s.confirm_hits||0)+' / '+(s.recommendations||0)],['推荐转化',pct(s.recommendations,s.fine_qualified)],['复盘成功/失败',(s.perf_success||0)+' / '+(s.perf_failed||0)],['漏选',s.missed_count||0]].map(function(x){return '<div class="mini"><span>'+x[0]+'</span><b>'+x[1]+'</b></div>';}).join('');$('detailBody').innerHTML='<div class="timeline">'+timeline+'</div><div class="summary-grid">'+minis+'</div><div class="chips">'+chipHtml+'</div>'+renderRows();}
function setFilter(f){state.filter=f;renderDetail();}
function rowStage(item){if(item.kind==='screening')return item.funnel_stage||item.layer||'discovery';if(item.kind==='recommendation')return (item.performance_status==='success'||item.performance_status==='failed')?'tracking':'trade_confirm';if(item.kind==='review')return 'review';if(item.kind==='missed')return 'discovery';return 'all';}
function collectRows(){var d=state.detail||{},rows=[];(d.screening_items||[]).forEach(function(x){rows.push({kind:'screening',layer:x.layer,funnel_stage:x.funnel_stage,time:x.scan_time,symbol:x.symbol,stage:x.candidate_stage_label||x.funnel_stage_label||x.stage_label,score:x.score,price:x.price,status:x.state,signals:x.signals||[],note:(x.detail_json&&x.detail_json.reason_label)||(x.detail_json&&x.detail_json.reason)||'',stage_code:x.candidate_stage});});(d.recommendations||[]).forEach(function(x){rows.push({kind:'recommendation',time:x.rec_time,symbol:x.symbol,stage:x.stage_label||'交易推荐',score:x.rec_score,price:x.entry_price,status:x.execution_label||x.action_status||x.status,signals:x.signal_labels&&x.signal_labels.length?x.signal_labels:(x.signals||[]),note:x.execution_reason||x.state_reason||'',performance_status:x.performance_status});});(d.reviews||[]).forEach(function(x){rows.push({kind:'review',time:x.review_time,symbol:x.symbol,stage:x.outcome==='爆发'?'复盘命中':(x.outcome==='失败'?'复盘失败':'复盘未验证'),score:'',price:x.pnl_48h,status:x.outcome,signals:x.hit_signals&&x.hit_signals.length?x.hit_signals:(x.triggered_signals||[]),note:x.lesson||''});});(d.missed_explosions||[]).forEach(function(x){rows.push({kind:'missed',time:x.detect_time,symbol:x.symbol,stage:'漏选',score:'',price:x.gain_pct,status:x.reason_missed||'漏选',signals:[],note:x.lesson||''});});rows.sort(function(a,b){return String(a.time||'').localeCompare(String(b.time||''));});return rows;}
function renderRows(){var rows=collectRows().filter(function(r){return state.filter==='all'||rowStage(r)===state.filter||(state.filter==='review'&&r.kind==='review');});if(!rows.length)return '<div class="empty">当前阶段暂无明细</div>';return '<div class="table-wrap"><table class="table"><thead><tr><th>时间</th><th>阶段</th><th>币种</th><th>分数/收益</th><th>价格/涨幅</th><th>状态</th><th>信号与说明</th></tr></thead><tbody>'+rows.map(function(r){var cls=r.kind==='recommendation'?'rec':r.stage==='复盘命中'?'win':(r.stage==='复盘失败'||r.kind==='missed')?'fail':'';var parts=uniq((r.signals||[]).slice(0,5).concat([r.note]));var desc=parts.map(friendlyReason).join(' · ');return '<tr><td>'+fmtTime(r.time)+'</td><td>'+label(r.stage,cls)+'</td><td class="sym">'+esc(r.symbol||'--')+'</td><td class="num">'+esc(r.score===''?'--':r.score)+'</td><td class="num">'+esc(r.price==null?'--':r.price)+'</td><td>'+esc(r.status||'--')+'</td><td class="signals">'+esc(desc||'--')+'</td></tr>';}).join('')+'</tbody></table></div>';}
loadRuns();
</script>
{% endblock %}