From b27c3b479c8bfe75cb65e1a094d99d41cf9237f6 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Fri, 14 Aug 2026 20:28:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(live/shadow):=20=E5=AE=9E=E4=BE=8B=E9=94=81?= =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E5=B1=82=E2=80=94=E2=80=94=E6=8C=89=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E5=A4=8D=E5=88=B6=E7=AD=96=E7=95=A5=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=96=87=E4=BB=B6(live=5F3/live=5F5=E5=85=B1=E7=94=A8QMT?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E8=A2=AB=E8=AF=AF=E5=88=A4=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E6=8B=92=E5=90=AF);bullet=5Ftrade=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E9=94=81=E5=88=A4=E9=87=8D=E9=94=AE=3D=E4=B8=BB?= =?UTF-8?q?=E6=9C=BA+strategy=5Fpath+broker+account=5Fidentity,=E5=90=8C?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E5=A4=9A=E7=AD=96=E7=95=A5=E6=98=AF=E5=90=88?= =?UTF-8?q?=E6=B3=95=E5=9C=BA=E6=99=AF=E9=A1=BB=E8=B7=AF=E5=BE=84=E4=BA=92?= =?UTF-8?q?=E5=BC=82=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sanguo_portfolio/runner_live.py | 21 ++++++++++++++++++++- sanguo_trader/shadow/runner.py | 20 +++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/sanguo_portfolio/runner_live.py b/sanguo_portfolio/runner_live.py index 72e68da..cedefd4 100644 --- a/sanguo_portfolio/runner_live.py +++ b/sanguo_portfolio/runner_live.py @@ -96,6 +96,25 @@ def _snapshot_loop(engine: Any, db: str, account_id: int, logger.warning("[live-snapshot] 落库失败 (account=%s): %s", account_id, e) +def _instance_adapter(account_id: str) -> Path: + """按账户复制一份策略适配文件。 + + bullet_trade 实例锁判重键=主机+strategy_path+broker_type+account_identity; + 多实盘共用同一 QMT 账号(合法场景:同账号跑多策略)时 strategy_path 相同会被 + 误判"重复实例"拒启 → 每账户一份副本(内容同、路径异)即视为不同逻辑实例。 + """ + if not account_id: + return ADAPTER_FILE + dst = (Path(__file__).resolve().parent.parent / "runtime" + / f"live_{account_id}" / ADAPTER_FILE.name) + dst.parent.mkdir(parents=True, exist_ok=True) + if not dst.exists() or dst.read_text(encoding="utf-8") != \ + ADAPTER_FILE.read_text(encoding="utf-8"): + import shutil + shutil.copyfile(ADAPTER_FILE, dst) + return dst + + def run_live(provider_config: Dict[str, Any] | None = None) -> None: """装配 LiveEngine(strategy_file=适配文件 + QmtBroker)并 run(阻塞)。""" from bullet_trade.core.live_engine import LiveEngine # type: ignore @@ -116,7 +135,7 @@ def run_live(provider_config: Dict[str, Any] | None = None) -> None: logger.info("QmtBroker 装配 account=%s data_path=%s", cfg["account"], cfg["mini_path"]) engine = LiveEngine( - ADAPTER_FILE, + _instance_adapter(cfg["account_id"]), broker_factory=lambda: broker, # bullet_trade 每实例锁 runtime 目录(单实例设计);多实盘并行须各用独立目录 live_config={"runtime_dir": str( diff --git a/sanguo_trader/shadow/runner.py b/sanguo_trader/shadow/runner.py index 3287aeb..cfc1032 100644 --- a/sanguo_trader/shadow/runner.py +++ b/sanguo_trader/shadow/runner.py @@ -121,6 +121,24 @@ def _snapshot_loop(broker: Any, db: str, account_id: int, logger.warning("[shadow-snapshot] 落库失败 (account=%s): %s", account_id, exc) +def _instance_adapter(account_id: str) -> Path: + """按账户复制一份策略适配文件(同 runner_live._instance_adapter)。 + + bullet_trade 实例锁按 strategy_path+broker_type+account_identity 判重; + 多影子账户共用 ShadowBroker(同 identity)时须路径互异才不被判重复实例。 + """ + if not account_id: + return ADAPTER_FILE + dst = (Path(__file__).resolve().parents[2] / "runtime" + / f"shadow_{account_id}" / ADAPTER_FILE.name) + dst.parent.mkdir(parents=True, exist_ok=True) + if not dst.exists() or dst.read_text(encoding="utf-8") != \ + ADAPTER_FILE.read_text(encoding="utf-8"): + import shutil + shutil.copyfile(ADAPTER_FILE, dst) + return dst + + def run_shadow(provider_config: Optional[Dict[str, Any]] = None) -> None: """装配 LiveEngine(影子 broker)并 run(阻塞)。""" from bullet_trade.core.live_engine import LiveEngine # type: ignore @@ -153,7 +171,7 @@ def run_shadow(provider_config: Optional[Dict[str, Any]] = None) -> None: ) engine = LiveEngine( - ADAPTER_FILE, + _instance_adapter(cfg["account_id"]), broker_factory=lambda: broker, # 独立 runtime 目录:bullet_trade 单实例锁默认 ./runtime, # 影子与实盘并行(双轨§8)会互抢锁,按账户分目录隔离