alphax/static/opportunity_detail.html
2026-05-28 01:05:11 +08:00

44 lines
18 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}.topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px}.back{height:36px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:999px;padding:0 12px;font-size:13px;font-weight:900;color:var(--ink);cursor:pointer}.hero{border:1px solid var(--hairline-soft);background:linear-gradient(135deg,#fffdf7 0%,#f7fbff 58%,#f7fff9 100%);border-radius:18px;padding:18px;margin-bottom:14px}.hero-main{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;flex-wrap:wrap}.symbol{font-size:32px;font-weight:950;letter-spacing:-1px;color:var(--ink)}.sub{margin-top:4px;color:var(--stone);font-size:12px;font-weight:800}.badges{display:flex;gap:8px;flex-wrap:wrap}.badge{display:inline-flex;align-items:center;height:28px;border-radius:999px;padding:0 10px;border:1px solid var(--hairline-soft);background:var(--canvas);color:var(--slate);font-size:12px;font-weight:900}.badge.buy{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.wait{background:var(--yellow-light);border-color:rgba(252,185,0,.24);color:var(--yellow-dark)}.badge.observe{background:rgba(66,98,255,.06);border-color:rgba(66,98,255,.16);color:var(--blue)}.badge.risk{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}.hero-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;margin-top:16px}.metric{border:1px solid var(--hairline-soft);background:rgba(255,255,255,.72);border-radius:14px;padding:12px;min-width:0}.metric span{display:block;color:var(--stone);font-size:10px;font-weight:950}.metric b{display:block;margin-top:5px;color:var(--ink);font-size:20px;line-height:1;font-weight:950}.metric b.green{color:var(--green)}.metric b.red{color:var(--red)}.layout{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(360px,.75fr);gap:14px;align-items:start}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden;margin-bottom:14px}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:15px;font-weight:950;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:850}.panel-body{padding:14px}.chart-panel .panel-body{padding:10px}.kline-int-bar{display:flex;gap:4px;padding:0 0 8px}.kline-int-btn{border:1px solid var(--hairline);background:var(--canvas);color:var(--stone);padding:5px 10px;border-radius:8px;font-size:11px;font-weight:900;cursor:pointer}.kline-int-btn.active{background:var(--primary);color:var(--on-primary);border-color:var(--primary)}.kline-container{position:relative;width:100%;height:430px}.kline-container .ax-chart{min-height:430px}.chart-loading,.empty{padding:30px;text-align:center;color:var(--stone);font-size:13px}.decision{display:grid;grid-template-columns:1fr 1fr;gap:10px}.decision-card{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:14px;padding:12px}.decision-card h3{font-size:12px;font-weight:950;color:var(--stone);margin-bottom:6px}.decision-card p{font-size:13px;line-height:1.55;color:var(--ink);font-weight:850}.score-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.score{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:14px;padding:12px}.score span{display:block;color:var(--stone);font-size:10px;font-weight:950}.score b{display:block;margin-top:5px;font-size:24px;line-height:1;font-weight:950}.score.opp b{color:var(--blue)}.score.entry b{color:var(--green)}.score.risk b{color:var(--red)}.chips{display:flex;flex-wrap:wrap;gap:6px}.chip{display:inline-flex;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:999px;padding:6px 9px;color:var(--slate);font-size:11px;font-weight:850}.timeline{display:grid;gap:8px}.row{display:grid;grid-template-columns:96px minmax(0,1fr) auto;gap:10px;align-items:start;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:12px;padding:10px}.row-time{color:var(--stone);font-size:11px;font-weight:900}.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}.row-val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;font-weight:950;color:var(--slate);white-space:nowrap}.green{color:var(--green)!important}.red{color:var(--red)!important}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ai-box{border:1px solid rgba(66,98,255,.14);background:rgba(66,98,255,.045);border-radius:14px;padding:12px;color:var(--slate);font-size:13px;line-height:1.6}.loading{padding:44px;text-align:center;color:var(--stone)}@media(max-width:1080px){.layout{grid-template-columns:1fr}.hero-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.kline-container,.kline-container .ax-chart{height:340px;min-height:340px}}@media(max-width:620px){.shell{width:min(100% - 24px,1320px);padding-top:18px}.symbol{font-size:26px}.hero-grid,.decision,.score-grid{grid-template-columns:1fr}.row{grid-template-columns:1fr}.row-val{white-space:normal}.kline-container,.kline-container .ax-chart{height:300px;min-height:300px}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="topbar">
<button class="back" onclick="history.length>1?history.back():location.href='/app'">返回机会列表</button>
<div class="panel-note" id="updatedAt">--</div>
</div>
<div id="detailRoot"><div class="loading">加载机会详情...</div></div>
</div>
{% endblock %}
{% block extra_script %}
<script src="/static/chart_widgets.js"></script>
<script>
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 clean(s){return String(s||'').replace(/[🟢🟡🔴🔥⚠️✅❌⏳👀]/g,'').replace(/即刻买入|可即刻买入|即刻入场/g,'入场窗口').replace(/强烈推荐/g,'强势异动').trim()}
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 dec(p){p=Math.abs(Number(p||0));if(p<.0001)return 8;if(p<.001)return 7;if(p<.01)return 6;if(p<.1)return 5;if(p<1)return 4;if(p<10)return 3;return 2}
function price(p){p=Number(p||0);return p>0?'$'+p.toFixed(dec(p)):'--'}
function pct(v){v=Number(v||0);return (v>0?'+':'')+v.toFixed(2)+'%'}
function num(v){v=Number(v||0);if(Math.abs(v)>=1e6)return (v/1e6).toFixed(2)+'M';if(Math.abs(v)>=1e3)return (v/1e3).toFixed(1)+'K';return v.toFixed(Math.abs(v)>=10?1:2)}
var LABELS={buy_now:'入场窗口',wait_pullback:'等待回踩',observe:'观察中',holding:'持仓中',completed:'已完成',invalid:'已失效'}
function statusLabel(r){return r.execution_label||LABELS[r.execution_status]||r.action_status||r.rec_state||'观察'}
function statusCls(r){if(r.execution_status==='buy_now')return'buy';if(r.execution_status==='wait_pullback')return'wait';if(r.execution_status==='invalid'||r.status==='stopped_out')return'risk';return'observe'}
function scoreComponents(r){var ep=r.entry_plan||{},mc=r.market_context||{};return ep.score_components||mc.score_components||{}}
function marketRegime(r){var ep=r.entry_plan||{},mc=r.market_context||{};return ep.market_regime||mc.market_regime||{}}
function decisionLog(r){var ep=r.entry_plan||{},mc=r.market_context||{};return ep.decision_log||mc.decision_log||{}}
function signals(r){return Array.isArray(r.signal_labels)&&r.signal_labels.length?r.signal_labels:(Array.isArray(r.signals)?r.signals:[])}
function aiInsight(r){return r.llm_insight&&r.llm_insight.content?r.llm_insight.content:null}
function renderRows(rows,opts){opts=opts||{};if(!rows||!rows.length)return'<div class="empty">'+(opts.empty||'暂无数据')+'</div>';return '<div class="timeline">'+rows.map(function(x){return '<div class="row"><div class="row-time">'+esc(opts.time?opts.time(x):'--')+'</div><div class="row-main"><div class="row-title">'+esc(opts.title?opts.title(x):'--')+'</div><div class="row-sub">'+esc(opts.sub?opts.sub(x):'')+'</div></div><div class="row-val '+(opts.cls?opts.cls(x):'')+'">'+esc(opts.val?opts.val(x):'')+'</div></div>'}).join('')+'</div>'}
function renderDetail(d){var r=d.current||{},symbol=d.symbol||r.symbol||'--',base=symbol.replace('/USDT',''),lp=d.latest_price||{},current=Number(lp.price||r.current_price||r.entry_price||0),ep=r.entry_plan||{},entry=Number(ep.entry_price||r.entry_price||0),stop=Number(ep.stop_loss||r.stop_loss||0),tp1=Number(ep.tp1||ep.take_profit_1||r.tp1||0),sc=scoreComponents(r),rg=marketRegime(r),dl=decisionLog(r),ai=aiInsight(r);$('updatedAt').textContent='最新价格 '+fmtTime(lp.updated_at||r.last_track_time||r.rec_time);var rr=entry&&tp1&&stop?((tp1-entry)/(entry-stop)).toFixed(2):'--';var chg=entry&&current?((current/entry-1)*100):0;var aiHtml=ai?'<div class="ai-box">'+esc(clean(ai.summary||ai.why_now_or_not||'已缓存 AI 解读'))+'</div>':'<div class="empty">暂无 AI 解读</div>';var onchain=d.onchain||{};var metric=onchain.metric||{};var root='<section class="hero"><div class="hero-main"><div><div class="symbol">'+esc(base)+'</div><div class="sub">'+esc(symbol)+' · 推荐 #'+esc(r.id||'--')+' · '+fmtTime(r.rec_time)+'</div></div><div class="badges"><span class="badge '+statusCls(r)+'">'+esc(statusLabel(r))+'</span><span class="badge">总分 '+esc(r.rec_score||0)+'</span><span class="badge">'+esc(r.strategy_version||'--')+'</span></div></div><div class="hero-grid"><div class="metric"><span>当前价</span><b>'+price(current)+'</b></div><div class="metric"><span>相对参考</span><b class="'+(chg>=0?'green':'red')+'">'+pct(chg)+'</b></div><div class="metric"><span>计划入场</span><b>'+price(entry)+'</b></div><div class="metric"><span>止损 / 止盈</span><b>'+price(stop)+' / '+price(tp1)+'</b></div><div class="metric"><span>盈亏比</span><b>'+esc(rr)+'</b></div></div></section><div class="layout"><main><section class="panel chart-panel"><div class="panel-head"><div class="panel-title">多周期 K 线</div><div class="panel-note">计划价、止损、止盈会在图上标注</div></div><div class="panel-body"><div class="kline-int-bar"><button class="kline-int-btn" data-int="5m" onclick="switchKline(this)">5m</button><button class="kline-int-btn" data-int="15m" onclick="switchKline(this)">15m</button><button class="kline-int-btn active" data-int="1h" onclick="switchKline(this)">1H</button><button class="kline-int-btn" data-int="4h" onclick="switchKline(this)">4H</button><button class="kline-int-btn" data-int="1d" onclick="switchKline(this)">1D</button></div><div class="kline-container loading" id="kline" data-symbol="'+esc(symbol)+'" data-entry-price="'+esc(entry||0)+'" data-stop-loss="'+esc(stop||0)+'" data-tp1="'+esc(tp1||0)+'" data-rec-time="'+esc(r.rec_time||'')+'" data-ref-price="'+esc(current||entry||0)+'"><div class="chart-loading">加载 K 线...</div></div></div></section><section class="panel"><div class="panel-head"><div class="panel-title">决策与计划</div><div class="panel-note">列表页只保留摘要,这里看完整依据</div></div><div class="panel-body"><div class="decision"><div class="decision-card"><h3>当前结论</h3><p>'+esc(statusLabel(r))+'</p></div><div class="decision-card"><h3>原因</h3><p>'+esc(clean(r.execution_reason||r.state_reason||dl.decision||'等待进一步确认'))+'</p></div><div class="decision-card"><h3>入场模型</h3><p>'+esc(clean(ep.entry_model||ep.entry_method||'--'))+'</p></div><div class="decision-card"><h3>失效条件</h3><p>'+esc(clean(ep.invalid_if||ep.stop_basis||'跌破风险边界或信号衰减'))+'</p></div></div></div></section><section class="panel"><div class="panel-head"><div class="panel-title">因子评分拆解</div><div class="panel-note">机会、买点、风险分开看</div></div><div class="panel-body"><div class="score-grid"><div class="score opp"><span>机会分</span><b>'+num(sc.opportunity_score||0)+'</b></div><div class="score entry"><span>买点分</span><b>'+num(sc.entry_score||0)+'</b></div><div class="score risk"><span>风险扣分</span><b>'+num(sc.risk_score||0)+'</b></div></div><div class="chips" style="margin-top:12px">'+signals(r).slice(0,12).map(function(s){return'<span class="chip">'+esc(clean(s))+'</span>'}).join('')+'</div></div></section><section class="panel"><div class="panel-head"><div class="panel-title">筛选与推荐历史</div><div class="panel-note">'+esc((d.summary||{}).history_count||0)+' 次推荐 · '+esc((d.summary||{}).screening_count||0)+' 条筛选记录</div></div><div class="panel-body">'+renderRows(d.history,{empty:'暂无推荐历史',time:function(x){return fmtTime(x.rec_time)},title:function(x){return '#'+x.id+' · '+statusLabel(x)},sub:function(x){return signals(x).slice(0,3).map(clean).join(' · ')||'--'},val:function(x){return '分 '+(x.rec_score||0)}})+'<div style="height:10px"></div>'+renderRows(d.screening,{empty:'暂无筛选记录',time:function(x){return fmtTime(x.scan_time)},title:function(x){return (x.layer||'筛选')+' · '+(x.state||'--')},sub:function(x){return (x.signals||[]).slice(0,3).map(clean).join(' · ')},val:function(x){return x.score||0}})+'</div></section></main><aside><section class="panel"><div class="panel-head"><div class="panel-title">市场环境</div><div class="panel-note">'+esc(rg.label||rg.regime||'--')+'</div></div><div class="panel-body"><div class="chips">'+(Array.isArray(rg.reasons)?rg.reasons:['市场环境已进入推荐上下文']).slice(0,8).map(function(s){return'<span class="chip">'+esc(clean(s))+'</span>'}).join('')+'</div></div></section><section class="panel"><div class="panel-head"><div class="panel-title">链上与外部证据</div><div class="panel-note">正向 '+esc(onchain.positive_count||0)+' · 风险 '+esc(onchain.risk_count||0)+'</div></div><div class="panel-body"><div class="decision"><div class="decision-card"><h3>链上分</h3><p>'+num(metric.onchain_score||0)+'</p></div><div class="decision-card"><h3>风险分</h3><p>'+num(metric.risk_score||0)+'</p></div></div><div style="height:10px"></div>'+renderRows(onchain.events,{empty:'暂无链上事件',time:function(x){return fmtTime(x.detected_at)},title:function(x){return x.signal_label||x.signal_code||'链上事件'},sub:function(x){return [x.chain,x.direction,x.tx_hash].filter(Boolean).join(' · ')},val:function(x){return x.value_usd?('$'+num(x.value_usd)):''},cls:function(x){return x.direction==='risk'?'red':'green'}})+'</div></section><section class="panel"><div class="panel-head"><div class="panel-title">策略交易关联</div><div class="panel-note">挂单 / 持仓 / 事件</div></div><div class="panel-body">'+renderRows(d.paper_orders,{empty:'暂无挂单',time:function(x){return fmtTime(x.created_at)},title:function(x){return '挂单 · '+(x.status||'--')},sub:function(x){return '目标 '+price(x.target_price)+' · 当前 '+price(x.current_price_at_create)},val:function(x){return x.cancel_reason||x.status||''}})+'<div style="height:10px"></div>'+renderRows(d.paper_trades,{empty:'暂无持仓交易',time:function(x){return fmtTime(x.opened_at)},title:function(x){return '交易 · '+(x.status||'--')},sub:function(x){return '入场 '+price(x.entry_price)+' · 当前 '+price(x.current_price)},val:function(x){return pct(x.pnl_pct||x.realized_pnl_pct)},cls:function(x){return Number(x.pnl_pct||x.realized_pnl_pct||0)>=0?'green':'red'}})+'<div style="height:10px"></div>'+renderRows(d.paper_events,{empty:'暂无交易事件',time:function(x){return fmtTime(x.event_time)},title:function(x){return x.event_type||'事件'},sub:function(x){return x.message||'--'},val:function(x){return price(x.price)}})+'</div></section><section class="panel"><div class="panel-head"><div class="panel-title">AI 解读</div><div class="panel-note">缓存分析</div></div><div class="panel-body">'+aiHtml+'</div></section><section class="panel"><div class="panel-head"><div class="panel-title">复盘记录</div><div class="panel-note">'+esc((d.summary||{}).review_count||0)+' 条</div></div><div class="panel-body">'+renderRows(d.reviews,{empty:'暂无复盘',time:function(x){return fmtTime(x.review_time)},title:function(x){return x.outcome||'复盘'},sub:function(x){return x.lesson||'--'},val:function(x){return pct(x.pnl_48h||0)},cls:function(x){return Number(x.pnl_48h||0)>=0?'green':'red'}})+'</div></section></aside></div>';$('detailRoot').innerHTML=root;loadKline()}
function loadKline(){var c=$('kline');if(!c)return;var active=document.querySelector('.kline-int-btn.active');var interval=active?active.dataset.int:'1h';fetch(API+'/api/kline?symbol='+encodeURIComponent(c.dataset.symbol)+'&interval='+interval+'&limit=100').then(function(r){return r.json()}).then(function(resp){var candles=resp.candles||[];if(!window.AlphaXCharts||!window.AlphaXCharts.renderKline)throw new Error('chart unavailable');c.innerHTML='';window.AlphaXCharts.renderKline(c,{symbol:c.dataset.symbol,candles:candles,entryPrice:Number(c.dataset.entryPrice||0),stopLoss:Number(c.dataset.stopLoss||0),tp1:Number(c.dataset.tp1||0),recTime:c.dataset.recTime||'',refPrice:Number(c.dataset.refPrice||0)});c.classList.remove('loading')}).catch(function(){c.innerHTML='<div class="chart-loading">K线加载失败</div>'})}
function switchKline(btn){document.querySelectorAll('.kline-int-btn').forEach(function(b){b.classList.remove('active')});btn.classList.add('active');var c=$('kline');c.classList.add('loading');c.innerHTML='<div class="chart-loading">加载 K 线...</div>';loadKline()}
async function load(){var q=new URLSearchParams(location.search);var recId=q.get('rec_id')||'';var symbol=q.get('symbol')||'';var url=API+'/api/opportunity/detail?symbol='+encodeURIComponent(symbol)+'&rec_id='+encodeURIComponent(recId);try{var d=await (await fetch(url)).json();if(d.error){$('detailRoot').innerHTML='<div class="empty">没有找到该机会</div>';return}renderDetail(d)}catch(e){$('detailRoot').innerHTML='<div class="empty">机会详情加载失败</div>'}}
load();
</script>
{% endblock %}