alphax/static/onchain.html
2026-05-16 14:52:10 +08:00

90 lines
20 KiB
HTML
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.

{% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — 链上异动{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link active" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %}
<style>
.shell{width:min(100% - 40px,1280px);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:-.5px;color:var(--ink)}.page-head p{margin-top:5px;color:var(--stone);font-size:13px;line-height:1.45}.head-actions{display:flex;gap:8px;align-items:center;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}.kpis{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;margin-bottom:14px}.kpi{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);padding:13px;min-width:0}.kpi span{display:block;color:var(--stone);font-size:11px;font-weight:900}.kpi b{display:block;margin-top:7px;color:var(--ink);font-size:23px;line-height:1;font-weight:950;letter-spacing:-.5px}.kpi b.green{color:var(--green)}.kpi b.red{color:var(--red)}.kpi b.blue{color:var(--blue)}.spotlight{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);min-width:0;overflow:hidden}.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:950;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:850}.raw-panel{margin-bottom:14px}.raw-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 14px;border-bottom:1px solid var(--hairline-soft);background:var(--surface);flex-wrap:wrap}.raw-feed{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:12px}.raw-card{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:11px;min-width:0}.raw-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.raw-title{font-size:12px;font-weight:950;color:var(--ink);line-height:1.35;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.raw-desc{color:var(--slate);font-size:12px;line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:34px}.raw-meta{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:9px}.raw-link{color:var(--blue);font-size:11px;font-weight:900;text-decoration:none}.token-list{display:grid;gap:8px;padding:12px}.token-row{display:grid;grid-template-columns:1fr auto;gap:8px;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:11px;cursor:pointer;transition:.12s}.token-row:hover{border-color:var(--hairline);background:rgba(66,98,255,.04)}.token-main{min-width:0}.sym{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:950;color:var(--ink);font-size:14px}.sub{margin-top:4px;color:var(--stone);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.score{display:grid;place-items:center;min-width:58px;height:42px;border-radius:var(--radius-md);border:1px solid var(--hairline-soft);background:var(--canvas);font-weight:950;color:var(--blue)}.score.risk{color:var(--red)}.layout{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:14px;align-items:start}.toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:12px 14px;border-bottom:1px solid var(--hairline-soft);background:var(--surface)}.table-wrap{overflow:auto}.table{width:100%;border-collapse:collapse;min-width:860px}.table th,.table td{padding:10px 11px;border-bottom:1px solid var(--hairline-soft);text-align:left;font-size:12px;vertical-align:middle}.table th{background:var(--surface);font-size:11px;color:var(--stone);font-weight:950;position:sticky;top:0}.table td{color:var(--slate)}.table tr{cursor:pointer}.table tr:hover td{background:rgba(66,98,255,.035)}.badge{display:inline-flex;align-items:center;height:24px;border-radius:var(--radius-full);padding:0 8px;border:1px solid var(--hairline-soft);background:var(--surface);font-size:11px;font-weight:900;color:var(--slate);white-space:nowrap}.badge.pos{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.risk{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}.badge.blue{background:rgba(66,98,255,.07);border-color:rgba(66,98,255,.16);color:var(--blue)}.badge.mapped{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.unmapped{background:rgba(66,98,255,.07);border-color:rgba(66,98,255,.16);color:var(--blue)}.num{font-weight:950;color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.pos-text{color:var(--green)}.neg-text{color:var(--red)}.detail-body{padding:14px}.empty,.loading{padding:34px 16px;text-align:center;color:var(--stone);font-size:13px}.detail-title{font-size:18px;font-weight:950;color:var(--ink);margin-bottom:5px}.detail-sub{color:var(--stone);font-size:12px;margin-bottom:12px}.metric-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}.metric{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px}.metric span{display:block;font-size:10px;color:var(--stone);font-weight:900}.metric b{display:block;margin-top:4px;color:var(--ink);font-size:15px;font-weight:950}.event-feed{display:grid;gap:8px}.event{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:10px}.event-top{display:flex;justify-content:space-between;gap:8px;align-items:center}.event-title{font-size:12px;font-weight:950;color:var(--ink);line-height:1.45}.event-meta{margin-top:5px;color:var(--stone);font-size:11px;line-height:1.45}.pager{display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;border-top:1px solid var(--hairline-soft);color:var(--stone);font-size:12px;font-weight:850}.pager button{height:32px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 10px;font-weight:850;color:var(--ink);cursor:pointer}.pager button:disabled{opacity:.45;cursor:default}.hint{padding:10px 12px;border:1px solid rgba(66,98,255,.14);background:rgba(66,98,255,.045);border-radius:var(--radius-md);color:var(--slate);font-size:12px;line-height:1.55;margin-bottom:14px}@media(max-width:1080px){.kpis{grid-template-columns:repeat(3,minmax(0,1fr))}.raw-feed{grid-template-columns:1fr 1fr}.spotlight,.layout{grid-template-columns:1fr}}@media(max-width:620px){.shell{width:min(100% - 24px,1280px)}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.raw-feed{grid-template-columns:1fr}.page-head h1{font-size:22px}.metric-grid{grid-template-columns:1fr}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="page-head">
<div><h1>链上异动</h1><p>跟踪 DEX 放量、流动性变化、资金流和鲸鱼行为。链上信号只负责发现线索,最终仍交给技术确认。</p></div>
<div class="head-actions">
<select class="select" id="hoursSel" onchange="reloadAll()"><option value="24">近 24h</option><option value="72">近 3 天</option><option value="168">近 7 天</option></select>
<button class="btn" onclick="reloadAll()">刷新</button>
</div>
</div>
<div class="hint">链上异动不是买入指令。高质量正向信号会进入技术检查;交易所流入、流动性撤出、持仓集中等负向信号只作为风险上下文。</div>
<section class="panel raw-panel">
<div class="panel-head"><div class="panel-title">实时链上流</div><div class="panel-note">原始事件先展示,映射后再进入分析链路</div></div>
<div class="raw-toolbar">
<div class="head-actions">
<select class="select" id="rawChainSel" onchange="reloadRawEvents(0)"><option value="">全部链</option><option value="ethereum">Ethereum</option><option value="bsc">BSC</option><option value="base">Base</option><option value="arbitrum">Arbitrum</option><option value="solana">Solana</option></select>
<select class="select" id="rawMapSel" onchange="reloadRawEvents(0)"><option value="">全部状态</option><option value="mapped">已映射</option><option value="unmapped">未映射</option></select>
</div>
<div class="panel-note" id="rawInfo">--</div>
</div>
<div class="raw-feed" id="rawFeed"><div class="loading">加载中...</div></div>
</section>
<div class="kpis" id="kpis"><div class="loading">加载中...</div></div>
<div class="spotlight">
<section class="panel"><div class="panel-head"><div class="panel-title">链上热度</div><div class="panel-note">按 onchain score</div></div><div class="token-list" id="hotTokens"><div class="loading">加载中...</div></div></section>
<section class="panel"><div class="panel-head"><div class="panel-title">风险异动</div><div class="panel-note">流入/撤池/集中度</div></div><div class="token-list" id="riskTokens"><div class="loading">加载中...</div></div></section>
</div>
<div class="layout">
<section class="panel">
<div class="toolbar">
<select class="select" id="chainSel" onchange="reloadTokens(0)"><option value="">全部链</option><option value="ethereum">Ethereum</option><option value="bsc">BSC</option><option value="base">Base</option><option value="arbitrum">Arbitrum</option><option value="solana">Solana</option></select>
<select class="select" id="signalSel" onchange="reloadTokens(0)"><option value="">全部信号</option><option value="dex_volume_spike">DEX 放量</option><option value="liquidity_add">流动性增加</option><option value="liquidity_remove_risk">流动性撤出</option><option value="exchange_outflow">交易所流出</option><option value="exchange_inflow_risk">交易所流入</option><option value="whale_accumulation">鲸鱼增持</option><option value="smart_money_buying">聪明钱买入</option></select>
</div>
<div class="table-wrap"><table class="table"><thead><tr><th>币种</th><th></th><th>链上分</th><th>风险分</th><th>DEX 成交</th><th>成交变化</th><th>流动性变化</th><th>主链路</th></tr></thead><tbody id="tokenTable"><tr><td colspan="8" class="loading">加载中...</td></tr></tbody></table></div>
<div class="pager"><button id="prevBtn" onclick="page(-1)">上一页</button><span id="pageInfo">--</span><button id="nextBtn" onclick="page(1)">下一页</button></div>
</section>
<section class="panel">
<div class="panel-head"><div class="panel-title">单币详情</div><div class="panel-note" id="detailNote">选择币种</div></div>
<div class="detail-body" id="detailBody"><div class="empty">从列表中选择一个币,查看链上事件与主链路关系。</div></div>
</section>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
var API='', state={offset:0,limit:24,total:0,selected:'',rawOffset:0,rawLimit:12,rawTotal:0};
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 fmtUsd(v){v=Number(v||0);if(Math.abs(v)>=1e9)return '$'+(v/1e9).toFixed(2)+'B';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(0)}
function fmtPct(v){v=Number(v||0);var cls=v>0?'pos-text':v<0?'neg-text':'';return '<span class="'+cls+'">'+(v>0?'+':'')+v.toFixed(1)+'%</span>'}
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 fmtAmount(v){v=Number(v||0);if(v>=1000)return v.toFixed(0);if(v>0)return v.toFixed(2);return '--'}
function recLabel(r){if(!r||!r.has_active)return '<span class="badge">未进入</span>';var s=r.execution_status||'';if(s==='buy_now')return '<span class="badge pos">入场窗口</span>';if(s==='wait_pullback')return '<span class="badge blue">等回踩</span>';return '<span class="badge blue">'+esc(r.action_status||'观察中')+'</span>'}
function renderKpis(k){var cells=[['原始事件',k.raw_event_count||0,'blue'],['已映射',k.raw_mapped_count||0,'green'],['覆盖币种',k.token_count||0,'blue'],['正向异动',k.positive_events||0,'green'],['风险事件',k.risk_events||0,'red'],['DEX 成交',fmtUsd(k.dex_volume_usd||0),'blue']];$('kpis').innerHTML=cells.map(function(x){return '<div class="kpi"><span>'+x[0]+'</span><b class="'+x[2]+'">'+x[1]+'</b></div>'}).join('')}
function tokenRow(t, risk){return '<div class="token-row" onclick="loadDetail(\''+esc(t.symbol)+'\')"><div class="token-main"><div class="sym">'+esc(t.symbol)+'</div><div class="sub">'+esc(t.chain)+' · DEX '+fmtUsd(t.dex_volume_usd)+' · 流动性 '+fmtUsd(t.liquidity_usd)+'</div></div><div class="score '+(risk?'risk':'')+'">'+Number(risk?t.risk_score:t.onchain_score||0).toFixed(0)+'</div></div>'}
function renderSpotlight(d){$('hotTokens').innerHTML=(d.hot_tokens||[]).length?(d.hot_tokens||[]).map(function(t){return tokenRow(t,false)}).join(''):'<div class="empty">暂无链上热点</div>';$('riskTokens').innerHTML=(d.risk_tokens||[]).length?(d.risk_tokens||[]).map(function(t){return tokenRow(t,true)}).join(''):'<div class="empty">暂无集中风险</div>'}
async function loadOverview(){try{var r=await fetch(API+'/api/onchain/overview?hours='+$('hoursSel').value);var d=await r.json();renderKpis(d.kpi||{});renderSpotlight(d)}catch(e){$('kpis').innerHTML='<div class="empty">链上总览加载失败</div>'}}
function renderRawCard(e){var mapped=e.mapping_status==='mapped';var desc=e.description||e.name||e.token_short||'链上源捕捉到新的 Token 动态';var amount=e.total_amount||e.amount||0;var link=e.url?'<a class="raw-link" href="'+esc(e.url)+'" target="_blank" rel="noopener">查看来源</a>':'';return '<div class="raw-card"><div class="raw-top"><div class="raw-title">'+esc(e.event_label||e.title||e.event_type)+'</div><span class="badge '+(mapped?'mapped':'unmapped')+'">'+(mapped?'已映射':'未映射')+'</span></div><div class="raw-desc">'+esc(desc)+'</div><div class="raw-meta"><span class="badge">'+esc(e.chain||'--')+'</span><span class="badge">'+esc(e.mapped_symbol||e.token_short||'未知 Token')+'</span><span class="badge">热度 '+fmtAmount(amount)+'</span><span class="sub">'+fmtTime(e.detected_at)+'</span>'+link+'</div></div>'}
async function reloadRawEvents(offset){state.rawOffset=offset||0;$('rawFeed').innerHTML='<div class="loading">加载中...</div>';try{var qs='hours='+$('hoursSel').value+'&limit='+state.rawLimit+'&offset='+state.rawOffset+'&chain='+encodeURIComponent($('rawChainSel').value)+'&mapping_status='+encodeURIComponent($('rawMapSel').value);var d=await (await fetch(API+'/api/onchain/raw-events?'+qs)).json();state.rawTotal=d.total||0;var items=d.items||[];$('rawInfo').textContent='共 '+state.rawTotal+' 条,显示最近 '+items.length+' 条';$('rawFeed').innerHTML=items.length?items.map(renderRawCard).join(''):'<div class="empty">暂无原始链上流</div>'}catch(e){$('rawFeed').innerHTML='<div class="empty">原始链上流加载失败</div>';$('rawInfo').textContent='加载失败'}}
async function reloadTokens(offset){state.offset=offset||0;$('tokenTable').innerHTML='<tr><td colspan="8" class="loading">加载中...</td></tr>';try{var qs='hours='+$('hoursSel').value+'&limit='+state.limit+'&offset='+state.offset+'&chain='+encodeURIComponent($('chainSel').value)+'&signal='+encodeURIComponent($('signalSel').value);var d=await (await fetch(API+'/api/onchain/tokens?'+qs)).json();state.total=d.total||0;var items=d.items||[];if(!items.length){$('tokenTable').innerHTML='<tr><td colspan="8" class="empty">暂无链上异动 token</td></tr>'}else{$('tokenTable').innerHTML=items.map(function(t){return '<tr onclick="loadDetail(\''+esc(t.symbol)+'\')"><td class="sym">'+esc(t.symbol)+'</td><td>'+esc(t.chain)+'</td><td class="num">'+Number(t.onchain_score||0).toFixed(0)+'</td><td class="num">'+Number(t.risk_score||0).toFixed(0)+'</td><td class="num">'+fmtUsd(t.dex_volume_usd)+'</td><td>'+fmtPct(t.dex_volume_change_pct)+'</td><td>'+fmtPct(t.liquidity_change_pct)+'</td><td>'+recLabel(t.recommendation)+'</td></tr>'}).join('');if(!state.selected&&items[0])loadDetail(items[0].symbol)}updatePager()}catch(e){$('tokenTable').innerHTML='<tr><td colspan="8" class="empty">加载失败</td></tr>'}}
function updatePager(){var page=Math.floor(state.offset/state.limit)+1,totalPages=Math.max(1,Math.ceil((state.total||0)/state.limit));$('pageInfo').textContent='第 '+page+' / '+totalPages+' 页,共 '+state.total+' 个';$('prevBtn').disabled=state.offset<=0;$('nextBtn').disabled=state.offset+state.limit>=state.total}
function page(step){var next=state.offset+step*state.limit;if(next<0||next>=state.total)return;reloadTokens(next)}
async function loadDetail(symbol){state.selected=symbol;$('detailNote').textContent=symbol;$('detailBody').innerHTML='<div class="loading">加载详情...</div>';try{var d=await (await fetch(API+'/api/onchain/tokens/'+encodeURIComponent(symbol)+'?hours=168')).json();var latest=(d.metrics||[])[0]||{};var rec=d.recommendation;var metrics='<div class="metric-grid">'+[['链上分',Number(latest.onchain_score||0).toFixed(0)],['风险分',Number(latest.risk_score||0).toFixed(0)],['DEX 成交',fmtUsd(latest.dex_volume_usd)],['流动性',fmtUsd(latest.liquidity_usd)]].map(function(x){return '<div class="metric"><span>'+x[0]+'</span><b>'+x[1]+'</b></div>'}).join('')+'</div>';var recHtml=rec?'<div class="hint">主链路状态:'+esc(rec.action_status||rec.execution_status||'观察')+' · 推荐 #'+esc(rec.id)+'</div>':'<div class="hint">尚未形成主链路推荐;若链上信号质量足够,会先进入技术检查。</div>';var events=(d.events||[]).slice(0,20).map(function(e){var cls=e.direction==='risk'?'risk':e.direction==='positive'?'pos':'blue';return '<div class="event"><div class="event-top"><div class="event-title">'+esc(e.signal_label||e.signal_code)+'</div><span class="badge '+cls+'">'+esc(e.severity||e.direction)+'</span></div><div class="event-meta">'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+fmtUsd(e.value_usd)+'<br>'+esc(e.wallet_label||e.counterparty_label||e.source||'链上事件')+'</div></div>'}).join('')||'<div class="empty">暂无事件明细</div>';$('detailBody').innerHTML='<div class="detail-title">'+esc(d.symbol)+'</div><div class="detail-sub">'+(d.mappings||[]).length+' 个合约映射 · 近 7 天</div>'+metrics+recHtml+'<div class="event-feed">'+events+'</div>'}catch(e){$('detailBody').innerHTML='<div class="empty">详情加载失败</div>'}}
function reloadAll(){state.offset=0;state.rawOffset=0;state.selected='';loadOverview();reloadRawEvents(0);reloadTokens(0)}
reloadAll();
setInterval(reloadAll,300000);
</script>
{% endblock %}