feat(v54): signal_engine reads strategy config from DB, writes strategy_id to paper_trades/signal_indicators

This commit is contained in:
root 2026-03-11 15:46:30 +00:00
parent 06552c2b75
commit d3784aaf79

View File

@ -70,6 +70,96 @@ def load_strategy_configs() -> list[dict]:
) )
return configs return configs
def load_strategy_configs_from_db() -> list[dict]:
"""
V5.4: strategies 表读取 running 状态的策略配置
DB 字段映射成现有 JSON 格式保持与 JSON 文件完全兼容
失败时返回空列表调用方应 fallback JSON
内存安全每次读取只返回配置列表无缓存无大对象
"""
try:
with get_sync_conn() as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT
strategy_id::text, display_name, symbol,
cvd_fast_window, cvd_slow_window,
weight_direction, weight_env, weight_aux, weight_momentum,
entry_score,
gate_obi_enabled, obi_threshold,
gate_whale_enabled, whale_cvd_threshold,
gate_vol_enabled, atr_percentile_min,
gate_spot_perp_enabled, spot_perp_threshold,
sl_atr_multiplier, tp1_ratio, tp2_ratio,
timeout_minutes, flip_threshold, direction
FROM strategies
WHERE status = 'running'
ORDER BY created_at ASC
""")
rows = cur.fetchall()
configs = []
for row in rows:
(sid, display_name, symbol,
cvd_fast, cvd_slow,
w_dir, w_env, w_aux, w_mom,
entry_score,
gate_obi, obi_thr,
gate_whale, whale_thr,
gate_vol, atr_pct_min,
gate_spot, spot_thr,
sl_mult, tp1_r, tp2_r,
timeout_min, flip_thr, direction) = row
# 把 display_name 映射回 legacy strategy name用于兼容评分逻辑
# legacy 策略用固定 UUID 识别
LEGACY_UUID_MAP = {
"00000000-0000-0000-0000-000000000053": "v53",
"00000000-0000-0000-0000-000000000054": "v53_middle",
"00000000-0000-0000-0000-000000000055": "v53_fast",
}
strategy_name = LEGACY_UUID_MAP.get(sid, f"custom_{sid[:8]}")
# 构造与 JSON 文件格式兼容的配置 dict
cfg = {
"name": strategy_name,
"strategy_id": sid, # V5.4 新增:用于写 strategy_id 到 DB
"strategy_name_snapshot": display_name, # V5.4 新增:写入时快照名称
"symbol": symbol,
"direction": direction,
"cvd_fast_window": cvd_fast,
"cvd_slow_window": cvd_slow,
"threshold": entry_score,
"weights": {
"direction": w_dir,
"env": w_env,
"aux": w_aux,
"momentum": w_mom,
},
"gates": {
"obi": {"enabled": gate_obi, "threshold": obi_thr},
"whale": {"enabled": gate_whale, "threshold": whale_thr},
"vol": {"enabled": gate_vol, "atr_percentile_min": atr_pct_min},
"spot_perp": {"enabled": gate_spot, "threshold": spot_thr},
},
"tp_sl": {
"sl_multiplier": sl_mult,
"tp1_multiplier": tp1_r,
"tp2_multiplier": tp2_r,
},
"timeout_minutes": timeout_min,
"flip_threshold": flip_thr,
}
configs.append(cfg)
logger.info(f"[DB] 已加载 {len(configs)} 个策略配置: {[c['name'] for c in configs]}")
return configs
except Exception as e:
logger.warning(f"[DB] load_strategy_configs_from_db 失败,将 fallback 到 JSON: {e}")
return []
# ─── 模拟盘配置 ─────────────────────────────────────────────────── # ─── 模拟盘配置 ───────────────────────────────────────────────────
PAPER_TRADING_ENABLED = False # 总开关(兼容旧逻辑) PAPER_TRADING_ENABLED = False # 总开关(兼容旧逻辑)
PAPER_ENABLED_STRATEGIES = [] # 分策略开关: ["v51_baseline", "v52_8signals"] PAPER_ENABLED_STRATEGIES = [] # 分策略开关: ["v51_baseline", "v52_8signals"]
@ -1018,7 +1108,8 @@ def fetch_new_trades(symbol: str, last_id: int) -> list:
for r in cur.fetchall()] for r in cur.fetchall()]
def save_indicator(ts: int, symbol: str, result: dict, strategy: str = "v52_8signals"): def save_indicator(ts: int, symbol: str, result: dict, strategy: str = "v52_8signals",
strategy_id: Optional[str] = None, strategy_name_snapshot: Optional[str] = None):
with get_sync_conn() as conn: with get_sync_conn() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
import json as _json3 import json as _json3
@ -1026,13 +1117,13 @@ def save_indicator(ts: int, symbol: str, result: dict, strategy: str = "v52_8sig
cvd_fast_5m = result.get("cvd_fast_5m") # v53_fast 专用5m窗口CVD其他策略为None cvd_fast_5m = result.get("cvd_fast_5m") # v53_fast 专用5m窗口CVD其他策略为None
cur.execute( cur.execute(
"INSERT INTO signal_indicators " "INSERT INTO signal_indicators "
"(ts,symbol,strategy,cvd_fast,cvd_mid,cvd_day,cvd_fast_slope,atr_5m,atr_percentile,atr_value,vwap_30m,price,p95_qty,p99_qty,score,signal,factors,cvd_fast_5m) " "(ts,symbol,strategy,cvd_fast,cvd_mid,cvd_day,cvd_fast_slope,atr_5m,atr_percentile,atr_value,vwap_30m,price,p95_qty,p99_qty,score,signal,factors,cvd_fast_5m,strategy_id,strategy_name_snapshot) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
(ts, symbol, strategy, result["cvd_fast"], result["cvd_mid"], result["cvd_day"], result["cvd_fast_slope"], (ts, symbol, strategy, result["cvd_fast"], result["cvd_mid"], result["cvd_day"], result["cvd_fast_slope"],
result["atr"], result["atr_pct"], result.get("atr_value", result["atr"]), result["atr"], result["atr_pct"], result.get("atr_value", result["atr"]),
result["vwap"], result["price"], result["vwap"], result["price"],
result["p95"], result["p99"], result["score"], result.get("signal"), factors_json, result["p95"], result["p99"], result["score"], result.get("signal"), factors_json,
cvd_fast_5m) cvd_fast_5m, strategy_id, strategy_name_snapshot)
) )
# 有信号时通知live_executor # 有信号时通知live_executor
if result.get("signal"): if result.get("signal"):
@ -1128,6 +1219,8 @@ def paper_open_trade(
factors: dict = None, factors: dict = None,
strategy: str = "v51_baseline", strategy: str = "v51_baseline",
tp_sl: Optional[dict] = None, tp_sl: Optional[dict] = None,
strategy_id: Optional[str] = None,
strategy_name_snapshot: Optional[str] = None,
): ):
"""模拟开仓""" """模拟开仓"""
import json as _json3 import json as _json3
@ -1158,8 +1251,8 @@ def paper_open_trade(
with get_sync_conn() as conn: with get_sync_conn() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
"INSERT INTO paper_trades (symbol,direction,score,tier,entry_price,entry_ts,tp1_price,tp2_price,sl_price,atr_at_entry,score_factors,strategy,risk_distance) " "INSERT INTO paper_trades (symbol,direction,score,tier,entry_price,entry_ts,tp1_price,tp2_price,sl_price,atr_at_entry,score_factors,strategy,risk_distance,strategy_id,strategy_name_snapshot) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
( (
symbol, symbol,
direction, direction,
@ -1174,6 +1267,8 @@ def paper_open_trade(
_json3.dumps(factors) if factors else None, _json3.dumps(factors) if factors else None,
strategy, strategy,
risk_distance, risk_distance,
strategy_id,
strategy_name_snapshot,
), ),
) )
conn.commit() conn.commit()
@ -1370,7 +1465,11 @@ def start_realtime_ws(states: dict):
def main(): def main():
init_schema() init_schema()
strategy_configs = load_strategy_configs() # V5.4: 优先从 DB 加载策略配置,失败 fallback 到 JSON
strategy_configs = load_strategy_configs_from_db()
if not strategy_configs:
logger.warning("[DB] 未能从 DB 加载策略配置,使用 JSON fallback")
strategy_configs = load_strategy_configs()
strategy_names = [cfg.get("name", "unknown") for cfg in strategy_configs] strategy_names = [cfg.get("name", "unknown") for cfg in strategy_configs]
logger.info(f"已加载策略配置: {', '.join(strategy_names)}") logger.info(f"已加载策略配置: {', '.join(strategy_names)}")
primary_strategy_name = "v52_8signals" if any(cfg.get("name") == "v52_8signals" for cfg in strategy_configs) else strategy_names[0] primary_strategy_name = "v52_8signals" if any(cfg.get("name") == "v52_8signals" for cfg in strategy_configs) else strategy_names[0]
@ -1409,7 +1508,10 @@ def main():
# 每个策略独立存储indicator # 每个策略独立存储indicator
for strategy_cfg, strategy_result in strategy_results: for strategy_cfg, strategy_result in strategy_results:
sname = strategy_cfg.get("name", "v51_baseline") sname = strategy_cfg.get("name", "v51_baseline")
save_indicator(now_ms, sym, strategy_result, strategy=sname) sid = strategy_cfg.get("strategy_id")
ssnap = strategy_cfg.get("strategy_name_snapshot")
save_indicator(now_ms, sym, strategy_result, strategy=sname,
strategy_id=sid, strategy_name_snapshot=ssnap)
save_feature_event(now_ms, sym, strategy_result, strategy=sname) save_feature_event(now_ms, sym, strategy_result, strategy=sname)
# 1m表仍用primary图表用 # 1m表仍用primary图表用
@ -1463,6 +1565,8 @@ def main():
factors=result.get("factors"), factors=result.get("factors"),
strategy=strategy_name, strategy=strategy_name,
tp_sl=strategy_cfg.get("tp_sl"), tp_sl=strategy_cfg.get("tp_sl"),
strategy_id=strategy_cfg.get("strategy_id"),
strategy_name_snapshot=strategy_cfg.get("strategy_name_snapshot"),
) )
# 模拟盘持仓检查由paper_monitor.py通过WebSocket实时处理这里不再检查 # 模拟盘持仓检查由paper_monitor.py通过WebSocket实时处理这里不再检查
@ -1476,7 +1580,12 @@ def main():
if cycle % 10 == 0: if cycle % 10 == 0:
old_strategies = list(PAPER_ENABLED_STRATEGIES) old_strategies = list(PAPER_ENABLED_STRATEGIES)
load_paper_config() load_paper_config()
strategy_configs = load_strategy_configs() # A1: 热重载权重/阈值/TP/SL # V5.4: 热重载优先读 DB失败 fallback 到 JSON
new_configs = load_strategy_configs_from_db()
if new_configs:
strategy_configs = new_configs
else:
strategy_configs = load_strategy_configs() # A1: 热重载权重/阈值/TP/SL
strategy_names = [cfg.get("name", "unknown") for cfg in strategy_configs] strategy_names = [cfg.get("name", "unknown") for cfg in strategy_configs]
primary_strategy_name = "v52_8signals" if any(cfg.get("name") == "v52_8signals" for cfg in strategy_configs) else strategy_names[0] primary_strategy_name = "v52_8signals" if any(cfg.get("name") == "v52_8signals" for cfg in strategy_configs) else strategy_names[0]
if list(PAPER_ENABLED_STRATEGIES) != old_strategies: if list(PAPER_ENABLED_STRATEGIES) != old_strategies: