update
This commit is contained in:
parent
a22fb1e775
commit
cabd96a875
10
.env.example
10
.env.example
@ -61,17 +61,7 @@ ALPHAX_ONCHAIN_CANDIDATE_ENABLED=1
|
|||||||
ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE=70
|
ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE=70
|
||||||
ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE=70
|
ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE=70
|
||||||
ALPHAX_ONCHAIN_CANDIDATE_COOLDOWN_HOURS=6
|
ALPHAX_ONCHAIN_CANDIDATE_COOLDOWN_HOURS=6
|
||||||
ALPHAX_ONCHAIN_DEXSCREENER_ENABLED=0
|
|
||||||
ALPHAX_ONCHAIN_DEX_VOLUME_SPIKE_PCT=80
|
|
||||||
ALPHAX_ONCHAIN_DEX_MIN_LIQUIDITY_USD=100000
|
|
||||||
ALPHAX_ONCHAIN_DEX_MIN_VOLUME_24H_USD=100000
|
|
||||||
ALPHAX_ONCHAIN_LIQUIDITY_ADD_PCT=25
|
|
||||||
ALPHAX_ONCHAIN_LIQUIDITY_REMOVE_PCT=-25
|
|
||||||
ALPHAX_ONCHAIN_WHALE_TX_USD=250000
|
ALPHAX_ONCHAIN_WHALE_TX_USD=250000
|
||||||
ALPHAX_ETHERSCAN_ENABLED=0
|
|
||||||
ALPHAX_ETHERSCAN_API_KEY=
|
|
||||||
ALPHAX_HELIUS_ENABLED=0
|
|
||||||
ALPHAX_HELIUS_API_KEY=
|
|
||||||
ALPHAX_SYSTEM_ERROR_FEISHU_ENABLED=0
|
ALPHAX_SYSTEM_ERROR_FEISHU_ENABLED=0
|
||||||
ALPHAX_SYSTEM_ERROR_FEISHU_WEBHOOK=
|
ALPHAX_SYSTEM_ERROR_FEISHU_WEBHOOK=
|
||||||
|
|
||||||
|
|||||||
@ -102,7 +102,8 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
|
|||||||
|
|
||||||
- 当前链上主数据源是 NodeReal,入口在 `app/services/nodereal_client.py` 和 `app/services/onchain_monitor.py`。
|
- 当前链上主数据源是 NodeReal,入口在 `app/services/nodereal_client.py` 和 `app/services/onchain_monitor.py`。
|
||||||
- 默认只跑 `ALPHAX_ONCHAIN_PROVIDER=nodereal`,并通过 `ALPHAX_NODEREAL_API_KEY` 访问 EVM JSON-RPC / Enhanced API。
|
- 默认只跑 `ALPHAX_ONCHAIN_PROVIDER=nodereal`,并通过 `ALPHAX_NODEREAL_API_KEY` 访问 EVM JSON-RPC / Enhanced API。
|
||||||
- DEX Screener、Etherscan、Helius 已从默认链路关闭,只保留历史兼容函数和旧数据展示,不应再作为新增链上逻辑的主入口。
|
- DEX Screener、Etherscan、Helius 已从运行时代码链路移除;当前只做 Ethereum / BSC 的 NodeReal 采集。
|
||||||
|
- NodeReal 原始 Transfer 会先记录到 `onchain_raw_events`,再通过 ERC-20 `symbol/name/decimals` 自动尝试映射到交易所 `XXX/USDT`,人工 `ALPHAX_ONCHAIN_TOKEN_MAPPINGS` 只作为兜底。
|
||||||
- 新增链上信号优先落到 `onchain_token_metrics` / `onchain_events`,不要直接创建推荐;高质量事件仍通过 `event_news` 进入技术检查。
|
- 新增链上信号优先落到 `onchain_token_metrics` / `onchain_events`,不要直接创建推荐;高质量事件仍通过 `event_news` 进入技术检查。
|
||||||
|
|
||||||
### 4.2 Web/API
|
### 4.2 Web/API
|
||||||
|
|||||||
@ -7,8 +7,8 @@
|
|||||||
- Web 默认暴露到宿主机 `8191`,容器内端口 `8190`。
|
- Web 默认暴露到宿主机 `8191`,容器内端口 `8190`。
|
||||||
- 运行时数据库是 PostgreSQL,compose 内置 `postgres:16` 服务。
|
- 运行时数据库是 PostgreSQL,compose 内置 `postgres:16` 服务。
|
||||||
- `DATABASE_URL` 是应用唯一运行时数据库连接入口。
|
- `DATABASE_URL` 是应用唯一运行时数据库连接入口。
|
||||||
- 链上主数据源是 NodeReal;`.env` 中配置 `ALPHAX_NODEREAL_API_KEY` 后,`python -m app.cli onchain` 才会产出 NodeReal 链上事件。
|
- 链上主数据源是 NodeReal,只采集 Ethereum / BSC;`.env` 中配置 `ALPHAX_NODEREAL_API_KEY` 后,`python -m app.cli onchain` 才会产出链上事件。
|
||||||
- 生产若出现 `nodereal_no_mappings`,说明 `onchain_token_map` 没有可用合约映射;可在配置中心 `system/onchain.token_mappings` 或 `.env` 的 `ALPHAX_ONCHAIN_TOKEN_MAPPINGS` 写入 `symbol/chain/contract_address` 种子。
|
- NodeReal 原始 Transfer 会先进入 `onchain_raw_events` 展示,再自动读取 ERC-20 metadata 尝试映射交易所 `XXX/USDT`;`ALPHAX_ONCHAIN_TOKEN_MAPPINGS` 只作为人工兜底。
|
||||||
- 调度器以并发子进程运行,并通过业务锁组避免主推荐写入冲突。
|
- 调度器以并发子进程运行,并通过业务锁组避免主推荐写入冲突。
|
||||||
- `.dockerignore` 排除了 `data/`、真实 `.env` 和所有 DB 文件,避免把数据库/密钥打进镜像。
|
- `.dockerignore` 排除了 `data/`、真实 `.env` 和所有 DB 文件,避免把数据库/密钥打进镜像。
|
||||||
|
|
||||||
|
|||||||
@ -90,22 +90,7 @@ def default_onchain_config(default_chains=("ethereum", "bsc")):
|
|||||||
"candidate_min_score": _env_float("ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE", 70),
|
"candidate_min_score": _env_float("ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE", 70),
|
||||||
"candidate_min_confidence": _env_int("ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE", 70),
|
"candidate_min_confidence": _env_int("ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE", 70),
|
||||||
"candidate_cooldown_hours": _env_float("ALPHAX_ONCHAIN_CANDIDATE_COOLDOWN_HOURS", 6),
|
"candidate_cooldown_hours": _env_float("ALPHAX_ONCHAIN_CANDIDATE_COOLDOWN_HOURS", 6),
|
||||||
"dexscreener_enabled": _env_bool("ALPHAX_ONCHAIN_DEXSCREENER_ENABLED", False),
|
|
||||||
"dex_volume_spike_pct": _env_float("ALPHAX_ONCHAIN_DEX_VOLUME_SPIKE_PCT", 80),
|
|
||||||
"dex_min_liquidity_usd": _env_float("ALPHAX_ONCHAIN_DEX_MIN_LIQUIDITY_USD", 100000),
|
|
||||||
"dex_min_volume_24h_usd": _env_float("ALPHAX_ONCHAIN_DEX_MIN_VOLUME_24H_USD", 100000),
|
|
||||||
"dex_min_hour_volume_usd": _env_float("ALPHAX_ONCHAIN_DEX_MIN_HOUR_VOLUME_USD", 50000),
|
|
||||||
"dex_hour_volume_share_pct": _env_float("ALPHAX_ONCHAIN_DEX_HOUR_VOLUME_SHARE_PCT", 8),
|
|
||||||
"liquidity_add_pct": _env_float("ALPHAX_ONCHAIN_LIQUIDITY_ADD_PCT", 25),
|
|
||||||
"liquidity_remove_pct": _env_float("ALPHAX_ONCHAIN_LIQUIDITY_REMOVE_PCT", -25),
|
|
||||||
"whale_tx_usd": _env_float("ALPHAX_ONCHAIN_WHALE_TX_USD", 250000),
|
"whale_tx_usd": _env_float("ALPHAX_ONCHAIN_WHALE_TX_USD", 250000),
|
||||||
"etherscan_enabled": _env_bool("ALPHAX_ETHERSCAN_ENABLED", False),
|
|
||||||
"etherscan_chains": _env_list("ALPHAX_ETHERSCAN_CHAINS", ("ethereum",)),
|
|
||||||
"helius_enabled": _env_bool("ALPHAX_HELIUS_ENABLED", False),
|
|
||||||
"etherscan_base_url": _env_str("ALPHAX_ETHERSCAN_BASE_URL", "https://api.etherscan.io/v2/api"),
|
|
||||||
"helius_base_url": _env_str("ALPHAX_HELIUS_BASE_URL", "https://api.helius.xyz"),
|
|
||||||
"etherscan_api_key_env": "ALPHAX_ETHERSCAN_API_KEY",
|
|
||||||
"helius_api_key_env": "ALPHAX_HELIUS_API_KEY",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -31,28 +31,10 @@ SIGNAL_LABELS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RAW_EVENT_TYPE_LABELS = {
|
RAW_EVENT_TYPE_LABELS = {
|
||||||
"token_profile_latest": "DEX 新币资料变更",
|
|
||||||
"token_boost_latest": "DEX 付费曝光新增",
|
|
||||||
"token_boost_top": "DEX 付费曝光榜",
|
|
||||||
"evm_transfer": "EVM 原始转账",
|
"evm_transfer": "EVM 原始转账",
|
||||||
}
|
}
|
||||||
|
|
||||||
RAW_EVENT_EXPLAINERS = {
|
RAW_EVENT_EXPLAINERS = {
|
||||||
"token_profile_latest": {
|
|
||||||
"plain": "项目方或社区刚在 DEX Screener 更新了代币资料、图标或链接。",
|
|
||||||
"meaning": "只代表曝光资料发生变化,信号较弱,通常不能单独说明有资金买入。",
|
|
||||||
"priority": "low",
|
|
||||||
},
|
|
||||||
"token_boost_latest": {
|
|
||||||
"plain": "有人为这个代币购买了 DEX Screener 曝光位。",
|
|
||||||
"meaning": "代表短期推广热度上升,可能吸引散户注意,但也可能只是营销。",
|
|
||||||
"priority": "medium",
|
|
||||||
},
|
|
||||||
"token_boost_top": {
|
|
||||||
"plain": "这个代币出现在 DEX Screener 付费曝光榜前列。",
|
|
||||||
"meaning": "代表平台内关注度较高,需要再看成交量、流动性和是否能映射交易对。",
|
|
||||||
"priority": "medium",
|
|
||||||
},
|
|
||||||
"evm_transfer": {
|
"evm_transfer": {
|
||||||
"plain": "NodeReal 捕捉到 EVM 链上的 ERC-20 Transfer 原始日志。",
|
"plain": "NodeReal 捕捉到 EVM 链上的 ERC-20 Transfer 原始日志。",
|
||||||
"meaning": "这代表链上确实有资金转移,但没有完成币种映射前,不能直接进入策略候选。",
|
"meaning": "这代表链上确实有资金转移,但没有完成币种映射前,不能直接进入策略候选。",
|
||||||
@ -476,11 +458,6 @@ def get_onchain_provider_status(hours=24):
|
|||||||
hours = int(hours or 24)
|
hours = int(hours or 24)
|
||||||
cutoff = (datetime.now() - timedelta(hours=hours)).isoformat()
|
cutoff = (datetime.now() - timedelta(hours=hours)).isoformat()
|
||||||
nodereal_env = str(cfg.get("nodereal_api_key_env") or "ALPHAX_NODEREAL_API_KEY")
|
nodereal_env = str(cfg.get("nodereal_api_key_env") or "ALPHAX_NODEREAL_API_KEY")
|
||||||
etherscan_env = str(cfg.get("etherscan_api_key_env") or "ALPHAX_ETHERSCAN_API_KEY")
|
|
||||||
helius_env = str(cfg.get("helius_api_key_env") or "ALPHAX_HELIUS_API_KEY")
|
|
||||||
etherscan_chains = cfg.get("etherscan_chains") or ["ethereum"]
|
|
||||||
if isinstance(etherscan_chains, str):
|
|
||||||
etherscan_chains = [x.strip().lower() for x in etherscan_chains.split(",") if x.strip()]
|
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
try:
|
try:
|
||||||
raw_total = conn.execute("SELECT COUNT(*) FROM onchain_raw_events WHERE detected_at >= %s", (cutoff,)).fetchone()[0]
|
raw_total = conn.execute("SELECT COUNT(*) FROM onchain_raw_events WHERE detected_at >= %s", (cutoff,)).fetchone()[0]
|
||||||
@ -544,72 +521,25 @@ def get_onchain_provider_status(hours=24):
|
|||||||
summary = _load(last_onchain.get("summary_json") if last_onchain else "{}", {}) if last_onchain else {}
|
summary = _load(last_onchain.get("summary_json") if last_onchain else "{}", {}) if last_onchain else {}
|
||||||
last_error = last_onchain.get("error_message") if last_onchain else ""
|
last_error = last_onchain.get("error_message") if last_onchain else ""
|
||||||
provider = str(cfg.get("provider") or "nodereal").strip().lower()
|
provider = str(cfg.get("provider") or "nodereal").strip().lower()
|
||||||
dexscreener_enabled = bool(cfg.get("dexscreener_enabled", False)) and provider != "nodereal"
|
nodereal_enabled = bool(cfg.get("nodereal_enabled", True)) and provider == "nodereal"
|
||||||
etherscan_enabled = bool(cfg.get("etherscan_enabled", False)) and provider != "nodereal"
|
nodereal_metrics = int(sum(row["count"] for row in metric_sources if row["source"] == "nodereal"))
|
||||||
helius_enabled = bool(cfg.get("helius_enabled", False)) and provider != "nodereal"
|
nodereal_signals = int(sum(row["count"] for row in signal_sources if row["source"] == "nodereal"))
|
||||||
providers = [
|
providers = [
|
||||||
{
|
{
|
||||||
"provider": "nodereal",
|
"provider": "nodereal",
|
||||||
"label": "NodeReal",
|
"label": "NodeReal",
|
||||||
"enabled": bool(cfg.get("nodereal_enabled", True)) and provider == "nodereal",
|
"enabled": nodereal_enabled,
|
||||||
"api_key_present": bool(os.getenv(nodereal_env, "").strip()),
|
"api_key_present": bool(os.getenv(nodereal_env, "").strip()),
|
||||||
"implemented": True,
|
"implemented": True,
|
||||||
"role": "EVM 主链上数据源:Transfer 日志、大额转账、holder 变化",
|
"role": "EVM 主链上数据源:Transfer 日志、大额转账、holder 变化",
|
||||||
"raw_events": 0,
|
|
||||||
"metrics": int(sum(row["count"] for row in metric_sources if row["source"] == "nodereal")),
|
|
||||||
"signals": int(sum(row["count"] for row in signal_sources if row["source"] == "nodereal")),
|
|
||||||
"status": _provider_status_label(
|
|
||||||
bool(cfg.get("nodereal_enabled", True)),
|
|
||||||
True,
|
|
||||||
int(sum(row["count"] for row in metric_sources if row["source"] == "nodereal"))
|
|
||||||
+ int(sum(row["count"] for row in signal_sources if row["source"] == "nodereal")),
|
|
||||||
last_error if "nodereal" in str(last_error).lower() else "",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"provider": "dexscreener",
|
|
||||||
"label": "DEX Screener",
|
|
||||||
"enabled": dexscreener_enabled,
|
|
||||||
"api_key_present": True,
|
|
||||||
"implemented": True,
|
|
||||||
"role": "低优先级曝光源:Token 资料、付费推广、已映射合约的 DEX 成交量与流动性",
|
|
||||||
"raw_events": int(raw_total or 0),
|
"raw_events": int(raw_total or 0),
|
||||||
"metrics": int(metric_total or 0),
|
"metrics": nodereal_metrics,
|
||||||
"signals": int(sum(row["count"] for row in signal_sources if row["source"] == "dexscreener")),
|
"signals": nodereal_signals,
|
||||||
"status": _provider_status_label(dexscreener_enabled, True, raw_total + metric_total, last_error),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"provider": "etherscan",
|
|
||||||
"label": "Etherscan",
|
|
||||||
"enabled": etherscan_enabled,
|
|
||||||
"api_key_present": bool(os.getenv(etherscan_env, "").strip()),
|
|
||||||
"implemented": True,
|
|
||||||
"role": "EVM 已映射合约的 ERC20 大额转账,当前链: " + ", ".join(etherscan_chains or ["ethereum"]),
|
|
||||||
"raw_events": 0,
|
|
||||||
"metrics": 0,
|
|
||||||
"signals": int(sum(row["count"] for row in signal_sources if row["source"] == "etherscan")),
|
|
||||||
"status": _provider_status_label(
|
"status": _provider_status_label(
|
||||||
etherscan_enabled,
|
nodereal_enabled,
|
||||||
True,
|
True,
|
||||||
int(sum(row["count"] for row in signal_sources if row["source"] == "etherscan")),
|
int(raw_total or 0) + nodereal_metrics + nodereal_signals,
|
||||||
last_error if "etherscan" in str(last_error).lower() else "",
|
last_error if "nodereal" in str(last_error).lower() else "",
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"provider": "helius",
|
|
||||||
"label": "Helius",
|
|
||||||
"enabled": helius_enabled,
|
|
||||||
"api_key_present": bool(os.getenv(helius_env, "").strip()),
|
|
||||||
"implemented": True,
|
|
||||||
"role": "Solana 已映射 mint 的解析交易与大额 token 活动",
|
|
||||||
"raw_events": 0,
|
|
||||||
"metrics": 0,
|
|
||||||
"signals": int(sum(row["count"] for row in signal_sources if row["source"] == "helius")),
|
|
||||||
"status": _provider_status_label(
|
|
||||||
helius_enabled,
|
|
||||||
True,
|
|
||||||
int(sum(row["count"] for row in signal_sources if row["source"] == "helius")),
|
|
||||||
last_error if "helius" in str(last_error).lower() else "",
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -8,9 +8,6 @@ but it never creates recommendations or changes recommendation state directly.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from app.config.system_config import onchain_config
|
from app.config.system_config import onchain_config
|
||||||
from app.db import onchain_db
|
from app.db import onchain_db
|
||||||
@ -36,16 +33,6 @@ from app.services.nodereal_client import DEFAULT_CHAIN_ENDPOINTS, NodeRealClient
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_CHAINS = ("ethereum", "bsc")
|
DEFAULT_CHAINS = ("ethereum", "bsc")
|
||||||
ETHERSCAN_CHAIN_IDS = {
|
|
||||||
"ethereum": "1",
|
|
||||||
"bsc": "56",
|
|
||||||
"base": "8453",
|
|
||||||
"arbitrum": "42161",
|
|
||||||
}
|
|
||||||
SOLANA_AUTO_ALLOWLIST = {
|
|
||||||
"WIF", "BONK", "JUP", "RAY", "PYTH", "PENGU", "JTO", "MEW", "POPCAT", "PNUT",
|
|
||||||
"FARTCOIN", "RENDER", "HNT", "MOBILE", "ORCA", "KMNO", "DRIFT", "TNSR", "IO",
|
|
||||||
}
|
|
||||||
NON_TARGET_NATIVE_BASES = {
|
NON_TARGET_NATIVE_BASES = {
|
||||||
"AVAX", "FIL", "SUI", "APT", "DOT", "ADA", "XRP", "LTC", "BCH", "ATOM", "NEAR",
|
"AVAX", "FIL", "SUI", "APT", "DOT", "ADA", "XRP", "LTC", "BCH", "ATOM", "NEAR",
|
||||||
"SEI", "INJ", "TON", "ETC", "ICP", "HBAR", "ALGO", "VET", "TRX", "XLM", "KAS",
|
"SEI", "INJ", "TON", "ETC", "ICP", "HBAR", "ALGO", "VET", "TRX", "XLM", "KAS",
|
||||||
@ -55,23 +42,6 @@ BRIDGED_TOKEN_MARKERS = (
|
|||||||
"wrapped", "wormhole", "portal", "bridged", "bridge", "axelar", "allbridge",
|
"wrapped", "wormhole", "portal", "bridged", "bridge", "axelar", "allbridge",
|
||||||
"binance-peg", "multichain", "layerzero", "lz", "wavax", "wfil",
|
"binance-peg", "multichain", "layerzero", "lz", "wavax", "wfil",
|
||||||
)
|
)
|
||||||
DEX_CHAIN_ALIASES = {
|
|
||||||
"ethereum": "ethereum",
|
|
||||||
"eth": "ethereum",
|
|
||||||
"bsc": "bsc",
|
|
||||||
"bnb": "bsc",
|
|
||||||
"base": "base",
|
|
||||||
"arbitrum": "arbitrum",
|
|
||||||
"arb": "arbitrum",
|
|
||||||
"solana": "solana",
|
|
||||||
"sol": "solana",
|
|
||||||
}
|
|
||||||
|
|
||||||
DEXSCREENER_RAW_ENDPOINTS = (
|
|
||||||
("token_profile_latest", "https://api.dexscreener.com/token-profiles/latest/v1"),
|
|
||||||
("token_boost_latest", "https://api.dexscreener.com/token-boosts/latest/v1"),
|
|
||||||
("token_boost_top", "https://api.dexscreener.com/token-boosts/top/v1"),
|
|
||||||
)
|
|
||||||
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
||||||
ERC20_SYMBOL_SELECTOR = "0x95d89b41"
|
ERC20_SYMBOL_SELECTOR = "0x95d89b41"
|
||||||
ERC20_NAME_SELECTOR = "0x06fdde03"
|
ERC20_NAME_SELECTOR = "0x06fdde03"
|
||||||
@ -107,15 +77,8 @@ def get_onchain_params():
|
|||||||
chains = [x.strip().lower() for x in chains_raw.split(",") if x.strip()]
|
chains = [x.strip().lower() for x in chains_raw.split(",") if x.strip()]
|
||||||
else:
|
else:
|
||||||
chains = [str(x).strip().lower() for x in chains_raw if str(x).strip()]
|
chains = [str(x).strip().lower() for x in chains_raw if str(x).strip()]
|
||||||
etherscan_env = str(cfg.get("etherscan_api_key_env") or "ALPHAX_ETHERSCAN_API_KEY")
|
|
||||||
helius_env = str(cfg.get("helius_api_key_env") or "ALPHAX_HELIUS_API_KEY")
|
|
||||||
nodereal_env = str(cfg.get("nodereal_api_key_env") or "ALPHAX_NODEREAL_API_KEY")
|
nodereal_env = str(cfg.get("nodereal_api_key_env") or "ALPHAX_NODEREAL_API_KEY")
|
||||||
token_mappings_env = str(cfg.get("token_mappings_env") or "ALPHAX_ONCHAIN_TOKEN_MAPPINGS")
|
token_mappings_env = str(cfg.get("token_mappings_env") or "ALPHAX_ONCHAIN_TOKEN_MAPPINGS")
|
||||||
etherscan_chains_raw = cfg.get("etherscan_chains") or ["ethereum"]
|
|
||||||
if isinstance(etherscan_chains_raw, str):
|
|
||||||
etherscan_chains = [x.strip().lower() for x in etherscan_chains_raw.split(",") if x.strip()]
|
|
||||||
else:
|
|
||||||
etherscan_chains = [str(x).strip().lower() for x in etherscan_chains_raw if str(x).strip()]
|
|
||||||
return {
|
return {
|
||||||
"enabled": bool(cfg.get("enabled", False)),
|
"enabled": bool(cfg.get("enabled", False)),
|
||||||
"provider": str(cfg.get("provider") or "nodereal").strip().lower(),
|
"provider": str(cfg.get("provider") or "nodereal").strip().lower(),
|
||||||
@ -138,22 +101,7 @@ def get_onchain_params():
|
|||||||
"candidate_min_score": float(cfg.get("candidate_min_score") or 70),
|
"candidate_min_score": float(cfg.get("candidate_min_score") or 70),
|
||||||
"candidate_min_confidence": int(cfg.get("candidate_min_confidence") or 70),
|
"candidate_min_confidence": int(cfg.get("candidate_min_confidence") or 70),
|
||||||
"candidate_cooldown_hours": float(cfg.get("candidate_cooldown_hours") or 6),
|
"candidate_cooldown_hours": float(cfg.get("candidate_cooldown_hours") or 6),
|
||||||
"dexscreener_enabled": bool(cfg.get("dexscreener_enabled", True)),
|
|
||||||
"etherscan_enabled": bool(cfg.get("etherscan_enabled", True)),
|
|
||||||
"etherscan_chains": etherscan_chains or ["ethereum"],
|
|
||||||
"helius_enabled": bool(cfg.get("helius_enabled", True)),
|
|
||||||
"dex_volume_spike_pct": float(cfg.get("dex_volume_spike_pct") or 80),
|
|
||||||
"dex_min_liquidity_usd": float(cfg.get("dex_min_liquidity_usd") or 100000),
|
|
||||||
"dex_min_volume_24h_usd": float(cfg.get("dex_min_volume_24h_usd") or 100000),
|
|
||||||
"liquidity_add_pct": float(cfg.get("liquidity_add_pct") or 25),
|
|
||||||
"liquidity_remove_pct": float(cfg.get("liquidity_remove_pct") or -25),
|
|
||||||
"dex_hour_volume_share_pct": float(cfg.get("dex_hour_volume_share_pct") or 8),
|
|
||||||
"dex_min_hour_volume_usd": float(cfg.get("dex_min_hour_volume_usd") or 50000),
|
|
||||||
"whale_tx_usd": float(cfg.get("whale_tx_usd") or 250000),
|
"whale_tx_usd": float(cfg.get("whale_tx_usd") or 250000),
|
||||||
"etherscan_base_url": str(cfg.get("etherscan_base_url") or "https://api.etherscan.io/v2/api").strip(),
|
|
||||||
"helius_base_url": str(cfg.get("helius_base_url") or "https://api.helius.xyz").strip().rstrip("/"),
|
|
||||||
"etherscan_api_key": os.getenv(etherscan_env, "").strip(),
|
|
||||||
"helius_api_key": os.getenv(helius_env, "").strip(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -228,13 +176,6 @@ def _now():
|
|||||||
return datetime.now()
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
def _request_json(url, params=None, timeout=15):
|
|
||||||
resp = requests.get(url, params=params or {}, timeout=timeout, headers={"User-Agent": "AlphaX-Agent-Crypto/1.0"})
|
|
||||||
if resp.status_code >= 400:
|
|
||||||
raise RuntimeError(f"http_{resp.status_code}:{resp.text[:200]}")
|
|
||||||
return resp.json()
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_float(value, default=0.0):
|
def _safe_float(value, default=0.0):
|
||||||
try:
|
try:
|
||||||
return float(value or 0)
|
return float(value or 0)
|
||||||
@ -242,14 +183,6 @@ def _safe_float(value, default=0.0):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _safe_pct_change(new_value, old_value):
|
|
||||||
new_value = _safe_float(new_value)
|
|
||||||
old_value = _safe_float(old_value)
|
|
||||||
if old_value <= 0:
|
|
||||||
return 0.0
|
|
||||||
return (new_value - old_value) / old_value * 100
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_int(value, default=0):
|
def _safe_int(value, default=0):
|
||||||
try:
|
try:
|
||||||
return int(float(value or 0))
|
return int(float(value or 0))
|
||||||
@ -257,11 +190,6 @@ def _safe_int(value, default=0):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _chain_alias(value):
|
|
||||||
key = str(value or "").lower()
|
|
||||||
return DEX_CHAIN_ALIASES.get(key, key)
|
|
||||||
|
|
||||||
|
|
||||||
def _chain_explorer_tx_url(chain, tx_hash):
|
def _chain_explorer_tx_url(chain, tx_hash):
|
||||||
tx_hash = str(tx_hash or "").strip()
|
tx_hash = str(tx_hash or "").strip()
|
||||||
if not tx_hash:
|
if not tx_hash:
|
||||||
@ -293,308 +221,7 @@ def _latest_metric(symbol, chain, contract_address):
|
|||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
def _event_amount(item):
|
def _event_from_metric(metric, signal_code, source="nodereal"):
|
||||||
return _safe_float(item.get("amount"))
|
|
||||||
|
|
||||||
|
|
||||||
def _event_total_amount(item):
|
|
||||||
return _safe_float(item.get("totalAmount") or item.get("total_amount"))
|
|
||||||
|
|
||||||
|
|
||||||
def _raw_importance(event_type, item):
|
|
||||||
amount = _event_amount(item)
|
|
||||||
total = _event_total_amount(item)
|
|
||||||
if event_type == "token_boost_top":
|
|
||||||
return max(total, amount, 1)
|
|
||||||
if event_type == "token_boost_latest":
|
|
||||||
return max(amount, total * 0.5, 1)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_dexscreener_raw_event(item, event_type, cfg=None):
|
|
||||||
cfg = cfg or get_onchain_params()
|
|
||||||
chain = _chain_alias(item.get("chainId"))
|
|
||||||
if chain not in set(cfg.get("chains") or DEFAULT_CHAINS):
|
|
||||||
return None
|
|
||||||
token_address = str(item.get("tokenAddress") or "").strip()
|
|
||||||
if not token_address:
|
|
||||||
return None
|
|
||||||
mapping = find_mapping_by_contract(chain, token_address)
|
|
||||||
links = item.get("links") or []
|
|
||||||
symbol_guess = ""
|
|
||||||
name = ""
|
|
||||||
if isinstance(links, list):
|
|
||||||
for link in links:
|
|
||||||
if not isinstance(link, dict):
|
|
||||||
continue
|
|
||||||
if not name and link.get("label"):
|
|
||||||
name = str(link.get("label") or "")
|
|
||||||
raw = {
|
|
||||||
"chainId": item.get("chainId"),
|
|
||||||
"tokenAddress": token_address,
|
|
||||||
"url": item.get("url") or "",
|
|
||||||
"description": item.get("description") or "",
|
|
||||||
"icon": item.get("icon") or "",
|
|
||||||
"header": item.get("header") or "",
|
|
||||||
"links": links,
|
|
||||||
"amount": item.get("amount"),
|
|
||||||
"totalAmount": item.get("totalAmount"),
|
|
||||||
}
|
|
||||||
title = "DEX Screener"
|
|
||||||
if event_type == "token_profile_latest":
|
|
||||||
title = "DEX 新币资料变更"
|
|
||||||
elif event_type == "token_boost_latest":
|
|
||||||
title = "DEX 付费曝光新增"
|
|
||||||
elif event_type == "token_boost_top":
|
|
||||||
title = "DEX 付费曝光榜"
|
|
||||||
return {
|
|
||||||
"source": "dexscreener",
|
|
||||||
"chain": chain,
|
|
||||||
"event_type": event_type,
|
|
||||||
"token_address": token_address,
|
|
||||||
"symbol_guess": symbol_guess,
|
|
||||||
"name": name,
|
|
||||||
"title": title,
|
|
||||||
"description": item.get("description") or "",
|
|
||||||
"url": item.get("url") or "",
|
|
||||||
"icon": item.get("icon") or "",
|
|
||||||
"amount": _event_amount(item),
|
|
||||||
"total_amount": _event_total_amount(item),
|
|
||||||
"importance": _raw_importance(event_type, item),
|
|
||||||
"mapped_symbol": mapping.get("symbol") if mapping else "",
|
|
||||||
"mapping_status": "mapped" if mapping else "unmapped",
|
|
||||||
"detected_at": _now().isoformat(timespec="seconds"),
|
|
||||||
"raw": raw,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_dexscreener_raw_events(limit=80):
|
|
||||||
cfg = get_onchain_params()
|
|
||||||
if not cfg.get("dexscreener_enabled", True):
|
|
||||||
return {"raw_events": [], "errors": ["dexscreener_disabled"]}
|
|
||||||
inserted = []
|
|
||||||
errors = []
|
|
||||||
per_source_limit = max(1, int(limit or 80))
|
|
||||||
for event_type, url in DEXSCREENER_RAW_ENDPOINTS:
|
|
||||||
try:
|
|
||||||
data = _request_json(url, timeout=cfg.get("timeout", 15))
|
|
||||||
items = data if isinstance(data, list) else data.get("items") or data.get("data") or []
|
|
||||||
for item in items[:per_source_limit]:
|
|
||||||
if not isinstance(item, dict):
|
|
||||||
continue
|
|
||||||
event = normalize_dexscreener_raw_event(item, event_type, cfg=cfg)
|
|
||||||
if not event:
|
|
||||||
continue
|
|
||||||
if insert_onchain_raw_event(event):
|
|
||||||
inserted.append(event)
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"{event_type}:{str(exc)[:160]}")
|
|
||||||
return {"raw_events": inserted, "errors": errors}
|
|
||||||
|
|
||||||
|
|
||||||
def _discover_seed_symbols(limit=120):
|
|
||||||
conn = get_conn()
|
|
||||||
symbols = []
|
|
||||||
try:
|
|
||||||
rows = conn.execute(
|
|
||||||
"""
|
|
||||||
SELECT DISTINCT symbol
|
|
||||||
FROM recommendation
|
|
||||||
WHERE status='active' AND COALESCE(display_bucket,'watch_pool') != 'history'
|
|
||||||
ORDER BY rec_time DESC
|
|
||||||
LIMIT %s
|
|
||||||
""",
|
|
||||||
(int(limit or 120),),
|
|
||||||
).fetchall()
|
|
||||||
symbols.extend([row["symbol"] for row in rows if row["symbol"]])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
rows = conn.execute(
|
|
||||||
"""
|
|
||||||
SELECT DISTINCT symbol
|
|
||||||
FROM coin_state
|
|
||||||
WHERE state != '过期'
|
|
||||||
ORDER BY detected_at DESC
|
|
||||||
LIMIT %s
|
|
||||||
""",
|
|
||||||
(int(limit or 120),),
|
|
||||||
).fetchall()
|
|
||||||
symbols.extend([row["symbol"] for row in rows if row["symbol"]])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
conn.close()
|
|
||||||
seen = set()
|
|
||||||
ordered = []
|
|
||||||
for symbol in symbols:
|
|
||||||
norm = normalize_symbol(symbol)
|
|
||||||
if not norm or norm in seen or not _tradable_symbol(norm):
|
|
||||||
continue
|
|
||||||
seen.add(norm)
|
|
||||||
ordered.append(norm)
|
|
||||||
return ordered[: int(limit or 120)]
|
|
||||||
|
|
||||||
|
|
||||||
def _score_pair_candidate(pair, requested_symbol, chains):
|
|
||||||
base = (pair.get("baseToken") or {})
|
|
||||||
quote = (pair.get("quoteToken") or {})
|
|
||||||
base_symbol = str(base.get("symbol") or "").upper()
|
|
||||||
req_base = str(requested_symbol or "").split("/")[0].upper()
|
|
||||||
liquidity = _safe_float((pair.get("liquidity") or {}).get("usd"))
|
|
||||||
volume = _safe_float((pair.get("volume") or {}).get("h24"))
|
|
||||||
chain = DEX_CHAIN_ALIASES.get(str(pair.get("chainId") or "").lower(), str(pair.get("chainId") or "").lower())
|
|
||||||
score = 0
|
|
||||||
if base_symbol == req_base:
|
|
||||||
score += 50
|
|
||||||
if chain in set(chains or []):
|
|
||||||
score += 15
|
|
||||||
if quote.get("symbol") in ("USDT", "USDC", "USD", "FDUSD", "USDE", "DAI", "USDS"):
|
|
||||||
score += 10
|
|
||||||
if liquidity >= 100000:
|
|
||||||
score += 10
|
|
||||||
if volume >= 100000:
|
|
||||||
score += 10
|
|
||||||
if liquidity >= 500000:
|
|
||||||
score += 5
|
|
||||||
return score
|
|
||||||
|
|
||||||
|
|
||||||
def _pair_rejection_reason(pair, requested_symbol, chains):
|
|
||||||
base = pair.get("baseToken") or {}
|
|
||||||
quote = pair.get("quoteToken") or {}
|
|
||||||
req_base = str(requested_symbol or "").split("/")[0].upper()
|
|
||||||
base_symbol = str(base.get("symbol") or "").upper()
|
|
||||||
base_name = str(base.get("name") or "").lower()
|
|
||||||
pair_url = str(pair.get("url") or "").lower()
|
|
||||||
chain = DEX_CHAIN_ALIASES.get(str(pair.get("chainId") or "").lower(), str(pair.get("chainId") or "").lower())
|
|
||||||
|
|
||||||
if base_symbol != req_base:
|
|
||||||
return "symbol_mismatch"
|
|
||||||
if chain not in set(chains or []):
|
|
||||||
return "chain_not_supported"
|
|
||||||
if req_base in NON_TARGET_NATIVE_BASES:
|
|
||||||
return "native_chain_not_in_scope"
|
|
||||||
if chain == "solana" and req_base not in SOLANA_AUTO_ALLOWLIST:
|
|
||||||
return "solana_not_allowlisted"
|
|
||||||
text = " ".join([base_name, base_symbol.lower(), str(quote.get("symbol") or "").lower(), pair_url])
|
|
||||||
if any(marker in text for marker in BRIDGED_TOKEN_MARKERS):
|
|
||||||
return "bridged_or_wrapped_token"
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def discover_token_mappings(limit=60):
|
|
||||||
cfg = get_onchain_params()
|
|
||||||
chains = set(cfg.get("chains") or DEFAULT_CHAINS)
|
|
||||||
seeds = _discover_seed_symbols(limit=limit)
|
|
||||||
if not seeds:
|
|
||||||
return {"inserted": 0, "candidates": [], "errors": ["no_seed_symbols"]}
|
|
||||||
inserted = []
|
|
||||||
errors = []
|
|
||||||
for symbol in seeds:
|
|
||||||
existing = get_token_mappings(symbol, min_confidence=1, active_only=False)
|
|
||||||
if existing:
|
|
||||||
continue
|
|
||||||
base = symbol.split("/")[0]
|
|
||||||
try:
|
|
||||||
data = _request_json("https://api.dexscreener.com/latest/dex/search", params={"q": base}, timeout=cfg.get("timeout", 15))
|
|
||||||
pairs = data.get("pairs") or []
|
|
||||||
pair_candidates = []
|
|
||||||
for pair in pairs:
|
|
||||||
chain = DEX_CHAIN_ALIASES.get(str(pair.get("chainId") or "").lower(), str(pair.get("chainId") or "").lower())
|
|
||||||
if chain not in chains:
|
|
||||||
continue
|
|
||||||
if _pair_rejection_reason(pair, symbol, chains):
|
|
||||||
continue
|
|
||||||
pair_candidates.append((pair, _score_pair_candidate(pair, symbol, chains)))
|
|
||||||
if not pair_candidates:
|
|
||||||
continue
|
|
||||||
pair_candidates.sort(key=lambda x: (x[1], _safe_float((x[0].get("liquidity") or {}).get("usd")), _safe_float((x[0].get("volume") or {}).get("h24"))), reverse=True)
|
|
||||||
best, score = pair_candidates[0]
|
|
||||||
if score < 55:
|
|
||||||
continue
|
|
||||||
base_token = best.get("baseToken") or {}
|
|
||||||
chain = DEX_CHAIN_ALIASES.get(str(best.get("chainId") or "").lower(), str(best.get("chainId") or "").lower())
|
|
||||||
contract = str(base_token.get("address") or "").strip()
|
|
||||||
if not contract:
|
|
||||||
continue
|
|
||||||
confidence = min(95, 60 + score)
|
|
||||||
mapping_id = onchain_db.upsert_token_mapping(
|
|
||||||
symbol=symbol,
|
|
||||||
chain=chain,
|
|
||||||
contract_address=contract,
|
|
||||||
source="dexscreener_search",
|
|
||||||
confidence=confidence,
|
|
||||||
raw={
|
|
||||||
"search_query": base,
|
|
||||||
"matched_pair": {
|
|
||||||
"pairAddress": best.get("pairAddress") or "",
|
|
||||||
"dexId": best.get("dexId") or "",
|
|
||||||
"url": best.get("url") or "",
|
|
||||||
"liquidity": best.get("liquidity") or {},
|
|
||||||
"volume": best.get("volume") or {},
|
|
||||||
"priceChange": best.get("priceChange") or {},
|
|
||||||
"baseToken": base_token,
|
|
||||||
"quoteToken": best.get("quoteToken") or {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
if mapping_id:
|
|
||||||
inserted.append({"symbol": symbol, "chain": chain, "contract_address": contract, "confidence": confidence})
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"{symbol}:{str(exc)[:160]}")
|
|
||||||
return {"inserted": len(inserted), "candidates": inserted, "errors": errors}
|
|
||||||
|
|
||||||
|
|
||||||
def _score_metric(metric):
|
|
||||||
score = 0.0
|
|
||||||
risk = 0.0
|
|
||||||
vol_change = _safe_float(metric.get("dex_volume_change_pct"))
|
|
||||||
liq_change = _safe_float(metric.get("liquidity_change_pct"))
|
|
||||||
netflow = _safe_float(metric.get("exchange_netflow_usd"))
|
|
||||||
whale = _safe_float(metric.get("whale_accumulation_usd"))
|
|
||||||
smart = _safe_float(metric.get("smart_money_score"))
|
|
||||||
if vol_change > 0:
|
|
||||||
score += min(35, vol_change / 4)
|
|
||||||
if liq_change > 0:
|
|
||||||
score += min(20, liq_change / 3)
|
|
||||||
if netflow < 0:
|
|
||||||
score += min(20, abs(netflow) / 100000)
|
|
||||||
if whale > 0:
|
|
||||||
score += min(20, whale / 100000)
|
|
||||||
score += min(20, smart)
|
|
||||||
if liq_change < 0:
|
|
||||||
risk += min(40, abs(liq_change))
|
|
||||||
if netflow > 0:
|
|
||||||
risk += min(35, netflow / 100000)
|
|
||||||
metric["onchain_score"] = round(min(score, 100), 2)
|
|
||||||
metric["risk_score"] = round(min(risk, 100), 2)
|
|
||||||
return metric
|
|
||||||
|
|
||||||
|
|
||||||
def derive_dex_signals(metric, cfg=None):
|
|
||||||
cfg = cfg or get_onchain_params()
|
|
||||||
signals = []
|
|
||||||
vol_change = _safe_float(metric.get("dex_volume_change_pct"))
|
|
||||||
liq_change = _safe_float(metric.get("liquidity_change_pct"))
|
|
||||||
volume_1h = _safe_float(metric.get("dex_volume_1h_usd"))
|
|
||||||
volume_24h = _safe_float(metric.get("dex_volume_usd"))
|
|
||||||
hour_share_pct = (volume_1h / volume_24h * 100) if volume_24h > 0 else 0
|
|
||||||
if vol_change >= cfg.get("dex_volume_spike_pct", 80):
|
|
||||||
signals.append("dex_volume_spike")
|
|
||||||
elif (
|
|
||||||
volume_1h >= cfg.get("dex_min_hour_volume_usd", 50000)
|
|
||||||
and hour_share_pct >= cfg.get("dex_hour_volume_share_pct", 8)
|
|
||||||
):
|
|
||||||
signals.append("dex_volume_spike")
|
|
||||||
if liq_change >= cfg.get("liquidity_add_pct", 25):
|
|
||||||
signals.append("liquidity_add")
|
|
||||||
if liq_change <= cfg.get("liquidity_remove_pct", -25):
|
|
||||||
signals.append("liquidity_remove_risk")
|
|
||||||
return signals
|
|
||||||
|
|
||||||
|
|
||||||
def _event_from_metric(metric, signal_code, source="dexscreener"):
|
|
||||||
direction = signal_direction(signal_code)
|
direction = signal_direction(signal_code)
|
||||||
severity = "RISK" if direction == "risk" else "A" if _safe_float(metric.get("onchain_score")) >= 75 else "B"
|
severity = "RISK" if direction == "risk" else "A" if _safe_float(metric.get("onchain_score")) >= 75 else "B"
|
||||||
return {
|
return {
|
||||||
@ -615,143 +242,6 @@ def _event_from_metric(metric, signal_code, source="dexscreener"):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def normalize_dexscreener_pair(pair, mapping, cfg=None):
|
|
||||||
cfg = cfg or get_onchain_params()
|
|
||||||
symbol = normalize_symbol(mapping.get("symbol"))
|
|
||||||
chain = DEX_CHAIN_ALIASES.get(str(pair.get("chainId") or mapping.get("chain") or "").lower(), str(mapping.get("chain") or "").lower())
|
|
||||||
contract = mapping.get("contract_address") or (pair.get("baseToken") or {}).get("address") or ""
|
|
||||||
liquidity = _safe_float((pair.get("liquidity") or {}).get("usd"))
|
|
||||||
volume_map = pair.get("volume") or {}
|
|
||||||
volume = _safe_float(volume_map.get("h24"))
|
|
||||||
volume_1h = _safe_float(volume_map.get("h1"))
|
|
||||||
volume_5m = _safe_float(volume_map.get("m5"))
|
|
||||||
volume_6h = _safe_float(volume_map.get("h6"))
|
|
||||||
txns_map = pair.get("txns") or {}
|
|
||||||
txns_h1 = txns_map.get("h1") or {}
|
|
||||||
prev = _latest_metric(symbol, chain, contract)
|
|
||||||
prev_volume = _safe_float(prev.get("dex_volume_usd") if prev else 0)
|
|
||||||
prev_liquidity = _safe_float(prev.get("liquidity_usd") if prev else 0)
|
|
||||||
metric = {
|
|
||||||
"symbol": symbol,
|
|
||||||
"chain": chain,
|
|
||||||
"contract_address": contract,
|
|
||||||
"window": "1h",
|
|
||||||
"metric_time": _now().isoformat(timespec="seconds"),
|
|
||||||
"dex_volume_usd": volume,
|
|
||||||
"dex_volume_1h_usd": volume_1h,
|
|
||||||
"dex_volume_5m_usd": volume_5m,
|
|
||||||
"dex_volume_6h_usd": volume_6h,
|
|
||||||
"dex_volume_1h_share_pct": round(volume_1h / volume * 100, 2) if volume > 0 else 0,
|
|
||||||
"dex_volume_change_pct": _safe_pct_change(volume, prev_volume),
|
|
||||||
"liquidity_usd": liquidity,
|
|
||||||
"liquidity_change_pct": _safe_pct_change(liquidity, prev_liquidity),
|
|
||||||
"exchange_netflow_usd": 0,
|
|
||||||
"whale_accumulation_usd": 0,
|
|
||||||
"holder_delta": 0,
|
|
||||||
"smart_money_score": 0,
|
|
||||||
"source": "dexscreener",
|
|
||||||
"url": pair.get("url") or "",
|
|
||||||
"raw": {
|
|
||||||
"pair_address": pair.get("pairAddress") or "",
|
|
||||||
"dex_id": pair.get("dexId") or "",
|
|
||||||
"price_usd": pair.get("priceUsd") or "",
|
|
||||||
"fdv": pair.get("fdv") or 0,
|
|
||||||
"txns": pair.get("txns") or {},
|
|
||||||
"price_change": pair.get("priceChange") or {},
|
|
||||||
"volume": pair.get("volume") or {},
|
|
||||||
"liquidity": pair.get("liquidity") or {},
|
|
||||||
"derived": {
|
|
||||||
"dex_volume_1h_usd": volume_1h,
|
|
||||||
"dex_volume_5m_usd": volume_5m,
|
|
||||||
"dex_volume_6h_usd": volume_6h,
|
|
||||||
"dex_volume_1h_share_pct": round(volume_1h / volume * 100, 2) if volume > 0 else 0,
|
|
||||||
"h1_buys": int(txns_h1.get("buys") or 0),
|
|
||||||
"h1_sells": int(txns_h1.get("sells") or 0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return _score_metric(metric)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_dexscreener_metrics(limit=60):
|
|
||||||
cfg = get_onchain_params()
|
|
||||||
if not cfg.get("dexscreener_enabled", True):
|
|
||||||
return {"metrics": [], "events": [], "errors": ["dexscreener_disabled"]}
|
|
||||||
mappings = get_token_mappings(min_confidence=MIN_MAPPING_CONFIDENCE)
|
|
||||||
bootstrap = None
|
|
||||||
if not mappings:
|
|
||||||
bootstrap = discover_token_mappings(limit=limit)
|
|
||||||
mappings = get_token_mappings(min_confidence=MIN_MAPPING_CONFIDENCE)
|
|
||||||
metrics = []
|
|
||||||
events = []
|
|
||||||
errors = []
|
|
||||||
if bootstrap:
|
|
||||||
errors.extend(bootstrap.get("errors") or [])
|
|
||||||
for mapping in mappings[: int(limit or 60)]:
|
|
||||||
symbol = normalize_symbol(mapping.get("symbol"))
|
|
||||||
if not symbol or not _tradable_symbol(symbol):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
url = "https://api.dexscreener.com/latest/dex/tokens/" + str(mapping.get("contract_address") or "").strip()
|
|
||||||
data = _request_json(url, timeout=cfg.get("timeout", 15))
|
|
||||||
pairs = data.get("pairs") or []
|
|
||||||
wanted_chain = DEX_CHAIN_ALIASES.get(str(mapping.get("chain") or "").lower(), str(mapping.get("chain") or "").lower())
|
|
||||||
pairs = [p for p in pairs if DEX_CHAIN_ALIASES.get(str(p.get("chainId") or "").lower(), str(p.get("chainId") or "").lower()) == wanted_chain]
|
|
||||||
if not pairs:
|
|
||||||
continue
|
|
||||||
best = max(pairs, key=lambda p: _safe_float((p.get("liquidity") or {}).get("usd")))
|
|
||||||
metric = normalize_dexscreener_pair(best, mapping, cfg=cfg)
|
|
||||||
if metric.get("liquidity_usd", 0) < cfg.get("dex_min_liquidity_usd", 100000) and metric.get("dex_volume_usd", 0) < cfg.get("dex_min_volume_24h_usd", 100000):
|
|
||||||
insert_token_metric(metric)
|
|
||||||
metrics.append(metric)
|
|
||||||
continue
|
|
||||||
insert_token_metric(metric)
|
|
||||||
metrics.append(metric)
|
|
||||||
for code in derive_dex_signals(metric, cfg):
|
|
||||||
event = _event_from_metric(metric, code, source="dexscreener")
|
|
||||||
if insert_onchain_event(event):
|
|
||||||
events.append(event)
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"{symbol}:{str(exc)[:160]}")
|
|
||||||
return {"metrics": metrics, "events": events, "errors": errors}
|
|
||||||
|
|
||||||
|
|
||||||
def _event_from_etherscan_transfer(row, mapping, cfg=None):
|
|
||||||
cfg = cfg or get_onchain_params()
|
|
||||||
decimals = _safe_int(row.get("tokenDecimal"), 18)
|
|
||||||
amount = _safe_float(row.get("value")) / (10 ** decimals if decimals >= 0 else 1)
|
|
||||||
price_usd = _latest_price_from_metric(mapping)
|
|
||||||
value_usd = amount * price_usd if price_usd > 0 else 0
|
|
||||||
threshold = _safe_float(cfg.get("whale_tx_usd"), 250000)
|
|
||||||
if value_usd > 0 and value_usd < threshold:
|
|
||||||
return None
|
|
||||||
if value_usd <= 0 and amount <= 0:
|
|
||||||
return None
|
|
||||||
tx_hash = str(row.get("hash") or "").strip()
|
|
||||||
chain = str(mapping.get("chain") or "").lower()
|
|
||||||
return {
|
|
||||||
"chain": chain,
|
|
||||||
"symbol": mapping.get("symbol"),
|
|
||||||
"contract_address": mapping.get("contract_address") or "",
|
|
||||||
"event_type": "token_transfer",
|
|
||||||
"signal_code": "whale_accumulation" if value_usd >= threshold else "large_token_transfer",
|
|
||||||
"signal_label": signal_label("whale_accumulation" if value_usd >= threshold else "large_token_transfer"),
|
|
||||||
"direction": "positive" if value_usd >= threshold else "neutral",
|
|
||||||
"value_usd": value_usd,
|
|
||||||
"amount": amount,
|
|
||||||
"tx_hash": tx_hash,
|
|
||||||
"wallet_address": row.get("to") or "",
|
|
||||||
"wallet_label": "EVM 接收地址",
|
|
||||||
"counterparty_label": "EVM 发送地址 " + _short_addr(row.get("from") or ""),
|
|
||||||
"confidence": 74 if value_usd >= threshold else 58,
|
|
||||||
"severity": "A" if value_usd >= threshold else "B",
|
|
||||||
"detected_at": _ts_to_iso(row.get("timeStamp")),
|
|
||||||
"source": "etherscan",
|
|
||||||
"url": _chain_explorer_tx_url(chain, tx_hash),
|
|
||||||
"raw": row,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _latest_price_from_metric(mapping):
|
def _latest_price_from_metric(mapping):
|
||||||
symbol = normalize_symbol(mapping.get("symbol"))
|
symbol = normalize_symbol(mapping.get("symbol"))
|
||||||
chain = str(mapping.get("chain") or "").lower()
|
chain = str(mapping.get("chain") or "").lower()
|
||||||
@ -1179,141 +669,6 @@ def fetch_nodereal_raw_events(client=None, cfg=None, limit=60):
|
|||||||
return {"raw_events": inserted, "errors": errors}
|
return {"raw_events": inserted, "errors": errors}
|
||||||
|
|
||||||
|
|
||||||
def fetch_etherscan_events(limit=60):
|
|
||||||
cfg = get_onchain_params()
|
|
||||||
if not cfg.get("etherscan_enabled", True):
|
|
||||||
return {"events": [], "errors": ["etherscan_disabled"]}
|
|
||||||
api_key = cfg.get("etherscan_api_key") or ""
|
|
||||||
if not api_key:
|
|
||||||
return {"events": [], "errors": ["etherscan_api_key_missing"]}
|
|
||||||
allowed_chains = set(cfg.get("etherscan_chains") or ["ethereum"])
|
|
||||||
mappings = [
|
|
||||||
m for m in get_token_mappings(min_confidence=MIN_MAPPING_CONFIDENCE)
|
|
||||||
if m.get("chain") in ETHERSCAN_CHAIN_IDS and m.get("chain") in allowed_chains
|
|
||||||
]
|
|
||||||
events = []
|
|
||||||
errors = []
|
|
||||||
for mapping in mappings[: int(limit or 60)]:
|
|
||||||
chain = str(mapping.get("chain") or "").lower()
|
|
||||||
contract = str(mapping.get("contract_address") or "").strip()
|
|
||||||
if not contract:
|
|
||||||
continue
|
|
||||||
params = {
|
|
||||||
"chainid": ETHERSCAN_CHAIN_IDS[chain],
|
|
||||||
"module": "account",
|
|
||||||
"action": "tokentx",
|
|
||||||
"contractaddress": contract,
|
|
||||||
"page": 1,
|
|
||||||
"offset": 25,
|
|
||||||
"sort": "desc",
|
|
||||||
"apikey": api_key,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
data = _request_json(cfg.get("etherscan_base_url"), params=params, timeout=cfg.get("timeout", 15))
|
|
||||||
status = str(data.get("status") or "")
|
|
||||||
message = str(data.get("message") or "")
|
|
||||||
rows = data.get("result") or []
|
|
||||||
if status == "0" and not isinstance(rows, list):
|
|
||||||
if "No transactions found" not in str(rows) and "No records" not in str(rows):
|
|
||||||
errors.append(f"{mapping.get('symbol')}:etherscan_{message}:{str(rows)[:100]}")
|
|
||||||
continue
|
|
||||||
if not isinstance(rows, list):
|
|
||||||
continue
|
|
||||||
for row in rows:
|
|
||||||
if not isinstance(row, dict):
|
|
||||||
continue
|
|
||||||
event = _event_from_etherscan_transfer(row, mapping, cfg=cfg)
|
|
||||||
if not event:
|
|
||||||
continue
|
|
||||||
if insert_onchain_event(event):
|
|
||||||
events.append(event)
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"{mapping.get('symbol')}:etherscan:{str(exc)[:160]}")
|
|
||||||
return {"events": events, "errors": errors}
|
|
||||||
|
|
||||||
|
|
||||||
def _event_from_helius_tx(tx, mapping, cfg=None):
|
|
||||||
cfg = cfg or get_onchain_params()
|
|
||||||
mint = str(mapping.get("contract_address") or "")
|
|
||||||
symbol = normalize_symbol(mapping.get("symbol"))
|
|
||||||
signature = str(tx.get("signature") or "")
|
|
||||||
token_transfers = tx.get("tokenTransfers") or []
|
|
||||||
native_transfers = tx.get("nativeTransfers") or []
|
|
||||||
matched = [t for t in token_transfers if str(t.get("mint") or "") == mint]
|
|
||||||
if not matched:
|
|
||||||
return None
|
|
||||||
amount = max(_safe_float(t.get("tokenAmount")) for t in matched)
|
|
||||||
price_usd = _latest_price_from_metric(mapping)
|
|
||||||
value_usd = amount * price_usd if price_usd > 0 else 0
|
|
||||||
sol_amount = max([_safe_float(t.get("amount")) / 1_000_000_000 for t in native_transfers] or [0])
|
|
||||||
threshold = _safe_float(cfg.get("whale_tx_usd"), 250000)
|
|
||||||
if value_usd > 0 and value_usd < threshold and sol_amount < 100:
|
|
||||||
return None
|
|
||||||
signal = "whale_accumulation" if value_usd >= threshold or sol_amount >= 100 else "large_token_transfer"
|
|
||||||
return {
|
|
||||||
"chain": "solana",
|
|
||||||
"symbol": symbol,
|
|
||||||
"contract_address": mint,
|
|
||||||
"event_type": "solana_token_activity",
|
|
||||||
"signal_code": signal,
|
|
||||||
"signal_label": signal_label(signal),
|
|
||||||
"direction": "positive" if signal == "whale_accumulation" else "neutral",
|
|
||||||
"value_usd": value_usd,
|
|
||||||
"amount": amount,
|
|
||||||
"tx_hash": signature,
|
|
||||||
"wallet_address": (matched[0].get("toUserAccount") or matched[0].get("userAccount") or ""),
|
|
||||||
"wallet_label": "Solana 接收地址",
|
|
||||||
"counterparty_label": "Solana 发送地址 " + _short_addr(matched[0].get("fromUserAccount") or ""),
|
|
||||||
"confidence": 74 if signal == "whale_accumulation" else 58,
|
|
||||||
"severity": "A" if signal == "whale_accumulation" else "B",
|
|
||||||
"detected_at": _ts_to_iso(tx.get("timestamp")),
|
|
||||||
"source": "helius",
|
|
||||||
"url": _chain_explorer_tx_url("solana", signature),
|
|
||||||
"raw": tx,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_helius_events(limit=60):
|
|
||||||
cfg = get_onchain_params()
|
|
||||||
if not cfg.get("helius_enabled", True):
|
|
||||||
return {"events": [], "errors": ["helius_disabled"]}
|
|
||||||
api_key = cfg.get("helius_api_key") or ""
|
|
||||||
if not api_key:
|
|
||||||
return {"events": [], "errors": ["helius_api_key_missing"]}
|
|
||||||
mappings = [m for m in get_token_mappings(min_confidence=MIN_MAPPING_CONFIDENCE) if m.get("chain") == "solana"]
|
|
||||||
events = []
|
|
||||||
errors = []
|
|
||||||
for mapping in mappings[: int(limit or 60)]:
|
|
||||||
mint = str(mapping.get("contract_address") or "").strip()
|
|
||||||
if not mint:
|
|
||||||
continue
|
|
||||||
query = urlencode({"api-key": api_key, "limit": 25})
|
|
||||||
url = f"{cfg.get('helius_base_url')}/v0/addresses/{mint}/transactions?{query}"
|
|
||||||
try:
|
|
||||||
data = _request_json(url, timeout=cfg.get("timeout", 15))
|
|
||||||
rows = data if isinstance(data, list) else data.get("transactions") or []
|
|
||||||
for tx in rows:
|
|
||||||
if not isinstance(tx, dict):
|
|
||||||
continue
|
|
||||||
event = _event_from_helius_tx(tx, mapping, cfg=cfg)
|
|
||||||
if not event:
|
|
||||||
continue
|
|
||||||
if insert_onchain_event(event):
|
|
||||||
events.append(event)
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"{mapping.get('symbol')}:helius:{str(exc)[:160]}")
|
|
||||||
return {"events": events, "errors": errors}
|
|
||||||
|
|
||||||
|
|
||||||
def _ts_to_iso(value):
|
|
||||||
try:
|
|
||||||
if value:
|
|
||||||
return datetime.fromtimestamp(float(value)).isoformat(timespec="seconds")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return _now().isoformat(timespec="seconds")
|
|
||||||
|
|
||||||
|
|
||||||
def _short_addr(value):
|
def _short_addr(value):
|
||||||
value = str(value or "")
|
value = str(value or "")
|
||||||
if len(value) <= 12:
|
if len(value) <= 12:
|
||||||
@ -1502,16 +857,10 @@ def run_once(limit=60):
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"POSITIVE_SIGNALS",
|
"POSITIVE_SIGNALS",
|
||||||
"RISK_SIGNALS",
|
"RISK_SIGNALS",
|
||||||
"derive_dex_signals",
|
|
||||||
"enqueue_onchain_candidates",
|
"enqueue_onchain_candidates",
|
||||||
"fetch_dexscreener_metrics",
|
|
||||||
"fetch_dexscreener_raw_events",
|
|
||||||
"fetch_etherscan_events",
|
|
||||||
"fetch_helius_events",
|
|
||||||
"fetch_nodereal_events",
|
"fetch_nodereal_events",
|
||||||
"get_onchain_params",
|
"get_onchain_params",
|
||||||
"ingest_normalized_events",
|
"ingest_normalized_events",
|
||||||
"normalize_dexscreener_pair",
|
|
||||||
"run_once",
|
"run_once",
|
||||||
"seed_configured_token_mappings",
|
"seed_configured_token_mappings",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -36,66 +36,12 @@ def test_mapping_requires_confidence_and_preserves_multi_chain(monkeypatch, tmp_
|
|||||||
assert usable[0]["contract_address"] == "0xaaa"
|
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):
|
def test_auto_mapping_rejects_non_target_native_and_wrapped_tokens(monkeypatch, tmp_path):
|
||||||
_temp_db(monkeypatch, tmp_path)
|
_temp_db(monkeypatch, tmp_path)
|
||||||
monkeypatch.setenv("ALPHAX_ONCHAIN_CHAINS", "ethereum,solana")
|
assert onchain_monitor._is_auto_mapping_symbol_allowed("STORJ", "Storj") is True
|
||||||
cfg = onchain_monitor.get_onchain_params()
|
assert onchain_monitor._is_auto_mapping_symbol_allowed("AVAX", "Avalanche") is False
|
||||||
chains = set(cfg.get("chains") or [])
|
assert onchain_monitor._is_auto_mapping_symbol_allowed("FIL", "Wrapped Filecoin") is False
|
||||||
|
assert onchain_monitor._is_auto_mapping_symbol_allowed("USDT", "Tether USD") is False
|
||||||
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):
|
def test_onchain_candidate_enqueues_event_news_not_recommendation(monkeypatch, tmp_path):
|
||||||
@ -210,58 +156,16 @@ def test_onchain_api_and_page(monkeypatch, tmp_path):
|
|||||||
assert provider_status.json()["coverage"]["metrics"] == 1
|
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")
|
|
||||||
monkeypatch.setenv("ALPHAX_ONCHAIN_DEXSCREENER_ENABLED", "1")
|
|
||||||
|
|
||||||
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):
|
def test_raw_event_api_and_overview_counts(monkeypatch, tmp_path):
|
||||||
_temp_db(monkeypatch, tmp_path)
|
_temp_db(monkeypatch, tmp_path)
|
||||||
onchain_db.upsert_token_mapping("ABC", "base", "0xabc", source="manual", confidence=95)
|
onchain_db.upsert_token_mapping("ABC", "ethereum", "0xabc", source="manual", confidence=95)
|
||||||
onchain_db.insert_onchain_raw_event(
|
onchain_db.insert_onchain_raw_event(
|
||||||
{
|
{
|
||||||
"source": "dexscreener",
|
"source": "nodereal",
|
||||||
"chain": "base",
|
"chain": "ethereum",
|
||||||
"event_type": "token_boost_top",
|
"event_type": "evm_transfer",
|
||||||
"token_address": "0xabc",
|
"token_address": "0xabc",
|
||||||
"title": "DEX Boost 榜单",
|
"title": "NodeReal ERC-20 原始转账",
|
||||||
"amount": 10,
|
"amount": 10,
|
||||||
"total_amount": 80,
|
"total_amount": 80,
|
||||||
"importance": 80,
|
"importance": 80,
|
||||||
@ -283,9 +187,9 @@ def test_raw_event_api_and_overview_counts(monkeypatch, tmp_path):
|
|||||||
assert events.status_code == 200
|
assert events.status_code == 200
|
||||||
assert events.json()["items"][0]["mapped_symbol"] == "ABC/USDT"
|
assert events.json()["items"][0]["mapped_symbol"] == "ABC/USDT"
|
||||||
assert important.status_code == 200
|
assert important.status_code == 200
|
||||||
assert important.json()["total"] == 0
|
assert important.json()["total"] == 1
|
||||||
assert low.status_code == 200
|
assert low.status_code == 200
|
||||||
assert low.json()["total"] == 1
|
assert low.json()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_nodereal_events_generate_metrics_and_normalized_event(monkeypatch, tmp_path):
|
def test_nodereal_events_generate_metrics_and_normalized_event(monkeypatch, tmp_path):
|
||||||
@ -515,16 +419,6 @@ def test_nodereal_seeds_configured_token_mappings_from_env(monkeypatch, tmp_path
|
|||||||
assert mappings[0]["contract_address"] == "0xabc"
|
assert mappings[0]["contract_address"] == "0xabc"
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_helius_is_disabled_by_default(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)
|
|
||||||
result = onchain_monitor.fetch_helius_events(limit=10)
|
|
||||||
|
|
||||||
assert result["events"] == []
|
|
||||||
assert result["errors"] == ["helius_disabled"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_scheduler_seeds_onchain_job(monkeypatch, tmp_path):
|
def test_scheduler_seeds_onchain_job(monkeypatch, tmp_path):
|
||||||
_temp_db(monkeypatch, tmp_path)
|
_temp_db(monkeypatch, tmp_path)
|
||||||
sched_path = tmp_path / "scheduler_state.db"
|
sched_path = tmp_path / "scheduler_state.db"
|
||||||
|
|||||||
@ -216,25 +216,24 @@ def test_llm_system_config_overrides_env_defaults(monkeypatch):
|
|||||||
|
|
||||||
def test_onchain_system_config_overrides_env(monkeypatch):
|
def test_onchain_system_config_overrides_env(monkeypatch):
|
||||||
monkeypatch.setenv("ALPHAX_ONCHAIN_ENABLED", "0")
|
monkeypatch.setenv("ALPHAX_ONCHAIN_ENABLED", "0")
|
||||||
monkeypatch.setenv("TEST_ETHERSCAN_KEY", "etherscan-secret")
|
monkeypatch.setenv("TEST_NODEREAL_KEY", "nodereal-secret")
|
||||||
set_config("system", "onchain", {
|
set_config("system", "onchain", {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"chains": ["base", "solana"],
|
"chains": ["ethereum", "bsc"],
|
||||||
"timeout": 9,
|
"timeout": 9,
|
||||||
"candidate_min_score": 88,
|
"candidate_min_score": 88,
|
||||||
"dex_min_liquidity_usd": 123456,
|
"nodereal_api_key_env": "TEST_NODEREAL_KEY",
|
||||||
"etherscan_api_key_env": "TEST_ETHERSCAN_KEY",
|
"nodereal_raw_max_logs_per_chain": 12,
|
||||||
"helius_api_key_env": "TEST_HELIUS_KEY",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
params = get_onchain_params()
|
params = get_onchain_params()
|
||||||
|
|
||||||
assert params["enabled"] is True
|
assert params["enabled"] is True
|
||||||
assert params["chains"] == ["base", "solana"]
|
assert params["chains"] == ["ethereum", "bsc"]
|
||||||
assert params["timeout"] == 9
|
assert params["timeout"] == 9
|
||||||
assert params["candidate_min_score"] == 88
|
assert params["candidate_min_score"] == 88
|
||||||
assert params["dex_min_liquidity_usd"] == 123456
|
assert params["nodereal_api_key"] == "nodereal-secret"
|
||||||
assert params["etherscan_api_key"] == "etherscan-secret"
|
assert params["nodereal_raw_max_logs_per_chain"] == 12
|
||||||
|
|
||||||
|
|
||||||
def test_paper_trading_system_config_controls_account_model(monkeypatch):
|
def test_paper_trading_system_config_controls_account_model(monkeypatch):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user