From 24859a6acf4da5d10df8a751f22302203fe622a1 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 21 May 2026 20:45:43 +0800 Subject: [PATCH] 1 --- app/db/onchain_db.py | 113 +++++++++++++++++++++++++++------ static/onchain.html | 22 +++---- tests/test_onchain_tracking.py | 40 ++++++++++++ 3 files changed, 146 insertions(+), 29 deletions(-) diff --git a/app/db/onchain_db.py b/app/db/onchain_db.py index 54c8f0c..1637d84 100644 --- a/app/db/onchain_db.py +++ b/app/db/onchain_db.py @@ -19,7 +19,7 @@ MIN_MAPPING_CONFIDENCE = 70 SIGNAL_LABELS = { "large_token_transfer": "链上大额转账", - "dex_volume_spike": "DEX 放量", + "dex_volume_spike": "链上成交放量", "liquidity_add": "流动性增加", "liquidity_remove_risk": "流动性撤出风险", "exchange_outflow": "交易所流出", @@ -44,6 +44,8 @@ RAW_EVENT_EXPLAINERS = { POSITIVE_SIGNALS = {"dex_volume_spike", "liquidity_add", "exchange_outflow", "whale_accumulation", "holder_growth", "smart_money_buying"} RISK_SIGNALS = {"liquidity_remove_risk", "exchange_inflow_risk", "holder_concentration_risk"} +STANDARD_SIGNAL_SOURCE = "nodereal" +LEGACY_ONCHAIN_SOURCES = {"dexscreener", "etherscan", "helius"} def _now(): @@ -87,6 +89,10 @@ def signal_direction(code): return "neutral" +def _is_legacy_onchain_source(source): + return str(source or "").lower().strip() in LEGACY_ONCHAIN_SOURCES + + def raw_event_type_label(event_type): return RAW_EVENT_TYPE_LABELS.get(str(event_type or ""), str(event_type or "链上原始事件")) @@ -426,28 +432,31 @@ def get_onchain_overview(hours=24): events = [dict(row) for row in event_rows] raw_events = [dict(row) for row in raw_rows] metrics = [dict(row) for row in metric_rows] - hot = sorted(metrics, key=lambda x: float(x.get("onchain_score") or 0), reverse=True)[:8] - risks = sorted(metrics, key=lambda x: float(x.get("risk_score") or 0), reverse=True)[:8] + standard_events = [e for e in events if str(e.get("source") or "").lower() == STANDARD_SIGNAL_SOURCE] + node_metrics = [m for m in metrics if not _is_legacy_onchain_source(m.get("source"))] + hot = sorted(node_metrics, key=lambda x: float(x.get("onchain_score") or 0), reverse=True)[:8] + risks = sorted(node_metrics, key=lambda x: float(x.get("risk_score") or 0), reverse=True)[:8] + mapped_feed = _mapped_raw_signal_items(raw_events, active, limit=8) total_netflow = sum(float(x.get("exchange_netflow_usd") or 0) for x in metrics) - dex_volume = sum(float(x.get("dex_volume_usd") or 0) for x in metrics) return { "hours": int(hours or 24), "updated_at": _now(), "kpi": { - "event_count": len(events), + "event_count": len(standard_events), "raw_event_count": len(raw_events), "raw_unmapped_count": sum(1 for e in raw_events if e.get("mapping_status") == "unmapped"), "raw_mapped_count": sum(1 for e in raw_events if e.get("mapping_status") == "mapped"), - "token_count": len({(m["symbol"], m["chain"], m.get("contract_address") or "") for m in metrics}), - "positive_events": sum(1 for e in events if e.get("direction") == "positive"), - "risk_events": sum(1 for e in events if e.get("direction") == "risk"), + "token_count": len({(m["symbol"], m["chain"], m.get("contract_address") or "") for m in node_metrics}) + or len({(e.get("mapped_symbol"), e.get("chain")) for e in raw_events if e.get("mapping_status") == "mapped" and e.get("mapped_symbol")}), + "positive_events": sum(1 for e in standard_events if e.get("direction") == "positive"), + "risk_events": sum(1 for e in standard_events if e.get("direction") == "risk"), "exchange_netflow_usd": round(total_netflow, 2), - "dex_volume_usd": round(dex_volume, 2), + "mapped_signal_count": len(mapped_feed), }, - "hot_tokens": [_format_metric_item(row, active) for row in hot], + "hot_tokens": ([_format_metric_item(row, active) for row in hot] or mapped_feed), "risk_tokens": [_format_metric_item(row, active) for row in risks], "raw_events": [_format_raw_event(row) for row in raw_latest], - "signals": _signal_counts(events), + "signals": _signal_counts(standard_events), "provider_status": get_onchain_provider_status(hours=hours), } @@ -461,8 +470,23 @@ def get_onchain_provider_status(hours=24): conn = get_conn() try: raw_total = conn.execute("SELECT COUNT(*) FROM onchain_raw_events WHERE detected_at >= %s", (cutoff,)).fetchone()[0] - metric_total = conn.execute("SELECT COUNT(*) FROM onchain_token_metrics WHERE metric_time >= %s", (cutoff,)).fetchone()[0] - signal_total = conn.execute("SELECT COUNT(*) FROM onchain_events WHERE detected_at >= %s", (cutoff,)).fetchone()[0] + metric_total = conn.execute( + """ + SELECT COUNT(*) + FROM onchain_token_metrics + WHERE metric_time >= %s + AND COALESCE(source, '') NOT IN ('dexscreener', 'etherscan', 'helius') + """, + (cutoff,), + ).fetchone()[0] + signal_total = conn.execute( + """ + SELECT COUNT(*) + FROM onchain_events + WHERE detected_at >= %s AND source=%s + """, + (cutoff, STANDARD_SIGNAL_SOURCE), + ).fetchone()[0] candidate_total = conn.execute( """ SELECT COUNT(*) @@ -602,12 +626,58 @@ def _format_metric_item(row, active=None): return item +def _mapped_raw_signal_items(raw_events, active=None, limit=8): + active = active or {} + grouped = {} + for event in raw_events: + if event.get("mapping_status") != "mapped" or not event.get("mapped_symbol"): + continue + if str(event.get("source") or "").lower() != STANDARD_SIGNAL_SOURCE: + continue + key = (event.get("mapped_symbol"), event.get("chain") or "") + current = grouped.setdefault( + key, + { + "symbol": event.get("mapped_symbol"), + "chain": event.get("chain") or "", + "contract_address": event.get("token_address") or "", + "source": STANDARD_SIGNAL_SOURCE, + "onchain_score": 0, + "risk_score": 0, + "event_count": 0, + "mapped_event_count": 0, + "latest_event_at": "", + "dex_volume_usd": 0, + "dex_volume_change_pct": 0, + "liquidity_usd": 0, + "liquidity_change_pct": 0, + }, + ) + importance = float(event.get("importance") or 0) + current["event_count"] += 1 + current["mapped_event_count"] += 1 + current["onchain_score"] = max(float(current.get("onchain_score") or 0), importance) + current["latest_event_at"] = max(str(current.get("latest_event_at") or ""), str(event.get("detected_at") or "")) + items = [] + for item in grouped.values(): + rec = active.get(item.get("symbol")) or {} + item["recommendation"] = { + "rec_id": rec.get("id") or 0, + "execution_status": rec.get("execution_status") or "", + "action_status": rec.get("action_status") or "", + "display_bucket": rec.get("display_bucket") or "", + "has_active": bool(rec), + } + items.append(item) + return sorted(items, key=lambda x: (float(x.get("onchain_score") or 0), str(x.get("latest_event_at") or "")), reverse=True)[: int(limit or 8)] + + def list_onchain_tokens(limit=30, offset=0, chain="", signal="", hours=24): init_onchain_tables() limit = max(1, min(int(limit or 30), 100)) offset = max(0, int(offset or 0)) cutoff = (datetime.now() - timedelta(hours=int(hours or 24))).isoformat() - clauses = [] + clauses = ["COALESCE(m.source, '') NOT IN ('dexscreener', 'etherscan', 'helius')"] params = [] if chain: clauses.append("m.chain=%s") @@ -619,6 +689,7 @@ def list_onchain_tokens(limit=30, offset=0, chain="", signal="", hours=24): SELECT 1 FROM onchain_events e WHERE e.symbol=m.symbol AND e.chain=m.chain AND e.detected_at >= %s AND e.signal_code=%s + AND COALESCE(e.source, '') NOT IN ('dexscreener', 'etherscan', 'helius') ) """ ) @@ -640,9 +711,11 @@ def list_onchain_tokens(limit=30, offset=0, chain="", signal="", hours=24): f""" SELECT m.*, (SELECT COUNT(*) FROM onchain_events e - WHERE e.symbol=m.symbol AND e.chain=m.chain AND e.detected_at >= %s) AS event_count, + WHERE e.symbol=m.symbol AND e.chain=m.chain AND e.detected_at >= %s + AND COALESCE(e.source, '') NOT IN ('dexscreener', 'etherscan', 'helius')) AS event_count, (SELECT COUNT(*) FROM onchain_events e - WHERE e.symbol=m.symbol AND e.chain=m.chain AND e.direction='risk' AND e.detected_at >= %s) AS risk_event_count + WHERE e.symbol=m.symbol AND e.chain=m.chain AND e.direction='risk' AND e.detected_at >= %s + AND COALESCE(e.source, '') NOT IN ('dexscreener', 'etherscan', 'helius')) AS risk_event_count FROM ({_latest_metrics_subquery(hours)}) m WHERE {where} ORDER BY m.onchain_score DESC, m.risk_score DESC, m.metric_time DESC @@ -677,6 +750,7 @@ def get_onchain_token_detail(symbol, hours=72): """ SELECT * FROM onchain_events WHERE symbol=%s AND detected_at >= %s + AND COALESCE(source, '') NOT IN ('dexscreener', 'etherscan', 'helius') ORDER BY detected_at DESC, id DESC LIMIT 100 """, @@ -686,6 +760,7 @@ def get_onchain_token_detail(symbol, hours=72): """ SELECT * FROM onchain_token_metrics WHERE symbol=%s AND metric_time >= %s + AND COALESCE(source, '') NOT IN ('dexscreener', 'etherscan', 'helius') ORDER BY metric_time DESC, id DESC LIMIT 100 """, @@ -764,9 +839,11 @@ def list_onchain_raw_events(limit=50, offset=0, chain="", source="", event_type= params.append(mapping_status) if priority: if priority == "important": - clauses.append("event_type NOT IN ('token_profile_latest', 'token_boost_latest', 'token_boost_top')") + clauses.append("importance >= %s") + params.append(70) elif priority == "low": - clauses.append("event_type IN ('token_profile_latest', 'token_boost_latest', 'token_boost_top')") + clauses.append("importance < %s") + params.append(70) where = " AND ".join(clauses) conn = get_conn() total = conn.execute(f"SELECT COUNT(*) FROM onchain_raw_events WHERE {where}", tuple(params)).fetchone()[0] diff --git a/static/onchain.html b/static/onchain.html index 0a10b94..2d1778f 100644 --- a/static/onchain.html +++ b/static/onchain.html @@ -22,9 +22,9 @@
- + - +
--
@@ -39,10 +39,10 @@
- - + +
-
币种链上分风险分DEX 成交成交变化流动性变化主链路
加载中...
+
币种链上分风险分映射事件最近事件数据源主链路
加载中...
--
@@ -64,18 +64,18 @@ function fmtAmount(v){v=Number(v||0);if(v>=1000)return v.toFixed(0);if(v>0)retur function recLabel(r){if(!r||!r.has_active)return '未进入';var s=r.execution_status||'';if(s==='buy_now')return '入场窗口';if(s==='wait_pullback')return '等回踩';return ''+esc(r.action_status||'观察中')+''} function setRawPriority(v){state.rawPriority=v;reloadRawEvents(0)} function statusBadgeClass(s){s=String(s||'');if(s.indexOf('正常')>=0)return 'pos';if(s.indexOf('失败')>=0||s.indexOf('未接入')>=0)return 'warn';if(s.indexOf('关闭')>=0)return '';return 'blue'} -function providerCard(p){var stats=[['原始',p.raw_events||0],['指标',p.metrics||0],['信号',p.signals||0]].map(function(x){return ''+x[0]+' '+x[1]+''}).join('');var key=p.api_key_present?'Key 已配置':'无 Key';var impl=p.implemented?'已接入':'待接入';var foot=p.provider==='nodereal'?'主链上数据源:当前负责 EVM Transfer 日志、大额转账和 holder 变化。':(p.status==='已关闭'?'已从主链路移除,保留仅用于兼容和历史数据展示。':'辅助来源:不再作为默认链上主链路。');return '
'+esc(p.label||p.provider)+'
'+esc(p.status||'--')+'
'+esc(p.role||'--')+'
'+stats+key+impl+'
'+foot+'
'} +function providerCard(p){var stats=[['原始',p.raw_events||0],['指标',p.metrics||0],['信号',p.signals||0]].map(function(x){return ''+x[0]+' '+x[1]+''}).join('');var key=p.api_key_present?'Key 已配置':'无 Key';var impl=p.implemented?'已接入':'待接入';var foot='主链上数据源:当前负责 EVM Transfer 日志、大额转账和 holder 变化。';return '
'+esc(p.label||p.provider)+'
'+esc(p.status||'--')+'
'+esc(p.role||'--')+'
'+stats+key+impl+'
'+foot+'
'} function renderProviderStatus(s){var providers=s.providers||[];$('providerStatus').innerHTML=providers.length?providers.map(providerCard).join(''):'
暂无数据源状态
';var c=s.coverage||{};var steps=[['原始流',c.raw_events||0],['币种映射',c.usable_mappings||0],['标准信号',c.signals||0],['技术检查候选',c.queued_candidates||0]];$('flowStatus').innerHTML=steps.map(function(x){return '
'+x[0]+''+x[1]+'
'}).join('')} -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 '
'+x[0]+''+x[1]+'
'}).join('')} -function tokenRow(t, risk){return '
'+esc(t.symbol)+'
'+esc(t.chain)+' · DEX '+fmtUsd(t.dex_volume_usd)+' · 流动性 '+fmtUsd(t.liquidity_usd)+'
'+Number(risk?t.risk_score:t.onchain_score||0).toFixed(0)+'
'} +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'],['映射信号流',k.mapped_signal_count||0,'blue']];$('kpis').innerHTML=cells.map(function(x){return '
'+x[0]+''+x[1]+'
'}).join('')} +function tokenRow(t, risk){var cnt=t.mapped_event_count||t.event_count||0;var latest=t.latest_event_at?(' · 最近 '+fmtTime(t.latest_event_at)):'';var sub=esc(t.chain)+' · 映射事件 '+cnt+latest;return '
'+esc(t.symbol)+'
'+sub+'
'+Number(risk?t.risk_score:t.onchain_score||0).toFixed(0)+'
'} function renderSpotlight(d){$('hotTokens').innerHTML=(d.hot_tokens||[]).length?(d.hot_tokens||[]).map(function(t){return tokenRow(t,false)}).join(''):'
暂无映射后的正向链上信号
';$('riskTokens').innerHTML=(d.risk_tokens||[]).length?(d.risk_tokens||[]).map(function(t){return tokenRow(t,true)}).join(''):'
暂无映射后的风险信号
'} 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);renderProviderStatus(d.provider_status||{})}catch(e){$('kpis').innerHTML='
链上总览加载失败
';$('providerStatus').innerHTML='
数据源状态加载失败
'}} function renderRawCard(e){var mapped=e.mapping_status==='mapped';var desc=e.plain_summary||e.description||e.name||e.token_short||'链上源捕捉到新的 Token 动态';var amount=e.total_amount||e.amount||0;var link=e.url?'查看来源':'';var pr=e.priority||'medium';var prBadge=pr==='low'?'低优先级':pr==='high'?'高优先级':'中优先级';return '
'+esc(e.event_label||e.title||e.event_type)+'
'+(mapped?'已映射':'未映射')+'
'+esc(desc)+'
'+esc(e.why_matters||'需要映射和验证后才可能进入主链路。')+'
'+esc(e.chain||'--')+''+esc(e.mapped_symbol||e.token_short||'未知 Token')+''+prBadge+(amount?'热度 '+fmtAmount(amount)+'':'')+''+fmtTime(e.detected_at)+''+link+'
'+esc(e.pipeline_note||'未完成映射时仅作为原始观察。')+'
'} -async function reloadRawEvents(offset){state.rawOffset=offset||0;$('rawFeed').innerHTML='
加载中...
';try{var qs='hours='+$('hoursSel').value+'&limit='+state.rawLimit+'&offset='+state.rawOffset+'&chain='+encodeURIComponent($('rawChainSel').value)+'&mapping_status='+encodeURIComponent($('rawMapSel').value)+'&priority='+encodeURIComponent(state.rawPriority||'');var d=await (await fetch(API+'/api/onchain/raw-events?'+qs)).json();state.rawTotal=d.total||0;var items=d.items||[];var mode=state.rawPriority==='low'?'低优先级曝光源':state.rawPriority==='important'?'重要事件':'全部原始流';$('rawInfo').textContent=mode+' · 共 '+state.rawTotal+' 条,显示最近 '+items.length+' 条';$('rawFeed').innerHTML=items.length?items.map(renderRawCard).join(''):(state.rawPriority==='important'?'
暂无高价值链上事件。DEX Screener 曝光流已默认隐藏,可手动切换查看。
':'
暂无原始链上流
')}catch(e){$('rawFeed').innerHTML='
原始链上流加载失败
';$('rawInfo').textContent='加载失败'}} -async function reloadTokens(offset){state.offset=offset||0;$('tokenTable').innerHTML='加载中...';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='暂无链上异动 token'}else{$('tokenTable').innerHTML=items.map(function(t){return ''+esc(t.symbol)+''+esc(t.chain)+''+Number(t.onchain_score||0).toFixed(0)+''+Number(t.risk_score||0).toFixed(0)+''+fmtUsd(t.dex_volume_usd)+''+fmtPct(t.dex_volume_change_pct)+''+fmtPct(t.liquidity_change_pct)+''+recLabel(t.recommendation)+''}).join('');if(!state.selected&&items[0])loadDetail(items[0].symbol)}updatePager()}catch(e){$('tokenTable').innerHTML='加载失败'}} +async function reloadRawEvents(offset){state.rawOffset=offset||0;$('rawFeed').innerHTML='
加载中...
';try{var qs='hours='+$('hoursSel').value+'&limit='+state.rawLimit+'&offset='+state.rawOffset+'&chain='+encodeURIComponent($('rawChainSel').value)+'&mapping_status='+encodeURIComponent($('rawMapSel').value)+'&priority='+encodeURIComponent(state.rawPriority||'');var d=await (await fetch(API+'/api/onchain/raw-events?'+qs)).json();state.rawTotal=d.total||0;var items=d.items||[];var mode=state.rawPriority==='low'?'低优先级':state.rawPriority==='important'?'重要事件':'全部原始流';$('rawInfo').textContent=mode+' · 共 '+state.rawTotal+' 条,显示最近 '+items.length+' 条';$('rawFeed').innerHTML=items.length?items.map(renderRawCard).join(''):(state.rawPriority==='important'?'
暂无高价值链上事件。
':'
暂无原始链上流
')}catch(e){$('rawFeed').innerHTML='
原始链上流加载失败
';$('rawInfo').textContent='加载失败'}} +async function reloadTokens(offset){state.offset=offset||0;$('tokenTable').innerHTML='加载中...';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='暂无链上异动 token'}else{$('tokenTable').innerHTML=items.map(function(t){return ''+esc(t.symbol)+''+esc(t.chain)+''+Number(t.onchain_score||0).toFixed(0)+''+Number(t.risk_score||0).toFixed(0)+''+Number(t.event_count||t.mapped_event_count||0).toFixed(0)+''+esc(t.latest_event_at?fmtTime(t.latest_event_at):'--')+''+esc(t.source||'nodereal')+''+recLabel(t.recommendation)+''}).join('');if(!state.selected&&items[0])loadDetail(items[0].symbol)}updatePager()}catch(e){$('tokenTable').innerHTML='加载失败'}} 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='
加载详情...
';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='
'+[['链上分',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 '
'+x[0]+''+x[1]+'
'}).join('')+'
';var recHtml=rec?'
主链路状态:'+esc(rec.action_status||rec.execution_status||'观察')+' · 推荐 #'+esc(rec.id)+'
':'
尚未形成主链路推荐;若链上信号质量足够,会先进入技术检查。
';var events=(d.events||[]).slice(0,20).map(function(e){var cls=e.direction==='risk'?'risk':e.direction==='positive'?'pos':'blue';return '
'+esc(e.signal_label||e.signal_code)+'
'+esc(e.severity||e.direction)+'
'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+fmtUsd(e.value_usd)+'
'+esc(e.wallet_label||e.counterparty_label||e.source||'链上事件')+'
'}).join('')||'
暂无事件明细
';$('detailBody').innerHTML='
'+esc(d.symbol)+'
'+(d.mappings||[]).length+' 个合约映射 · 近 7 天
'+metrics+recHtml+'
'+events+'
'}catch(e){$('detailBody').innerHTML='
详情加载失败
'}} +async function loadDetail(symbol){state.selected=symbol;$('detailNote').textContent=symbol;$('detailBody').innerHTML='
加载详情...
';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='
'+[['链上分',Number(latest.onchain_score||0).toFixed(0)],['风险分',Number(latest.risk_score||0).toFixed(0)],['映射事件',Number(latest.event_count||latest.mapped_event_count||0).toFixed(0)],['数据源',latest.source||'nodereal']].map(function(x){return '
'+x[0]+''+x[1]+'
'}).join('')+'
';var recHtml=rec?'
主链路状态:'+esc(rec.action_status||rec.execution_status||'观察')+' · 推荐 #'+esc(rec.id)+'
':'
尚未形成主链路推荐;若链上信号质量足够,会先进入技术检查。
';var events=(d.events||[]).slice(0,20).map(function(e){var cls=e.direction==='risk'?'risk':e.direction==='positive'?'pos':'blue';return '
'+esc(e.signal_label||e.signal_code)+'
'+esc(e.severity||e.direction)+'
'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+fmtUsd(e.value_usd)+'
'+esc(e.wallet_label||e.counterparty_label||e.source||'链上事件')+'
'}).join('')||'
暂无事件明细
';$('detailBody').innerHTML='
'+esc(d.symbol)+'
'+(d.mappings||[]).length+' 个合约映射 · 近 7 天
'+metrics+recHtml+'
'+events+'
'}catch(e){$('detailBody').innerHTML='
详情加载失败
'}} function reloadAll(){state.offset=0;state.rawOffset=0;state.selected='';loadOverview();reloadRawEvents(0);reloadTokens(0)} reloadAll(); setInterval(reloadAll,300000); diff --git a/tests/test_onchain_tracking.py b/tests/test_onchain_tracking.py index e538b08..a663c90 100644 --- a/tests/test_onchain_tracking.py +++ b/tests/test_onchain_tracking.py @@ -192,6 +192,46 @@ def test_raw_event_api_and_overview_counts(monkeypatch, tmp_path): assert low.json()["total"] == 0 +def test_overview_ignores_legacy_signals_and_surfaces_mapped_raw_feed(monkeypatch, tmp_path): + _temp_db(monkeypatch, tmp_path) + onchain_db.insert_onchain_event( + { + "chain": "ethereum", + "symbol": "OLD/USDT", + "signal_code": "dex_volume_spike", + "direction": "positive", + "value_usd": 1000000, + "confidence": 80, + "severity": "A", + "detected_at": datetime.now().isoformat(), + "source": "dexscreener", + } + ) + onchain_db.insert_onchain_raw_event( + { + "source": "nodereal", + "chain": "bsc", + "event_type": "evm_transfer", + "token_address": "0xbeam", + "title": "NodeReal ERC-20 原始转账", + "amount": 200, + "total_amount": 200, + "importance": 82, + "mapped_symbol": "BEAM/USDT", + "mapping_status": "mapped", + "detected_at": datetime.now().isoformat(), + } + ) + + overview = onchain_db.get_onchain_overview(hours=24) + + assert overview["kpi"]["positive_events"] == 0 + assert overview["kpi"]["raw_mapped_count"] == 1 + assert overview["kpi"]["mapped_signal_count"] == 1 + assert overview["hot_tokens"][0]["symbol"] == "BEAM/USDT" + assert overview["signals"] == [] + + def test_nodereal_events_generate_metrics_and_normalized_event(monkeypatch, tmp_path): _temp_db(monkeypatch, tmp_path) monkeypatch.setenv("ALPHAX_NODEREAL_API_KEY", "test-key")