feat: Phase 1 - V5.3 dual-track signal engine (ALT 55/25/15/5, BTC gate-control)

This commit is contained in:
root 2026-03-03 13:35:23 +00:00
parent 4c43309c16
commit fcac8c2334
4 changed files with 337 additions and 2 deletions

View File

@ -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
}
}

View File

@ -38,7 +38,7 @@ logger = logging.getLogger("signal-engine")
SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]
LOOP_INTERVAL = 15 # 秒从5改15CPU降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/SOL55/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 Layer55分──────────────────────────────────
# cvd_resonance30分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_alignment0/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_bonus0/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 Layer25分──────────────────────────────────
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 Layer15分────────────────────────────────
environment_score = round(environment_score_raw / 15 * 15) # 已是0~15
# ── Auxiliary Layer5分──────────────────────────────────
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操作 ───────────────────────────────────────────────────

View File

@ -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"]
}

View File

@ -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"]
}