diff --git a/backend/paper_config.json b/backend/paper_config.json index 05c40ae..e9f3e43 100644 --- a/backend/paper_config.json +++ b/backend/paper_config.json @@ -1,6 +1,6 @@ { "enabled": true, - "enabled_strategies": ["v51_baseline", "v52_8signals", "v53_alt", "v53_btc"], + "enabled_strategies": ["v51_baseline", "v52_8signals", "v53"], "initial_balance": 10000, "risk_per_trade": 0.02, "max_positions": 4, diff --git a/backend/signal_engine.py b/backend/signal_engine.py index 6bc7cd7..a193071 100644 --- a/backend/signal_engine.py +++ b/backend/signal_engine.py @@ -42,7 +42,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", "v53_alt.json", "v53_btc.json"] +DEFAULT_STRATEGY_FILES = ["v51_baseline.json", "v52_8signals.json", "v53.json"] def load_strategy_configs() -> list[dict]: @@ -453,23 +453,15 @@ class SymbolState: 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逻辑 + # v53 → 统一评分(BTC/ETH/XRP/SOL) + # v53_alt / v53_btc → 兼容旧策略名,转发到 _evaluate_v53() # v51/v52 → 原有代码路径(兼容,不修改) - if track == "ALT" and strategy_name.startswith("v53"): - # 检查symbol限制(v53_alt只跑ALT,不跑BTC) + if strategy_name.startswith("v53"): 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) + return self._evaluate_v53(now_ms, strategy_cfg, snapshot) # ─── 原有V5.1/V5.2评分逻辑(保持不变)──────────────────────── strategy_cfg = strategy_cfg or {} strategy_name = strategy_cfg.get("name", "v51_baseline") @@ -725,13 +717,17 @@ class SymbolState: "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: + def _evaluate_v53(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双重计分问题 + V5.3 统一评分(BTC/ETH/XRP/SOL) + 架构:四层评分 55/25/15/5 + per-symbol 四门控制 + - 门1:波动率下限(atr/price) + - 门2:CVD共振(fast+mid同向) + - 门3:OBI否决(实时WebSocket,fallback DB) + - 门4:期现背离否决(实时WebSocket,fallback DB) + BTC额外:whale_cvd_ratio(>$100k巨鲸CVD) """ - strategy_name = strategy_cfg.get("name", "v53_alt") + strategy_name = strategy_cfg.get("name", "v53") strategy_threshold = int(strategy_cfg.get("threshold", 75)) flip_threshold = int(strategy_cfg.get("flip_threshold", 85)) @@ -741,34 +737,31 @@ class SymbolState: 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 - # ── Per-symbol 四门参数(从 strategy_cfg.symbol_gates 读取)────────── + # ── Per-symbol 四门参数 ──────────────────────────────────── symbol_gates = (strategy_cfg.get("symbol_gates") or {}).get(self.symbol, {}) - min_vol = float(symbol_gates.get("min_vol_threshold", 0.003)) - whale_usd = float(symbol_gates.get("whale_threshold_usd", 50000)) - obi_veto = float(symbol_gates.get("obi_veto_threshold", 0.35)) - spd_veto = float(symbol_gates.get("spot_perp_divergence_veto", 0.005)) + min_vol = float(symbol_gates.get("min_vol_threshold", 0.002)) + whale_usd = float(symbol_gates.get("whale_threshold_usd", 50000)) + obi_veto = float(symbol_gates.get("obi_veto_threshold", 0.35)) + spd_veto = float(symbol_gates.get("spot_perp_divergence_veto", 0.005)) - gate_block = None # 门控否决原因,None = 全部通过 + gate_block = None # 门1:波动率下限 atr_pct_price = atr / price if price > 0 else 0 if atr_pct_price < min_vol: gate_block = f"low_vol({atr_pct_price:.4f}<{min_vol})" - # ── Direction Layer(55分)────────────────────────────────── - # cvd_resonance(30分):fast+mid同向 = 有效方向 + # 门2:CVD共振(方向门) if cvd_fast > 0 and cvd_mid > 0: direction = "LONG" cvd_resonance = 30 @@ -780,25 +773,35 @@ class SymbolState: else: direction = "LONG" if cvd_fast > 0 else "SHORT" cvd_resonance = 0 - no_direction = True # gate: 方向不一致 → 直接不开仓 + no_direction = True + if not gate_block: + gate_block = "no_direction_consensus" - # 门2:鲸鱼推力(大单净方向需与信号方向一致) + # 门3:鲸鱼否决(BTC用whale_cvd_ratio,ALT用大单对立) if not gate_block and not no_direction: - whale_aligned = any( - (direction == "LONG" and lt[2] == 0 and lt[1] * price >= whale_usd) or - (direction == "SHORT" and lt[2] == 1 and lt[1] * price >= whale_usd) - for lt in self.recent_large_trades - ) - whale_adverse = any( - (direction == "LONG" and lt[2] == 1 and lt[1] * price >= whale_usd) or - (direction == "SHORT" and lt[2] == 0 and lt[1] * price >= whale_usd) - for lt in self.recent_large_trades - ) - # 有明确对立大单时否决(没有大单时不否决,给机会) - if whale_adverse and not whale_aligned: - gate_block = f"whale_adverse(>${whale_usd/1000:.0f}k)" + if self.symbol == "BTCUSDT": + # BTC:巨鲸CVD净方向与信号方向冲突时否决 + whale_cvd = self.whale_cvd_ratio if self._whale_trades else to_float(self.market_indicators.get("tiered_cvd_whale")) or 0.0 + whale_threshold_pct = float(symbol_gates.get("whale_flow_threshold_pct", 0.5)) / 100 + if (direction == "LONG" and whale_cvd < -whale_threshold_pct) or \ + (direction == "SHORT" and whale_cvd > whale_threshold_pct): + gate_block = f"whale_cvd_veto({whale_cvd:.3f})" + else: + # ALT:recent_large_trades 里有对立大单则否决 + whale_adverse = any( + (direction == "LONG" and lt[2] == 1 and lt[1] * price >= whale_usd) or + (direction == "SHORT" and lt[2] == 0 and lt[1] * price >= whale_usd) + for lt in self.recent_large_trades + ) + whale_aligned = any( + (direction == "LONG" and lt[2] == 0 and lt[1] * price >= whale_usd) or + (direction == "SHORT" and lt[2] == 1 and lt[1] * price >= whale_usd) + for lt in self.recent_large_trades + ) + if whale_adverse and not whale_aligned: + gate_block = f"whale_adverse(>${whale_usd/1000:.0f}k)" - # 门3:OBI否决(优先实时WebSocket值,fallback DB) + # 门4:OBI否决(实时WS优先,fallback DB) obi_raw = self.rt_obi if self.rt_obi != 0.0 else to_float(self.market_indicators.get("obi_depth_10")) if not gate_block and not no_direction and obi_raw is not None: if direction == "LONG" and obi_raw < -obi_veto: @@ -806,14 +809,16 @@ class SymbolState: elif direction == "SHORT" and obi_raw > obi_veto: gate_block = f"obi_veto({obi_raw:.3f}>{obi_veto})" - # 门4:期现背离否决(优先实时WebSocket值,fallback DB) + # 门5:期现背离否决(实时WS优先,fallback DB) spot_perp_div = self.rt_spot_perp_div if self.rt_spot_perp_div != 0.0 else to_float(self.market_indicators.get("spot_perp_divergence")) if not gate_block and not no_direction and spot_perp_div is not None: if (direction == "LONG" and spot_perp_div < -spd_veto) or \ (direction == "SHORT" and spot_perp_div > spd_veto): gate_block = f"spd_veto({spot_perp_div:.4f})" - # p99_flow_alignment(0/10/20分) + gate_passed = gate_block is None + + # ── Direction Layer(55分)───────────────────────────────── has_adverse_p99 = any( (direction == "LONG" and lt[2] == 1) or (direction == "SHORT" and lt[2] == 0) for lt in self.recent_large_trades @@ -822,25 +827,17 @@ class SymbolState: (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分) + p99_flow = 20 if has_aligned_p99 else (10 if not has_adverse_p99 else 0) 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分)────────────────────────────────── + # ── Crowding Layer(25分)───────────────────────────────── long_short_ratio = to_float(self.market_indicators.get("long_short_ratio")) if long_short_ratio is None: - ls_score = 7 # 缺失给中间分 + 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): @@ -858,13 +855,12 @@ class SymbolState: 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 + # ── Environment Layer(15分)────────────────────────────── + environment_score = round(environment_score_raw / 15 * 15) - # ── Auxiliary Layer(5分)────────────────────────────────── + # ── Auxiliary Layer(5分)──────────────────────────────── coinbase_premium = to_float(self.market_indicators.get("coinbase_premium")) if coinbase_premium is None: aux_score = 2 @@ -877,23 +873,24 @@ class SymbolState: total_score = min(direction_score + crowding_score + environment_score + aux_score, 100) total_score = max(0, round(total_score, 1)) - - # 门控否决时归零分、清方向 - gate_passed = gate_block is None if not gate_passed: total_score = 0 + # whale_cvd for BTC display + whale_cvd_display = (self.whale_cvd_ratio if self._whale_trades else to_float(self.market_indicators.get("tiered_cvd_whale"))) if self.symbol == "BTCUSDT" else None + result.update({ "score": total_score, "direction": direction if (not no_direction and gate_passed) else None, "atr_value": atr_value, "factors": { - "track": "ALT", + "track": "BTC" if self.symbol == "BTCUSDT" else "ALT", "gate_passed": gate_passed, "gate_block": gate_block, "atr_pct_price": round(atr_pct_price, 5), "obi_raw": obi_raw, "spot_perp_div": spot_perp_div, + "whale_cvd_ratio": whale_cvd_display, "direction": { "score": direction_score, "max": 55, "cvd_resonance": cvd_resonance, "p99_flow": p99_flow, "accel_bonus": accel_bonus, @@ -920,114 +917,16 @@ class SymbolState: self.last_signal_dir[strategy_name] = direction return result + def _evaluate_v53_alt(self, now_ms: int, strategy_cfg: dict, snapshot: Optional[dict] = None) -> dict: + """已废弃,由 _evaluate_v53() 统一处理,保留供兼容""" + return self._evaluate_v53(now_ms, strategy_cfg, snapshot) + 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否决 — 优先用实时WebSocket值,回退DB值 - obi_raw = self.rt_obi if self.rt_obi != 0.0 else 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: 期现背离否决 — 优先用实时WebSocket值,回退DB值 - spot_perp_div = self.rt_spot_perp_div if self.rt_spot_perp_div != 0.0 else 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评分作为参考分(不影响门控决策,仅供记录) - # whale_cvd_ratio 优先用实时计算值 - whale_cvd = self.whale_cvd_ratio if self._whale_trades else to_float(self.market_indicators.get("tiered_cvd_whale")) - 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, - "whale_cvd_ratio": whale_cvd, - "alt_score_ref": alt_result["score"], - # 透传ALT层分数供前端展示 - "direction": (alt_result.get("factors") or {}).get("direction"), - "crowding": (alt_result.get("factors") or {}).get("crowding"), - "environment": (alt_result.get("factors") or {}).get("environment"), - "auxiliary": (alt_result.get("factors") or {}).get("auxiliary"), - }, - }) - - 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 + """已废弃,由 _evaluate_v53() 统一处理,保留供兼容""" + return self._evaluate_v53(now_ms, strategy_cfg, snapshot) # ─── PG DB操作 ─────────────────────────────────────────────────── +# ─── PG DB操作 ─────────────────────────────────────────────────── def load_historical(state: SymbolState, window_ms: int): now_ms = int(time.time() * 1000) @@ -1088,18 +987,17 @@ def save_feature_event(ts: int, symbol: str, result: dict, strategy: str): V5.3 专用:每次评分后把 raw features + score 层写入 signal_feature_events。 只对 v53_alt / v53_btc 调用,其他策略跳过。 """ - if strategy not in ("v53_alt", "v53_btc"): + if not strategy.startswith("v53"): return f = result.get("factors") or {} track = f.get("track", "ALT") side = result.get("direction") or ("LONG" if result.get("score", 0) >= 0 else "SHORT") - # ALT层分 - score_direction = (f.get("direction") or {}).get("score", 0) if track == "ALT" else None - score_crowding = (f.get("crowding") or {}).get("score", 0) if track == "ALT" else None - score_env = (f.get("environment") or {}).get("score", 0) if track == "ALT" else None - score_aux = (f.get("auxiliary") or {}).get("score", 0) if track == "ALT" else None - gate_passed = f.get("gate_passed") if track == "BTC" else True # ALT轨无gate概念,视为通过 - block_reason = f.get("block_reason") if track == "BTC" else None + score_direction = (f.get("direction") or {}).get("score", 0) if track == "ALT" else (f.get("direction") or {}).get("score", 0) + score_crowding = (f.get("crowding") or {}).get("score", 0) + score_env = (f.get("environment") or {}).get("score", 0) + score_aux = (f.get("auxiliary") or {}).get("score", 0) + gate_passed = f.get("gate_passed", True) + block_reason = f.get("gate_block") or f.get("block_reason") with get_sync_conn() as conn: with conn.cursor() as cur: diff --git a/backend/strategies/v53_alt.json b/backend/strategies/v53.json similarity index 62% rename from backend/strategies/v53_alt.json rename to backend/strategies/v53.json index 6b424c7..f58b7e1 100644 --- a/backend/strategies/v53_alt.json +++ b/backend/strategies/v53.json @@ -1,8 +1,7 @@ { - "name": "v53_alt", + "name": "v53", "version": "5.3", - "track": "ALT", - "description": "V5.3 ALT轨(ETH/XRP/SOL): 55/25/15/5权重,删除确认层,per-symbol四门控制", + "description": "V5.3 统一策略(BTC/ETH/XRP/SOL): 四层评分 55/25/15/5 + per-symbol 四门控制", "threshold": 75, "flip_threshold": 85, "weights": { @@ -11,25 +10,21 @@ "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"], + "symbols": ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"], "symbol_gates": { + "BTCUSDT": { + "min_vol_threshold": 0.002, + "whale_threshold_usd": 100000, + "whale_flow_threshold_pct": 0.5, + "obi_veto_threshold": 0.30, + "spot_perp_divergence_veto": 0.003 + }, "ETHUSDT": { "min_vol_threshold": 0.003, "whale_threshold_usd": 50000, diff --git a/backend/strategies/v53_btc.json b/backend/strategies/v53_btc.json deleted file mode 100644 index fd57245..0000000 --- a/backend/strategies/v53_btc.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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"] -}