diff --git a/config/data_platform.yaml b/config/data_platform.yaml index cd25e5c..51e6919 100644 --- a/config/data_platform.yaml +++ b/config/data_platform.yaml @@ -41,9 +41,11 @@ performance: risk_free_rate: 0.02 # 实盘集成(D期,spec §5)— 默认关闭,D-4a 联调再开 -# bridge_token 走环境变量 BRIDGE_TOKEN,不写入 yaml(不进 git) +# bridge_token 优先从 config 读,fallback 环境变量 BRIDGE_TOKEN live: enabled: false # 总开关(false=影子分支整个跳过,live_step 行为不变) bridge_url: https://bridge.mysanguo.top shadow: true # 模式A影子下单(模拟撮合为准,信号同步POST bridge影子) mode_b: false # D-4c 模式B: bridge回报校正账本(默认关,切实盘再开) + # 和 Windows bridge 同值;不进 git。占位空值,真实值部署时填实际 config + bridge_token: diff --git a/sanguo_trader/live_orchestrator.py b/sanguo_trader/live_orchestrator.py index c1618cb..a720f32 100644 --- a/sanguo_trader/live_orchestrator.py +++ b/sanguo_trader/live_orchestrator.py @@ -187,9 +187,9 @@ def _shadow_trades_to_bridge(db_path: str, account_id: int, today: str, cfg) -> live_cfg = getattr(cfg, "live", None) or {} if not live_cfg.get("enabled"): return - token = os.environ.get("BRIDGE_TOKEN") + token = live_cfg.get("bridge_token") or os.environ.get("BRIDGE_TOKEN") if not token: - logger.warning("live_step %s: 影子下单启用但 BRIDGE_TOKEN 未设,跳过", account_id) + logger.warning("live_step %s: 影子下单启用但 bridge_token(config 或 BRIDGE_TOKEN env)未设,跳过", account_id) return url = live_cfg.get("bridge_url") if not url: @@ -248,9 +248,9 @@ def reconcile_from_bridge(db_path: str, account_id: int, today: str, return if not live_cfg.get("mode_b"): return - token = os.environ.get("BRIDGE_TOKEN") + token = live_cfg.get("bridge_token") or os.environ.get("BRIDGE_TOKEN") if not token: - logger.warning("reconcile %s: mode_b 启用但 BRIDGE_TOKEN 未设,跳过", account_id) + logger.warning("reconcile %s: mode_b 启用但 bridge_token(config 或 BRIDGE_TOKEN env)未设,跳过", account_id) return url = live_cfg.get("bridge_url") if not url: diff --git a/tests/trader/test_reconcile.py b/tests/trader/test_reconcile.py index fc32dc4..ff7cfc8 100644 --- a/tests/trader/test_reconcile.py +++ b/tests/trader/test_reconcile.py @@ -142,3 +142,55 @@ class TestReconcile: patch("sanguo_trader.bridge_client.BridgeClient") as mbc: mbc.return_value.get_account.side_effect = RuntimeError("boom") reconcile_from_bridge(db, 1, _DATE, acc, _cfg(True)) # 不抛 + + +class TestReconcileTokenSource: + """bridge_token 优先级:config > env > skip(reconcile 模式 B)。""" + + @staticmethod + def _ok_account_resp(): + return {"ok": True, "cash": 80000.0, "frozen": 0, + "market_value": 0.0, "total": 80000.0} + + def test_config_token_used(self, tmp_path): + """(a) config 有 bridge_token → 用 config token(env 未设)。""" + db = _mk_db(tmp_path) + acc = Account(100000.0) + cfg = SimpleNamespace(live={ + "enabled": True, "bridge_url": "http://b.test", + "shadow": True, "mode_b": True, "bridge_token": "cfg-token", + }) + with patch.dict("os.environ", {}, clear=True), \ + patch("sanguo_trader.bridge_client.BridgeClient") as mbc: + mbc.return_value.get_account.return_value = self._ok_account_resp() + mbc.return_value.get_positions.return_value = [] + reconcile_from_bridge(db, 1, _DATE, acc, cfg) + assert mbc.call_args[0][1] == "cfg-token" # BridgeClient(url, token) + + def test_env_fallback_when_config_missing(self, tmp_path): + """(b) config 无 bridge_token → fallback 到 BRIDGE_TOKEN env。""" + db = _mk_db(tmp_path) + acc = Account(100000.0) + cfg = SimpleNamespace(live={ + "enabled": True, "bridge_url": "http://b.test", + "shadow": True, "mode_b": True, + }) + with patch.dict("os.environ", {"BRIDGE_TOKEN": "env-token"}), \ + patch("sanguo_trader.bridge_client.BridgeClient") as mbc: + mbc.return_value.get_account.return_value = self._ok_account_resp() + mbc.return_value.get_positions.return_value = [] + reconcile_from_bridge(db, 1, _DATE, acc, cfg) + assert mbc.call_args[0][1] == "env-token" + + def test_no_token_skips(self, tmp_path): + """(c) config 和 env 都无 bridge_token → 跳过、warning。""" + db = _mk_db(tmp_path) + acc = Account(100000.0) + cfg = SimpleNamespace(live={ + "enabled": True, "bridge_url": "http://b.test", + "shadow": True, "mode_b": True, + }) + with patch.dict("os.environ", {}, clear=True), \ + patch("sanguo_trader.bridge_client.BridgeClient") as mbc: + reconcile_from_bridge(db, 1, _DATE, acc, cfg) + mbc.assert_not_called() diff --git a/tests/trader/test_shadow_orders.py b/tests/trader/test_shadow_orders.py index 5fc7bc8..d229259 100644 --- a/tests/trader/test_shadow_orders.py +++ b/tests/trader/test_shadow_orders.py @@ -98,3 +98,46 @@ class TestShadowTradesToBridge: "SELECT status FROM paper_shadow_orders WHERE account_id=1" ).fetchone() assert row and row[0] == "failed" + + +class TestShadowTokenSource: + """bridge_token 优先级:config > env > skip(D-3 影子下单)。""" + + def test_config_token_used(self, tmp_path): + """(a) config 有 bridge_token → 用 config token(env 未设)。""" + db = _mk_db(tmp_path) + _insert_trade(db) + cfg = SimpleNamespace(live={ + "enabled": True, "bridge_url": "http://b.test", "shadow": True, + "bridge_token": "cfg-token", + }) + with patch.dict("os.environ", {}, clear=True), \ + patch("sanguo_trader.bridge_client.BridgeClient") as mbc: + mbc.return_value.place_order.return_value = {"ok": True, "order_id": 1} + _shadow_trades_to_bridge(db, 1, _DATE, cfg) + assert mbc.call_args[0][1] == "cfg-token" # BridgeClient(url, token) + + def test_env_fallback_when_config_missing(self, tmp_path): + """(b) config 无 bridge_token → fallback 到 BRIDGE_TOKEN env。""" + db = _mk_db(tmp_path) + _insert_trade(db) + cfg = SimpleNamespace(live={ + "enabled": True, "bridge_url": "http://b.test", "shadow": True, + }) + with patch.dict("os.environ", {"BRIDGE_TOKEN": "env-token"}), \ + patch("sanguo_trader.bridge_client.BridgeClient") as mbc: + mbc.return_value.place_order.return_value = {"ok": True, "order_id": 1} + _shadow_trades_to_bridge(db, 1, _DATE, cfg) + assert mbc.call_args[0][1] == "env-token" + + def test_no_token_skips(self, tmp_path): + """(c) config 和 env 都无 bridge_token → 跳过、warning,不下单。""" + db = _mk_db(tmp_path) + _insert_trade(db) + cfg = SimpleNamespace(live={ + "enabled": True, "bridge_url": "http://b.test", "shadow": True, + }) + with patch.dict("os.environ", {}, clear=True), \ + patch("sanguo_trader.bridge_client.BridgeClient") as mbc: + _shadow_trades_to_bridge(db, 1, _DATE, cfg) + mbc.assert_not_called()