Files
sanguo_vnpy_v2/tests/trader/test_shadow_orders.py
T
claude_dev 0761342baf 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
2026-07-13 19:30:58 +08:00

144 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""D-3 影子下单集成测试(persistence 幂等 + _shadow_trades_to_bridge 正向路径)。
不依赖 Windows——mock BridgeClient.place_order,验证:
- persistence 幂等(save_shadow_order / is_trade_shadowed
- enabled=false 跳过(不下单、不查库)
- enabled=true + bridge ok → paper_shadow_orders 记录 + symbol 转换正确
- 幂等:重跑同 trade_id 不重复下单
- bridge 失败 → status=failed,不阻断
"""
import sqlite3
from types import SimpleNamespace
from unittest.mock import patch
from sanguo_trader.live_orchestrator import _shadow_trades_to_bridge
from sanguo_trader.persistence import (
init_db,
is_trade_shadowed,
save_shadow_order,
save_trade,
)
_DATE = "2026-07-11"
def _mk_db(tmp_path) -> str:
db = str(tmp_path / "t.db")
init_db(db)
return db
def _cfg(enabled: bool) -> SimpleNamespace:
return SimpleNamespace(live={
"enabled": enabled, "bridge_url": "http://b.test", "shadow": True,
})
def _insert_trade(db, account_id=1, symbol="600000", direction="buy") -> int:
"""插一条当日成交,返回 trade_id。"""
return save_trade(db, account_id, {
"symbol": symbol, "direction": direction, "price": 10.5,
"volume": 100, "bar_date": _DATE, "strategy_id": "s1",
})
class TestShadowPersistence:
def test_idempotent_roundtrip(self, tmp_path):
db = _mk_db(tmp_path)
assert not is_trade_shadowed(db, 1, 100)
save_shadow_order(db, 1, 100, 555, "ok")
assert is_trade_shadowed(db, 1, 100)
class TestShadowTradesToBridge:
def test_disabled_skips(self, tmp_path):
"""enabled=false → 不创建 BridgeClient、不下单。"""
db = _mk_db(tmp_path)
_insert_trade(db)
with patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
_shadow_trades_to_bridge(db, 1, _DATE, _cfg(False))
mbc.assert_not_called()
def test_enabled_records_and_symbol_converted(self, tmp_path):
"""enabled=true + bridge ok → 记录 paper_shadow_orders + code 转换正确。"""
db = _mk_db(tmp_path)
trade_id = _insert_trade(db, symbol="600000", direction="buy")
with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \
patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
mbc.return_value.place_order.return_value = {"ok": True, "order_id": 777}
_shadow_trades_to_bridge(db, 1, _DATE, _cfg(True))
call = mbc.return_value.place_order.call_args
assert call[0][0] == "sh600000" # code 已转 sh 前缀
assert call[0][1] == "buy"
assert is_trade_shadowed(db, 1, trade_id)
def test_idempotent_no_duplicate(self, tmp_path):
"""同 trade_id 重跑 → 不重复下单。"""
db = _mk_db(tmp_path)
_insert_trade(db)
with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \
patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
mbc.return_value.place_order.return_value = {"ok": True, "order_id": 777}
_shadow_trades_to_bridge(db, 1, _DATE, _cfg(True))
assert mbc.return_value.place_order.call_count == 1
# 重跑:trade_id 已 shadowed,不再下单
_shadow_trades_to_bridge(db, 1, _DATE, _cfg(True))
assert mbc.return_value.place_order.call_count == 1
def test_bridge_failure_does_not_block(self, tmp_path):
"""bridge 返回 None(失败)→ 记 status=failed,不抛异常。"""
db = _mk_db(tmp_path)
_insert_trade(db)
with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \
patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
mbc.return_value.place_order.return_value = None
_shadow_trades_to_bridge(db, 1, _DATE, _cfg(True)) # 不抛
with sqlite3.connect(db) as conn:
row = conn.execute(
"SELECT status FROM paper_shadow_orders WHERE account_id=1"
).fetchone()
assert row and row[0] == "failed"
class TestShadowTokenSource:
"""bridge_token 优先级:config > env > skipD-3 影子下单)。"""
def test_config_token_used(self, tmp_path):
"""(a) config 有 bridge_token → 用 config tokenenv 未设)。"""
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()