From fcac8c2334f9870fbce4349e1fdf18cff49fde7a Mon Sep 17 00:00:00 2001 From: root Date: Tue, 3 Mar 2026 13:35:23 +0000 Subject: [PATCH] feat: Phase 1 - V5.3 dual-track signal engine (ALT 55/25/15/5, BTC gate-control) --- backend/paper_config.json | 3 +- backend/signal_engine.py | 275 +++++++++++++++++++++++++++++++- backend/strategies/v53_alt.json | 32 ++++ backend/strategies/v53_btc.json | 29 ++++ 4 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 backend/strategies/v53_alt.json create mode 100644 backend/strategies/v53_btc.json diff --git a/backend/paper_config.json b/backend/paper_config.json index db3db3f..05c40ae 100644 --- a/backend/paper_config.json +++ b/backend/paper_config.json @@ -1,6 +1,6 @@ { "enabled": true, - "enabled_strategies": ["v52_8signals"], + "enabled_strategies": ["v51_baseline", "v52_8signals", "v53_alt", "v53_btc"], "initial_balance": 10000, "risk_per_trade": 0.02, "max_positions": 4, @@ -10,3 +10,4 @@ "heavy": 1.5 } } + diff --git a/backend/signal_engine.py b/backend/signal_engine.py index 33dcb36..ce13c6f 100644 --- a/backend/signal_engine.py +++ b/backend/signal_engine.py @@ -38,7 +38,7 @@ logger = logging.getLogger("signal-engine") SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"] LOOP_INTERVAL = 15 # 秒(从5改15,CPU降60%,信号质量无影响) STRATEGY_DIR = os.path.join(os.path.dirname(__file__), "strategies") -DEFAULT_STRATEGY_FILES = ["v51_baseline.json", "v52_8signals.json"] +DEFAULT_STRATEGY_FILES = ["v51_baseline.json", "v52_8signals.json", "v53_alt.json", "v53_btc.json"] def load_strategy_configs() -> list[dict]: @@ -409,6 +409,29 @@ class SymbolState: return None def evaluate_signal(self, now_ms: int, strategy_cfg: Optional[dict] = None, snapshot: Optional[dict] = None) -> dict: + strategy_cfg = strategy_cfg or {} + strategy_name = strategy_cfg.get("name", "v51_baseline") + track = strategy_cfg.get("track", "ALT") + + # ─── Track Router ─────────────────────────────────────────── + # V5.3策略按track路由到专属评分逻辑 + # v53_alt → ALT轨(ETH/XRP/SOL),55/25/15/5,删确认层 + # v53_btc → BTC轨,gate-control逻辑 + # v51/v52 → 原有代码路径(兼容,不修改) + if track == "ALT" and strategy_name.startswith("v53"): + # 检查symbol限制(v53_alt只跑ALT,不跑BTC) + allowed_symbols = strategy_cfg.get("symbols", []) + if allowed_symbols and self.symbol not in allowed_symbols: + snap = snapshot or self.build_evaluation_snapshot(now_ms) + return self._empty_result(strategy_name, snap) + return self._evaluate_v53_alt(now_ms, strategy_cfg, snapshot) + if track == "BTC" and strategy_name.startswith("v53"): + # v53_btc只跑BTC + if self.symbol != "BTCUSDT": + snap = snapshot or self.build_evaluation_snapshot(now_ms) + return self._empty_result(strategy_name, snap) + return self._evaluate_v53_btc(now_ms, strategy_cfg, snapshot) + # ─── 原有V5.1/V5.2评分逻辑(保持不变)──────────────────────── strategy_cfg = strategy_cfg or {} strategy_name = strategy_cfg.get("name", "v51_baseline") strategy_threshold = int(strategy_cfg.get("threshold", 75)) @@ -651,6 +674,256 @@ class SymbolState: self.last_signal_dir[strategy_name] = direction return result + def _empty_result(self, strategy_name: str, snap: dict) -> dict: + """返回空评分结果(symbol不匹配track时使用)""" + return { + "strategy": strategy_name, + "cvd_fast": snap["cvd_fast"], "cvd_mid": snap["cvd_mid"], + "cvd_day": snap["cvd_day"], "cvd_fast_slope": snap["cvd_fast_slope"], + "atr": snap["atr"], "atr_value": snap.get("atr_value", snap["atr"]), + "atr_pct": snap["atr_pct"], "vwap": snap["vwap"], "price": snap["price"], + "p95": snap["p95"], "p99": snap["p99"], + "signal": None, "direction": None, "score": 0, "tier": None, "factors": {}, + } + + def _evaluate_v53_alt(self, now_ms: int, strategy_cfg: dict, snapshot: Optional[dict] = None) -> dict: + """ + V5.3 ALT轨评分(ETH/XRP/SOL) + 权重:Direction 55 | Crowding 25 | Environment 15 | Auxiliary 5 + 删除独立确认层,解决CVD双重计分问题 + """ + strategy_name = strategy_cfg.get("name", "v53_alt") + strategy_threshold = int(strategy_cfg.get("threshold", 75)) + flip_threshold = int(strategy_cfg.get("flip_threshold", 85)) + + snap = snapshot or self.build_evaluation_snapshot(now_ms) + cvd_fast = snap["cvd_fast"] + cvd_mid = snap["cvd_mid"] + price = snap["price"] + atr = snap["atr"] + atr_value = snap.get("atr_value", atr) + atr_pct = snap["atr_pct"] + cvd_fast_accel = snap["cvd_fast_accel"] + environment_score_raw = snap["environment_score"] + + result = self._empty_result(strategy_name, snap) + + if self.warmup or price == 0 or atr == 0: + return result + + last_signal_ts = self.last_signal_ts.get(strategy_name, 0) + in_cooldown = now_ms - last_signal_ts < COOLDOWN_MS + + # ── Direction Layer(55分)────────────────────────────────── + # cvd_resonance(30分):fast+mid同向 = 有效方向 + if cvd_fast > 0 and cvd_mid > 0: + direction = "LONG" + cvd_resonance = 30 + no_direction = False + elif cvd_fast < 0 and cvd_mid < 0: + direction = "SHORT" + cvd_resonance = 30 + no_direction = False + else: + direction = "LONG" if cvd_fast > 0 else "SHORT" + cvd_resonance = 0 + no_direction = True # gate: 方向不一致 → 直接不开仓 + + # p99_flow_alignment(0/10/20分) + has_adverse_p99 = any( + (direction == "LONG" and lt[2] == 1) or (direction == "SHORT" and lt[2] == 0) + for lt in self.recent_large_trades + ) + has_aligned_p99 = any( + (direction == "LONG" and lt[2] == 0) or (direction == "SHORT" and lt[2] == 1) + for lt in self.recent_large_trades + ) + if has_aligned_p99: + p99_flow = 20 + elif not has_adverse_p99: + p99_flow = 10 + else: + p99_flow = 0 + + # cvd_accel_bonus(0/5分) + accel_bonus = 5 if ( + (direction == "LONG" and cvd_fast_accel > 0) or + (direction == "SHORT" and cvd_fast_accel < 0) + ) else 0 + + direction_score = min(cvd_resonance + p99_flow + accel_bonus, 55) + + # ── Crowding Layer(25分)────────────────────────────────── + long_short_ratio = to_float(self.market_indicators.get("long_short_ratio")) + if long_short_ratio is None: + ls_score = 7 # 缺失给中间分 + elif (direction == "SHORT" and long_short_ratio > 2.0) or (direction == "LONG" and long_short_ratio < 0.5): + ls_score = 15 + elif (direction == "SHORT" and long_short_ratio > 1.5) or (direction == "LONG" and long_short_ratio < 0.7): + ls_score = 10 + elif (direction == "SHORT" and long_short_ratio > 1.0) or (direction == "LONG" and long_short_ratio < 1.0): + ls_score = 7 + else: + ls_score = 0 + + top_trader_position = to_float(self.market_indicators.get("top_trader_position")) + if top_trader_position is None: + top_trader_score = 5 + else: + if direction == "LONG": + top_trader_score = 10 if top_trader_position >= 0.55 else (0 if top_trader_position <= 0.45 else 5) + else: + top_trader_score = 10 if top_trader_position <= 0.45 else (0 if top_trader_position >= 0.55 else 5) + + crowding_score = min(ls_score + top_trader_score, 25) + + # ── Environment Layer(15分)──────────────────────────────── + environment_score = round(environment_score_raw / 15 * 15) # 已是0~15 + + # ── Auxiliary Layer(5分)────────────────────────────────── + coinbase_premium = to_float(self.market_indicators.get("coinbase_premium")) + if coinbase_premium is None: + aux_score = 2 + elif (direction == "LONG" and coinbase_premium > 0.0005) or (direction == "SHORT" and coinbase_premium < -0.0005): + aux_score = 5 + elif abs(coinbase_premium) <= 0.0005: + aux_score = 2 + else: + aux_score = 0 + + total_score = min(direction_score + crowding_score + environment_score + aux_score, 100) + total_score = max(0, round(total_score, 1)) + + result.update({ + "score": total_score, + "direction": direction if not no_direction else None, + "atr_value": atr_value, + "factors": { + "track": "ALT", + "direction": { + "score": direction_score, "max": 55, + "cvd_resonance": cvd_resonance, "p99_flow": p99_flow, "accel_bonus": accel_bonus, + }, + "crowding": { + "score": crowding_score, "max": 25, + "lsr_contrarian": ls_score, "top_trader_position": top_trader_score, + }, + "environment": {"score": environment_score, "max": 15}, + "auxiliary": {"score": aux_score, "max": 5, "coinbase_premium": coinbase_premium}, + }, + }) + + if not no_direction and not in_cooldown: + if total_score >= flip_threshold: + result["signal"] = direction + result["tier"] = "heavy" + elif total_score >= strategy_threshold: + result["signal"] = direction + result["tier"] = "standard" + + if result["signal"]: + self.last_signal_ts[strategy_name] = now_ms + self.last_signal_dir[strategy_name] = direction + return result + + def _evaluate_v53_btc(self, now_ms: int, strategy_cfg: dict, snapshot: Optional[dict] = None) -> dict: + """ + V5.3 BTC轨评分(gate-control逻辑) + 不用线性总分,用条件门控+否决条件决定是否开仓 + 新特征(Phase 2采集后启用):tiered_cvd_whale, obi_depth_10, spot_perp_divergence + 当前Phase1版本:用已有特征填充,为数据接入预留扩展点 + """ + strategy_name = strategy_cfg.get("name", "v53_btc") + btc_gate = strategy_cfg.get("btc_gate", {}) + min_vol = btc_gate.get("min_vol_threshold", 0.002) + obi_veto = btc_gate.get("obi_veto_threshold", 0.30) + spot_perp_veto = btc_gate.get("spot_perp_divergence_veto", 0.003) + + snap = snapshot or self.build_evaluation_snapshot(now_ms) + cvd_fast = snap["cvd_fast"] + cvd_mid = snap["cvd_mid"] + price = snap["price"] + atr = snap["atr"] + atr_value = snap.get("atr_value", atr) + atr_pct = snap["atr_pct"] + + result = self._empty_result(strategy_name, snap) + if self.warmup or price == 0 or atr == 0: + return result + + last_signal_ts = self.last_signal_ts.get(strategy_name, 0) + in_cooldown = now_ms - last_signal_ts < COOLDOWN_MS + + block_reason = None + + # Gate 1: 波动率门控(atr_percent_1h = atr/price) + atr_pct_price = atr / price if price > 0 else 0 + if atr_pct_price < min_vol: + block_reason = f"low_vol_regime({atr_pct_price:.4f}<{min_vol})" + + # Gate 2: 方向门控(CVD共振,BTC需要更严格) + if not block_reason: + if cvd_fast > 0 and cvd_mid > 0: + direction = "LONG" + elif cvd_fast < 0 and cvd_mid < 0: + direction = "SHORT" + else: + block_reason = "no_direction_consensus" + direction = "LONG" if cvd_fast > 0 else "SHORT" + + else: + direction = "LONG" if cvd_fast > 0 else "SHORT" + + # Gate 3: OBI否决(Phase2接入obi_depth_10后生效) + obi_raw = to_float(self.market_indicators.get("obi_depth_10")) + if not block_reason and obi_raw is not None: + # obi_raw: 正值=买单占优,负值=卖单占优,[-1,1] + if direction == "LONG" and obi_raw < -obi_veto: + block_reason = f"obi_imbalance_veto(obi={obi_raw:.3f})" + elif direction == "SHORT" and obi_raw > obi_veto: + block_reason = f"obi_imbalance_veto(obi={obi_raw:.3f})" + + # Gate 4: 期现背离否决(Phase2接入spot_perp_divergence后生效) + spot_perp_div = to_float(self.market_indicators.get("spot_perp_divergence")) + if not block_reason and spot_perp_div is not None: + # spot_perp_div: 绝对背离率,如0.005=0.5% + if abs(spot_perp_div) > spot_perp_veto: + # 背离方向与信号方向相反时否决 + if (direction == "LONG" and spot_perp_div < -spot_perp_veto) or \ + (direction == "SHORT" and spot_perp_div > spot_perp_veto): + block_reason = f"spot_perp_divergence_veto({spot_perp_div:.4f})" + + # 所有门控通过后,用ALT评分作为BTC综合评分(暂用,待Phase2换专属特征) + gate_passed = block_reason is None + + # 复用ALT评分作为参考分(不影响门控决策,仅供记录) + alt_result = self._evaluate_v53_alt(now_ms, strategy_cfg, snap) + total_score = alt_result["score"] if gate_passed else 0 + + result.update({ + "score": total_score, + "direction": direction if gate_passed else None, + "atr_value": atr_value, + "factors": { + "track": "BTC", + "gate_passed": gate_passed, + "block_reason": block_reason, + "atr_pct_price": round(atr_pct_price, 5), + "obi_raw": obi_raw, + "spot_perp_div": spot_perp_div, + "alt_score_ref": alt_result["score"], + }, + }) + + strategy_threshold = int(strategy_cfg.get("threshold", 75)) + if gate_passed and not in_cooldown and total_score >= strategy_threshold: + result["signal"] = direction + result["tier"] = "standard" + + if result["signal"]: + self.last_signal_ts[strategy_name] = now_ms + self.last_signal_dir[strategy_name] = direction + return result # ─── PG DB操作 ─────────────────────────────────────────────────── diff --git a/backend/strategies/v53_alt.json b/backend/strategies/v53_alt.json new file mode 100644 index 0000000..b5468ee --- /dev/null +++ b/backend/strategies/v53_alt.json @@ -0,0 +1,32 @@ +{ + "name": "v53_alt", + "version": "5.3", + "track": "ALT", + "description": "V5.3 ALT轨(ETH/XRP/SOL): 55/25/15/5权重,删除确认层(解决CVD双重计分),独立BTC走gate-control", + "threshold": 75, + "flip_threshold": 85, + "weights": { + "direction": 55, + "crowding": 25, + "environment": 15, + "auxiliary": 5 + }, + "direction_sub": { + "cvd_resonance": 30, + "p99_flow_alignment": 20, + "cvd_accel_bonus": 5 + }, + "crowding_sub": { + "lsr_contrarian": 15, + "top_trader_position": 10 + }, + "accel_bonus": 5, + "tp_sl": { + "sl_multiplier": 2.0, + "tp1_multiplier": 1.5, + "tp2_multiplier": 3.0, + "tp_maker": true + }, + "symbols": ["ETHUSDT", "XRPUSDT", "SOLUSDT"], + "signals": ["cvd", "p99", "accel", "ls_ratio", "top_trader", "oi", "coinbase_premium"] +} diff --git a/backend/strategies/v53_btc.json b/backend/strategies/v53_btc.json new file mode 100644 index 0000000..fd57245 --- /dev/null +++ b/backend/strategies/v53_btc.json @@ -0,0 +1,29 @@ +{ + "name": "v53_btc", + "version": "5.3", + "track": "BTC", + "description": "V5.3 BTC轨: gate-control逻辑(不用线性加分),核心特征=tiered_cvd_whale+obi_depth_10+spot_perp_divergence+atr_percent_1h", + "threshold": 75, + "flip_threshold": 85, + "btc_gate": { + "min_vol_threshold": 0.002, + "obi_veto_threshold": 0.30, + "whale_flow_threshold_pct": 0.5, + "spot_perp_divergence_veto": 0.003 + }, + "weights": { + "direction": 55, + "crowding": 25, + "environment": 15, + "auxiliary": 5 + }, + "accel_bonus": 5, + "tp_sl": { + "sl_multiplier": 2.0, + "tp1_multiplier": 1.5, + "tp2_multiplier": 3.0, + "tp_maker": true + }, + "symbols": ["BTCUSDT"], + "signals": ["cvd", "p99", "accel", "ls_ratio", "top_trader", "oi", "coinbase_premium"] +}