feat(trader): live bridge token 改从 config 读取,免容器重建
容器 docker run(非 compose)注入 BRIDGE_TOKEN env 需重建容器,风险大。改为 live_cfg.bridge_token 优先、fallback BRIDGE_TOKEN env。run_live_step 每次 load_config,改 config 免重启即生效。 - _shadow_trades_to_bridge + reconcile_from_bridge 两处 token 读取 - config: live.bridge_token 占位空值(真实值填 NAS gitignored config,不入库) - tests: +6 测试(config优先/env fallback/都无跳过),191 passed
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user