From a9c3523a245cd48a2b6c6feb7cf1555764356766 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 1 Mar 2026 12:43:46 +0000 Subject: [PATCH] feat: independent strategy paper trading controls - Each strategy has its own position count (max 4 each) - paper_config.enabled_strategies: per-strategy toggle - is_strategy_enabled() checks both master switch and strategy list - API: /api/paper/config now supports enabled_strategies array - Config auto-loads on API startup Usage: POST /api/paper/config {enabled_strategies: ['v52_8signals']} --- backend/main.py | 14 ++++++++++++-- backend/signal_engine.py | 37 +++++++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/backend/main.py b/backend/main.py index 16894d9..c53513d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -597,12 +597,23 @@ async def get_signal_trades( # 模拟盘配置状态(与signal_engine共享的运行时状态) paper_config = { "enabled": False, + "enabled_strategies": [], # 分策略开关: ["v51_baseline", "v52_8signals"] "initial_balance": 10000, "risk_per_trade": 0.02, "max_positions": 4, "tier_multiplier": {"light": 0.5, "standard": 1.0, "heavy": 1.5}, } +# 启动时加载已有配置 +_config_path = os.path.join(os.path.dirname(__file__), "paper_config.json") +if os.path.exists(_config_path): + try: + with open(_config_path, "r") as _f: + _saved = json.load(_f) + paper_config.update(_saved) + except Exception: + pass + @app.get("/api/paper/config") async def paper_get_config(user: dict = Depends(get_current_user)): @@ -616,11 +627,10 @@ async def paper_set_config(request: Request, user: dict = Depends(get_current_us if user.get("role") != "admin": raise HTTPException(status_code=403, detail="仅管理员可修改") body = await request.json() - for k in ["enabled", "initial_balance", "risk_per_trade", "max_positions"]: + for k in ["enabled", "enabled_strategies", "initial_balance", "risk_per_trade", "max_positions"]: if k in body: paper_config[k] = body[k] # 写入配置文件让signal_engine也能读到 - import json config_path = os.path.join(os.path.dirname(__file__), "paper_config.json") with open(config_path, "w") as f: json.dump(paper_config, f, indent=2) diff --git a/backend/signal_engine.py b/backend/signal_engine.py index dff9345..dc7eec2 100644 --- a/backend/signal_engine.py +++ b/backend/signal_engine.py @@ -67,10 +67,11 @@ def load_strategy_configs() -> list[dict]: return configs # ─── 模拟盘配置 ─────────────────────────────────────────────────── -PAPER_TRADING_ENABLED = False # 开关(范总确认后通过API开启) +PAPER_TRADING_ENABLED = False # 总开关(兼容旧逻辑) +PAPER_ENABLED_STRATEGIES = [] # 分策略开关: ["v51_baseline", "v52_8signals"] PAPER_INITIAL_BALANCE = 10000 # 虚拟初始资金 USDT PAPER_RISK_PER_TRADE = 0.02 # 单笔风险 2%(即200U) -PAPER_MAX_POSITIONS = 4 # 最大同时持仓数 +PAPER_MAX_POSITIONS = 4 # 每套策略最大同时持仓数 PAPER_TIER_MULTIPLIER = { # 档位仓位倍数 "light": 0.5, # 轻仓: 1% "standard": 1.0, # 标准: 2% @@ -80,18 +81,29 @@ PAPER_FEE_RATE = 0.0005 # Taker手续费 0.05%(开仓+平仓各一 def load_paper_config(): """从配置文件加载模拟盘开关和参数""" - global PAPER_TRADING_ENABLED, PAPER_INITIAL_BALANCE, PAPER_RISK_PER_TRADE, PAPER_MAX_POSITIONS + global PAPER_TRADING_ENABLED, PAPER_ENABLED_STRATEGIES, PAPER_INITIAL_BALANCE, PAPER_RISK_PER_TRADE, PAPER_MAX_POSITIONS config_path = os.path.join(os.path.dirname(__file__), "paper_config.json") try: with open(config_path, "r") as f: import json as _json2 cfg = _json2.load(f) PAPER_TRADING_ENABLED = cfg.get("enabled", False) + PAPER_ENABLED_STRATEGIES = cfg.get("enabled_strategies", []) PAPER_INITIAL_BALANCE = cfg.get("initial_balance", 10000) PAPER_RISK_PER_TRADE = cfg.get("risk_per_trade", 0.02) PAPER_MAX_POSITIONS = cfg.get("max_positions", 4) except FileNotFoundError: pass + + +def is_strategy_enabled(strategy_name: str) -> bool: + """检查某策略是否启用模拟盘""" + if not PAPER_TRADING_ENABLED: + return False + # 如果enabled_strategies为空,走旧逻辑(全部启用) + if not PAPER_ENABLED_STRATEGIES: + return True + return strategy_name in PAPER_ENABLED_STRATEGIES # ───────────────────────────────────────────────────────────────── # 窗口大小(毫秒) @@ -870,11 +882,14 @@ def paper_close_by_signal(symbol: str, current_price: float, now_ms: int, strate conn.commit() -def paper_active_count() -> int: - """当前所有币种活跃持仓总数""" +def paper_active_count(strategy: Optional[str] = None) -> int: + """当前活跃持仓总数(按策略独立计数)""" with get_sync_conn() as conn: with conn.cursor() as cur: - cur.execute("SELECT COUNT(*) FROM paper_trades WHERE status IN ('active','tp1_hit')") + if strategy: + cur.execute("SELECT COUNT(*) FROM paper_trades WHERE strategy=%s AND status IN ('active','tp1_hit')", (strategy,)) + else: + cur.execute("SELECT COUNT(*) FROM paper_trades WHERE status IN ('active','tp1_hit')") return cur.fetchone()[0] @@ -929,9 +944,11 @@ def main(): last_1m_save[sym] = bar_1m # 反向信号平仓:按策略独立判断,score>=75才触发 - if PAPER_TRADING_ENABLED and warmup_cycles <= 0: + if warmup_cycles <= 0: for strategy_cfg, result in strategy_results: strategy_name = strategy_cfg.get("name", "v51_baseline") + if not is_strategy_enabled(strategy_name): + continue eval_dir = result.get("direction") existing_dir = paper_get_active_direction(sym, strategy_name) if existing_dir and eval_dir and existing_dir != eval_dir and result["score"] >= 75: @@ -948,10 +965,10 @@ def main(): f"[{sym}] 🚨 信号[{strategy_name}]: {result['signal']} " f"score={result['score']} price={result['price']:.1f}" ) - # 模拟盘开仓(需开关开启 + 跳过冷启动) - if PAPER_TRADING_ENABLED and warmup_cycles <= 0: + # 模拟盘开仓(需该策略启用 + 跳过冷启动) + if is_strategy_enabled(strategy_name) and warmup_cycles <= 0: if not paper_has_active_position(sym, strategy_name): - active_count = paper_active_count() + active_count = paper_active_count(strategy_name) if active_count < PAPER_MAX_POSITIONS: tier = result.get("tier", "standard") paper_open_trade(