This commit is contained in:
aaron 2026-06-01 21:11:33 +08:00
parent 195e3adb0e
commit 1a450e59d1
2 changed files with 116 additions and 12 deletions

View File

@ -48,6 +48,91 @@ ERC20_SYMBOL_SELECTOR = "0x95d89b41"
ERC20_NAME_SELECTOR = "0x06fdde03"
ERC20_DECIMALS_SELECTOR = "0x313ce567"
# ---------------------------------------------------------------------------
# Known CEX hot/deposit wallet addresses (lowercase).
# Sources: Etherscan/BscScan labeled addresses, Arkham, Nansen public tags.
# Used to classify transfer direction: inflow (to CEX) vs outflow (from CEX).
# ---------------------------------------------------------------------------
_CEX_ADDRESSES: set[str] = {
# Binance
"0x28c6c06298d514db089934071355e5743bf21d60",
"0x21a31ee1afc51d94c2efccaa2092ad1028285549",
"0xdfd5293d8e347dfe59e90efd55b2956a1343963d",
"0x56eddb7aa87536c09ccc2793473599fd21a8b17f",
"0x9696f59e4d72e237be84ffd425dcad154bf96976",
"0xf977814e90da44bfa03b6295a0616a897441acec",
"0x8894e0a0c962cb723c1ef8a1b67f07aa277d42ad",
"0xe2fc31f816a9b94326492132018c3aecc4a93ae1",
"0x3c783c21a0383057d128bae431894a5c19f9cf06",
"0xb38e8c17e38363af6ebdcb3dae12e0243582891d",
"0x5a52e96bacdabb82fd05763e25335261b270efcb",
"0x835678a611b28684005a5e2233695fb6cbbb00a4",
# OKX
"0x6cc5f688a315f3dc28a7781717a9a798a59fda7b",
"0x236f9f97e0e62388479bf9e5ba4889e46b0273c3",
"0xa7efae728d2936e78bda97dc267687568dd593f3",
"0x98ec059dc3adfbdd63429454aeb0c990fba4a128",
"0x6fb624b48d9299674022a23d92515e76ba880113",
# Bybit
"0xf89d7b9c864f589bbf53a82105107622b35eaa40",
"0x1db92e2eebc8e0c075a02bea49a2935bcd2dfcf4",
# Coinbase
"0x71660c4005ba85c37ccec55d0c4493e66fe775d3",
"0x503828976d22510aad0201ac7ec88293211d23da",
"0xddfabcdc4d8ffc6d5beaf154f18b778f892a0740",
"0x3cd751e6b0078be393132286c442345e68ff0aaa",
"0xb5d85cbf7cb3ee0d56b3bb207d5fc4b82f43f511",
"0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43",
# Kraken
"0x2910543af39aba0cd09dbb2d50200b3e800a63d2",
"0x267be1c1d684f78cb4f6a176c4911b741e4ffdc0",
# KuCoin
"0xd6216fc19db775df9774a6e33526131da7d19a2c",
"0xf16e9b0d03470827a95cdfd0cb8a8a3b46969b91",
"0x738cf6903e6c4e699d1c2dd9ab8b67fcdb3121ea",
# Gate.io
"0x0d0707963952f2fba59dd06f2b425ace40b492fe",
"0x1c4b70a3968436b9a0a9cf5205c787eb81bb558c",
# Huobi / HTX
"0xab5c66752a9e8167967685f1450532fb96d5d24f",
"0x6748f50f686bfbca6fe8ad62b22228b87f31ff2b",
"0xfdb16996831753d5331ff813c29a93c76834a0ad",
"0x46340b20830761efd32832a74d7169b29feb9758",
# Bitfinex
"0x876eabf441b2ee5b5b0554fd502a8e0600950cfa",
"0x742d35cc6634c0532925a3b844bc9e7595f2bd3e",
# Crypto.com
"0x6262998ced04146fa42253a5c0af90ca02dfd2a3",
"0x46340b20830761efd32832a74d7169b29feb9758",
# MEXC
"0x3cc936b795a188f0e246cbb2d74c5bd190aecf18",
# Upbit
"0x5e032243d507c743b061ef27c9169ae92ed40ec0",
}
def is_cex_address(address: str) -> bool:
"""Check if an address belongs to a known centralized exchange."""
return str(address or "").lower().strip() in _CEX_ADDRESSES
def classify_transfer_signal(from_addr: str, to_addr: str) -> tuple[str, str]:
"""Classify a transfer's signal code and direction based on CEX address labels.
Returns (signal_code, direction):
- to CEX ("exchange_inflow_risk", "risk") likely selling
- from CEX ("exchange_outflow", "positive") likely accumulating
- neither ("whale_accumulation", "positive") large wallet-to-wallet move
"""
to_is_cex = is_cex_address(to_addr)
from_is_cex = is_cex_address(from_addr)
if to_is_cex and not from_is_cex:
return "exchange_inflow_risk", "risk"
if from_is_cex and not to_is_cex:
return "exchange_outflow", "positive"
# Both CEX (internal transfer) or neither (wallet-to-wallet whale move)
return "whale_accumulation", "positive"
def _env_bool(name, default=False):
value = os.getenv(name)
@ -454,22 +539,37 @@ def _event_from_evm_transfer(log, mapping, cfg=None, source="nodereal"):
return None
chain = str(mapping.get("chain") or "").lower()
tx_hash = str(log.get("transactionHash") or "").strip()
from_addr = _topic_to_address(topics[1])
to_addr = _topic_to_address(topics[2])
sig_code, direction = classify_transfer_signal(from_addr, to_addr)
severity = "RISK" if direction == "risk" else "A"
confidence = 80 if direction == "risk" else 76
# Descriptive labels based on classification
if sig_code == "exchange_inflow_risk":
wallet_label = "CEX 充值地址"
counterparty_label = "发送方 " + _short_addr(from_addr)
elif sig_code == "exchange_outflow":
wallet_label = "接收钱包 " + _short_addr(to_addr)
counterparty_label = "CEX 提币地址"
else:
wallet_label = "EVM 接收地址"
counterparty_label = "EVM 发送地址 " + _short_addr(from_addr)
return {
"chain": chain,
"symbol": mapping.get("symbol"),
"contract_address": mapping.get("contract_address") or "",
"event_type": "token_transfer",
"signal_code": "whale_accumulation",
"signal_label": signal_label("whale_accumulation"),
"direction": "positive",
"signal_code": sig_code,
"signal_label": signal_label(sig_code),
"direction": direction,
"value_usd": value_usd,
"amount": amount,
"tx_hash": tx_hash,
"wallet_address": _topic_to_address(topics[2]),
"wallet_label": "EVM 接收地址",
"counterparty_label": "EVM 发送地址 " + _short_addr(_topic_to_address(topics[1])),
"confidence": 76,
"severity": "A",
"wallet_address": to_addr,
"wallet_label": wallet_label,
"counterparty_label": counterparty_label,
"confidence": confidence,
"severity": severity,
"detected_at": _now().isoformat(timespec="seconds"),
"source": source,
"url": _chain_explorer_tx_url(chain, tx_hash),

View File

@ -624,14 +624,18 @@ def _apply_daily_factor_weight_governance():
new_weight = old_weight
action = ""
if total >= kill_min_samples and hit_rate < kill_hit_rate:
# Expectancy-first governance: avg_pnl is the per-trade expectancy and the
# source of truth for profitability. hit_rate alone must not kill or demote
# a low-hit-rate but high-payoff signal (typical of breakout/trend setups),
# nor promote a high-hit-rate but net-losing one.
if total >= kill_min_samples and avg_pnl <= 0 and hit_rate < kill_hit_rate:
new_weight = 0.0
action = "淘汰"
elif hit_rate < warn_hit_rate or avg_pnl <= -3:
elif avg_pnl <= -3 or (avg_pnl <= 0 and hit_rate < warn_hit_rate):
new_weight = round(max(0.0, old_weight * 0.5), 3)
action = "降权"
elif hit_rate >= 55 and avg_pnl > 0:
target = round(min(4.0, max(old_weight, hit_rate / 50 * base)), 3)
elif avg_pnl > 0 and (hit_rate >= 55 or avg_pnl >= 2):
target = round(min(4.0, max(old_weight, (hit_rate / 50) * base, (1 + avg_pnl / 5) * base)), 3)
if target > old_weight:
new_weight = target
action = "升权"