1
This commit is contained in:
parent
f3463e284e
commit
24859a6acf
@ -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]
|
||||
|
||||
@ -22,9 +22,9 @@
|
||||
<div class="raw-toolbar">
|
||||
<div class="head-actions">
|
||||
<button class="btn" onclick="setRawPriority('important')">重要事件</button>
|
||||
<button class="btn" onclick="setRawPriority('low')">低优先级曝光源</button>
|
||||
<button class="btn" onclick="setRawPriority('low')">低优先级</button>
|
||||
<button class="btn" onclick="setRawPriority('')">全部原始流</button>
|
||||
<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="rawChainSel" onchange="reloadRawEvents(0)"><option value="">全部链</option><option value="ethereum">Ethereum</option><option value="bsc">BSC</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>
|
||||
@ -39,10 +39,10 @@
|
||||
<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>
|
||||
<select class="select" id="chainSel" onchange="reloadTokens(0)"><option value="">全部链</option><option value="ethereum">Ethereum</option><option value="bsc">BSC</option></select>
|
||||
<select class="select" id="signalSel" onchange="reloadTokens(0)"><option value="">全部信号</option><option value="large_token_transfer">大额转账</option><option value="whale_accumulation">鲸鱼增持</option><option value="holder_growth">持有人增长</option><option value="exchange_outflow">交易所流出</option><option value="exchange_inflow_risk">交易所流入</option><option value="holder_concentration_risk">持仓集中风险</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="table-wrap"><table class="table"><thead><tr><th>币种</th><th>链</th><th>链上分</th><th>风险分</th><th>映射事件</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">
|
||||
@ -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 '<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 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 '<span class="badge">'+x[0]+' '+x[1]+'</span>'}).join('');var key=p.api_key_present?'<span class="badge mapped">Key 已配置</span>':'<span class="badge unmapped">无 Key</span>';var impl=p.implemented?'<span class="badge blue">已接入</span>':'<span class="badge warn">待接入</span>';var foot=p.provider==='nodereal'?'主链上数据源:当前负责 EVM Transfer 日志、大额转账和 holder 变化。':(p.status==='已关闭'?'已从主链路移除,保留仅用于兼容和历史数据展示。':'辅助来源:不再作为默认链上主链路。');return '<div class="source-card"><div class="source-top"><div class="source-name">'+esc(p.label||p.provider)+'</div><span class="badge '+statusBadgeClass(p.status)+'">'+esc(p.status||'--')+'</span></div><div class="source-role">'+esc(p.role||'--')+'</div><div class="source-stats">'+stats+key+impl+'</div><div class="source-foot">'+foot+'</div></div>'}
|
||||
function providerCard(p){var stats=[['原始',p.raw_events||0],['指标',p.metrics||0],['信号',p.signals||0]].map(function(x){return '<span class="badge">'+x[0]+' '+x[1]+'</span>'}).join('');var key=p.api_key_present?'<span class="badge mapped">Key 已配置</span>':'<span class="badge unmapped">无 Key</span>';var impl=p.implemented?'<span class="badge blue">已接入</span>':'<span class="badge warn">待接入</span>';var foot='主链上数据源:当前负责 EVM Transfer 日志、大额转账和 holder 变化。';return '<div class="source-card"><div class="source-top"><div class="source-name">'+esc(p.label||p.provider)+'</div><span class="badge '+statusBadgeClass(p.status)+'">'+esc(p.status||'--')+'</span></div><div class="source-role">'+esc(p.role||'--')+'</div><div class="source-stats">'+stats+key+impl+'</div><div class="source-foot">'+foot+'</div></div>'}
|
||||
function renderProviderStatus(s){var providers=s.providers||[];$('providerStatus').innerHTML=providers.length?providers.map(providerCard).join(''):'<div class="empty">暂无数据源状态</div>';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 '<div class="flow-step"><span>'+x[0]+'</span><b>'+x[1]+'</b></div>'}).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 '<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 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 '<div class="kpi"><span>'+x[0]+'</span><b class="'+x[2]+'">'+x[1]+'</b></div>'}).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 '<div class="token-row" onclick="loadDetail(\''+esc(t.symbol)+'\')"><div class="token-main"><div class="sym">'+esc(t.symbol)+'</div><div class="sub">'+sub+'</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);renderProviderStatus(d.provider_status||{})}catch(e){$('kpis').innerHTML='<div class="empty">链上总览加载失败</div>';$('providerStatus').innerHTML='<div class="empty">数据源状态加载失败</div>'}}
|
||||
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?'<a class="raw-link" href="'+esc(e.url)+'" target="_blank" rel="noopener">查看来源</a>':'';var pr=e.priority||'medium';var prBadge=pr==='low'?'<span class="badge">低优先级</span>':pr==='high'?'<span class="badge pos">高优先级</span>':'<span class="badge blue">中优先级</span>';return '<div class="raw-card '+esc(pr)+'"><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 class="raw-meaning">'+esc(e.why_matters||'需要映射和验证后才可能进入主链路。')+'</div></div><div class="raw-meta"><span class="badge">'+esc(e.chain||'--')+'</span><span class="badge">'+esc(e.mapped_symbol||e.token_short||'未知 Token')+'</span>'+prBadge+(amount?'<span class="badge">热度 '+fmtAmount(amount)+'</span>':'')+'<span class="sub">'+fmtTime(e.detected_at)+'</span>'+link+'</div><div class="raw-meaning">'+esc(e.pipeline_note||'未完成映射时仅作为原始观察。')+'</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)+'&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'?'<div class="empty">暂无高价值链上事件。DEX Screener 曝光流已默认隐藏,可手动切换查看。</div>':'<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>'}}
|
||||
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)+'&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'?'<div class="empty">暂无高价值链上事件。</div>':'<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">'+Number(t.event_count||t.mapped_event_count||0).toFixed(0)+'</td><td>'+esc(t.latest_event_at?fmtTime(t.latest_event_at):'--')+'</td><td>'+esc(t.source||'nodereal')+'</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>'}}
|
||||
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)],['映射事件',Number(latest.event_count||latest.mapped_event_count||0).toFixed(0)],['数据源',latest.source||'nodereal']].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);
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user