From 1a450e59d15cf3ac0bbd2b7b387ea6ee67647f24 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 1 Jun 2026 21:11:33 +0800 Subject: [PATCH] 1 --- app/services/onchain_monitor.py | 116 +++++++++++++++++++++++++++++--- app/services/review_engine.py | 12 ++-- 2 files changed, 116 insertions(+), 12 deletions(-) diff --git a/app/services/onchain_monitor.py b/app/services/onchain_monitor.py index 5ac3437..7818dc1 100644 --- a/app/services/onchain_monitor.py +++ b/app/services/onchain_monitor.py @@ -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), diff --git a/app/services/review_engine.py b/app/services/review_engine.py index 7daf867..dc01f66 100644 --- a/app/services/review_engine.py +++ b/app/services/review_engine.py @@ -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 = "升权"