alphax/tests/test_onchain_tracking.py
2026-05-17 19:39:11 +08:00

393 lines
14 KiB
Python

import json
import os
import sqlite3
import sys
from datetime import datetime
from fastapi.testclient import TestClient
PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if PROJECT_DIR not in sys.path:
sys.path.insert(0, PROJECT_DIR)
from app.db import altcoin_db, onchain_db, scheduler_db
from app.services import onchain_monitor
from app.web import web_server
def _temp_db(monkeypatch, tmp_path):
db_path = tmp_path / "altcoin_monitor.db"
monkeypatch.setattr(altcoin_db, "DB_PATH", str(db_path))
monkeypatch.setattr(web_server, "init_db", altcoin_db.init_db)
altcoin_db.init_db()
onchain_db.init_onchain_tables()
return db_path
def test_mapping_requires_confidence_and_preserves_multi_chain(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
onchain_db.upsert_token_mapping("ABC", "ethereum", "0xaaa", source="manual", confidence=95)
onchain_db.upsert_token_mapping("ABC", "bsc", "0xbbb", source="manual", confidence=55)
usable = onchain_db.get_token_mappings("ABC", min_confidence=70)
assert len(usable) == 1
assert usable[0]["chain"] == "ethereum"
assert usable[0]["contract_address"] == "0xaaa"
def test_dex_signal_codes_from_metric(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
cfg = {
"dex_volume_spike_pct": 80,
"dex_min_hour_volume_usd": 50000,
"dex_hour_volume_share_pct": 8,
"liquidity_add_pct": 25,
"liquidity_remove_pct": -25,
}
assert onchain_monitor.derive_dex_signals({"dex_volume_change_pct": 120, "liquidity_change_pct": 40}, cfg) == [
"dex_volume_spike",
"liquidity_add",
]
assert onchain_monitor.derive_dex_signals({"dex_volume_change_pct": 10, "liquidity_change_pct": -35}, cfg) == [
"liquidity_remove_risk"
]
assert onchain_monitor.derive_dex_signals(
{"dex_volume_usd": 600000, "dex_volume_1h_usd": 80000, "dex_volume_change_pct": 0, "liquidity_change_pct": 0},
cfg,
) == ["dex_volume_spike"]
def test_auto_mapping_rejects_non_target_native_and_wrapped_tokens(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
cfg = onchain_monitor.get_onchain_params()
chains = set(cfg.get("chains") or [])
avax_pair = {
"chainId": "solana",
"baseToken": {"symbol": "AVAX", "name": "Avalanche"},
"quoteToken": {"symbol": "USDC"},
"liquidity": {"usd": 500000},
"volume": {"h24": 1000000},
"url": "https://example.com",
}
wrapped_pair = {
"chainId": "ethereum",
"baseToken": {"symbol": "FIL", "name": "Wrapped Filecoin"},
"quoteToken": {"symbol": "USDT"},
"liquidity": {"usd": 500000},
"volume": {"h24": 1000000},
"url": "https://example.com/wrapped-filecoin",
}
assert onchain_monitor._pair_rejection_reason(avax_pair, "AVAX/USDT", chains) == "native_chain_not_in_scope"
assert onchain_monitor._pair_rejection_reason(wrapped_pair, "FIL/USDT", chains) == "native_chain_not_in_scope"
assert onchain_monitor._pair_rejection_reason(
{
"chainId": "solana",
"baseToken": {"symbol": "UNKNOWN", "name": "Unknown"},
"quoteToken": {"symbol": "USDC"},
"liquidity": {"usd": 500000},
"volume": {"h24": 1000000},
"url": "https://example.com",
},
"UNKNOWN/USDT",
chains,
) == "solana_not_allowlisted"
def test_onchain_candidate_enqueues_event_news_not_recommendation(monkeypatch, tmp_path):
db_path = _temp_db(monkeypatch, tmp_path)
onchain_db.insert_token_metric(
{
"symbol": "ABC/USDT",
"chain": "ethereum",
"contract_address": "0xaaa",
"window": "1h",
"metric_time": datetime.now().isoformat(),
"dex_volume_usd": 500000,
"dex_volume_change_pct": 160,
"liquidity_usd": 300000,
"liquidity_change_pct": 35,
"onchain_score": 82,
"risk_score": 0,
"source": "test",
}
)
event_id = onchain_db.insert_onchain_event(
{
"chain": "ethereum",
"symbol": "ABC/USDT",
"contract_address": "0xaaa",
"signal_code": "dex_volume_spike",
"direction": "positive",
"value_usd": 500000,
"confidence": 88,
"severity": "A",
"detected_at": datetime.now().isoformat(),
"source": "test",
}
)
result = onchain_monitor.enqueue_onchain_candidates(min_score=70, min_confidence=70, cooldown_hours=6)
assert event_id > 0
assert result["queued"] == 1
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
news = conn.execute("SELECT * FROM event_news WHERE source='onchain'").fetchone()
rec_count = conn.execute("SELECT COUNT(*) FROM recommendation").fetchone()[0]
status = conn.execute("SELECT status FROM onchain_events WHERE id=?", (event_id,)).fetchone()[0]
conn.close()
assert news["event_type"] == "onchain_candidate"
assert json.loads(news["raw_json"])["signal_code"] == "dex_volume_spike"
assert rec_count == 0
assert status == "candidate_queued"
def test_negative_onchain_signal_is_risk_context_only(monkeypatch, tmp_path):
db_path = _temp_db(monkeypatch, tmp_path)
onchain_db.insert_onchain_event(
{
"chain": "ethereum",
"symbol": "RISK/USDT",
"signal_code": "exchange_inflow_risk",
"direction": "risk",
"value_usd": 900000,
"confidence": 92,
"severity": "RISK",
"detected_at": datetime.now().isoformat(),
"source": "test",
}
)
result = onchain_monitor.enqueue_onchain_candidates(min_score=1, min_confidence=1)
conn = sqlite3.connect(db_path)
news_count = conn.execute("SELECT COUNT(*) FROM event_news WHERE source='onchain'").fetchone()[0]
conn.close()
assert result["queued"] == 0
assert news_count == 0
def test_onchain_api_and_page(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
onchain_db.insert_token_metric(
{
"symbol": "ABC/USDT",
"chain": "base",
"contract_address": "0xabc",
"window": "1h",
"metric_time": datetime.now().isoformat(),
"dex_volume_usd": 123000,
"dex_volume_change_pct": 90,
"liquidity_usd": 456000,
"liquidity_change_pct": 12,
"onchain_score": 76,
"risk_score": 8,
"source": "test",
}
)
client = TestClient(web_server.app)
page = client.get("/onchain")
assert page.status_code == 200
assert "链上异动" in page.text
overview = client.get("/api/onchain/overview")
assert overview.status_code == 200
assert overview.json()["kpi"]["token_count"] == 1
assert overview.json()["provider_status"]["providers"][0]["provider"] == "dexscreener"
tokens = client.get("/api/onchain/tokens")
assert tokens.status_code == 200
assert tokens.json()["items"][0]["symbol"] == "ABC/USDT"
provider_status = client.get("/api/onchain/provider-status")
assert provider_status.status_code == 200
assert provider_status.json()["coverage"]["metrics"] == 1
def test_raw_dexscreener_events_store_without_mapping(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
monkeypatch.setenv("ALPHAX_ONCHAIN_ENABLED", "1")
monkeypatch.setenv("ALPHAX_ONCHAIN_CHAINS", "ethereum,solana")
def fake_request(url, params=None, timeout=15):
if "token-profiles" in url:
return [
{
"chainId": "ethereum",
"tokenAddress": "0xraw",
"url": "https://dexscreener.com/ethereum/0xraw",
"description": "Unmapped token started trending",
"icon": "https://example.com/icon.png",
}
]
if "token-boosts/latest" in url:
return [
{
"chainId": "solana",
"tokenAddress": "So111",
"url": "https://dexscreener.com/solana/So111",
"amount": 25,
"totalAmount": 100,
}
]
if "token-boosts/top" in url:
return []
return {"pairs": []}
monkeypatch.setattr(onchain_monitor, "_request_json", fake_request)
result = onchain_monitor.fetch_dexscreener_raw_events(limit=10)
assert len(result["raw_events"]) == 2
raw = onchain_db.list_onchain_raw_events(hours=24)
assert raw["total"] == 2
assert raw["items"][0]["mapping_status"] == "unmapped"
assert raw["items"][0]["event_label"]
def test_raw_event_api_and_overview_counts(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
onchain_db.upsert_token_mapping("ABC", "base", "0xabc", source="manual", confidence=95)
onchain_db.insert_onchain_raw_event(
{
"source": "dexscreener",
"chain": "base",
"event_type": "token_boost_top",
"token_address": "0xabc",
"title": "DEX Boost 榜单",
"amount": 10,
"total_amount": 80,
"importance": 80,
"mapped_symbol": "ABC/USDT",
"mapping_status": "mapped",
"detected_at": datetime.now().isoformat(),
}
)
client = TestClient(web_server.app)
overview = client.get("/api/onchain/overview")
events = client.get("/api/onchain/raw-events")
important = client.get("/api/onchain/raw-events?priority=important")
low = client.get("/api/onchain/raw-events?priority=low")
assert overview.status_code == 200
assert overview.json()["kpi"]["raw_event_count"] == 1
assert overview.json()["kpi"]["raw_mapped_count"] == 1
assert events.status_code == 200
assert events.json()["items"][0]["mapped_symbol"] == "ABC/USDT"
assert important.status_code == 200
assert important.json()["total"] == 0
assert low.status_code == 200
assert low.json()["total"] == 1
def test_etherscan_events_generate_normalized_onchain_event(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
monkeypatch.setenv("ALPHAX_ETHERSCAN_API_KEY", "test-key")
onchain_db.upsert_token_mapping("ABC", "ethereum", "0xabc", source="manual", confidence=95)
onchain_db.insert_token_metric(
{
"symbol": "ABC/USDT",
"chain": "ethereum",
"contract_address": "0xabc",
"window": "1h",
"metric_time": datetime.now().isoformat(),
"dex_volume_usd": 100000,
"liquidity_usd": 100000,
"source": "test",
"raw": {"price_usd": "2"},
}
)
def fake_request(url, params=None, timeout=15):
assert params["chainid"] == "1"
assert params["contractaddress"] == "0xabc"
return {
"status": "1",
"message": "OK",
"result": [
{
"hash": "0xtx",
"timeStamp": "1700000000",
"from": "0xfrom",
"to": "0xto",
"value": "200000000000000000000000",
"tokenDecimal": "18",
}
],
}
monkeypatch.setattr(onchain_monitor, "_request_json", fake_request)
result = onchain_monitor.fetch_etherscan_events(limit=10)
assert result["errors"] == []
assert len(result["events"]) == 1
events = onchain_db.list_onchain_events(hours=50000)
assert events["total"] == 1
assert events["items"][0]["source"] == "etherscan"
assert events["items"][0]["signal_code"] == "whale_accumulation"
def test_helius_events_generate_normalized_onchain_event(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
monkeypatch.setenv("ALPHAX_HELIUS_API_KEY", "test-key")
onchain_db.upsert_token_mapping("SOLX", "solana", "Mint111", source="manual", confidence=95)
onchain_db.insert_token_metric(
{
"symbol": "SOLX/USDT",
"chain": "solana",
"contract_address": "Mint111",
"window": "1h",
"metric_time": datetime.now().isoformat(),
"dex_volume_usd": 100000,
"liquidity_usd": 100000,
"source": "test",
"raw": {"price_usd": "5"},
}
)
def fake_request(url, params=None, timeout=15):
assert "api-key=test-key" in url
return [
{
"signature": "sig111",
"timestamp": 1700000000,
"tokenTransfers": [
{
"mint": "Mint111",
"tokenAmount": 60000,
"fromUserAccount": "fromSol",
"toUserAccount": "toSol",
}
],
"nativeTransfers": [],
}
]
monkeypatch.setattr(onchain_monitor, "_request_json", fake_request)
result = onchain_monitor.fetch_helius_events(limit=10)
assert result["errors"] == []
assert len(result["events"]) == 1
events = onchain_db.list_onchain_events(hours=50000)
assert events["total"] == 1
assert events["items"][0]["source"] == "helius"
assert events["items"][0]["signal_code"] == "whale_accumulation"
def test_scheduler_seeds_onchain_job(monkeypatch, tmp_path):
_temp_db(monkeypatch, tmp_path)
sched_path = tmp_path / "scheduler_state.db"
monkeypatch.setattr(scheduler_db, "SCHEDULER_DB_PATH", str(sched_path))
scheduler_db.init_scheduler_tables()
jobs = {item["job_name"]: item for item in scheduler_db.get_job_configs()}
assert jobs["onchain"]["command"] == "onchain"
assert jobs["onchain"]["lock_group"] == "onchain_write"