Files
sanguo_vnpy_v2/tests/trader/test_reconcile.py
T
claude_dev e77c9df0d4 feat(live): D-4c模式B reconcile—bridge回报驱动账本(真桥验证通过)
- bridge_client: from_bridge_code(sh/sz→纯数字码, to_bridge_code逆函数)
- live_orchestrator: reconcile_from_bridge 读bridge /account /positions校正account现金+持仓+持久化, 默认mode_b=false
- live_step step8: 影子后调reconcile(mode_b=true生效, mode_b=false跳过)
- config: live.mode_b开关(默认false模式A)
- test_reconcile: 10例(cash/positions校正+code转换+失败降级+mode_b跳过)
- NAS环境15 passed(reconcile10+shadow5无回归)
- 真桥集成: live_step mode_b=true → reconcile读bridge → account校正(1000万/空仓=bridge真实账本)+持久化
安全: mode_b默认关+bridge失败降级不阻断+token走env
2026-07-11 06:30:08 +08:00

145 lines
6.2 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-4c 模式 B reconcile 集成测试(bridge 回报校正 account 账本)。
不依赖 Windows——mock BridgeClient.get_account/get_positions,验证:
- mode_b=false / enabled=false → 跳过(不创建 BridgeClient
- mode_b=true + bridge ok → cash/positions 校正 + code 转换 + 持久化
- bridge 失败(get_account/get_positions 返回 None)→ 不阻断、降级(不持久化)
- 无 BRIDGE_TOKEN → 跳过
参考 tests/trader/test_shadow_orders.py 的 mock 风格。
"""
from types import SimpleNamespace
from unittest.mock import patch
from sanguo_trader.account import Account
from sanguo_trader.bridge_client import from_bridge_code
from sanguo_trader.live_orchestrator import reconcile_from_bridge
from sanguo_trader.persistence import (
init_db,
load_last_balance,
load_positions,
)
_DATE = "2026-07-11"
def _mk_db(tmp_path) -> str:
db = str(tmp_path / "t.db")
init_db(db)
return db
def _cfg(mode_b: bool, enabled: bool = True) -> SimpleNamespace:
return SimpleNamespace(live={
"enabled": enabled, "bridge_url": "http://b.test",
"shadow": True, "mode_b": mode_b,
})
class TestFromBridgeCode:
def test_sh_prefix(self):
assert from_bridge_code("sh600000") == "600000"
def test_sz_prefix(self):
assert from_bridge_code("sz000001") == "000001"
def test_already_plain(self):
assert from_bridge_code("600000") == "600000"
class TestReconcile:
def test_mode_b_false_skips(self, tmp_path):
"""mode_b=false → 不创建 BridgeClient。"""
db = _mk_db(tmp_path)
acc = Account(100000.0)
with patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
reconcile_from_bridge(db, 1, _DATE, acc, _cfg(False))
mbc.assert_not_called()
def test_enabled_false_skips(self, tmp_path):
"""enabled=false → 跳过(即使 mode_b=true)。"""
db = _mk_db(tmp_path)
acc = Account(100000.0)
with patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
reconcile_from_bridge(db, 1, _DATE, acc, _cfg(True, enabled=False))
mbc.assert_not_called()
def test_no_token_skips(self, tmp_path):
"""mode_b=true 但无 BRIDGE_TOKEN → 跳过。"""
db = _mk_db(tmp_path)
acc = Account(100000.0)
with patch.dict("os.environ", {}, clear=True), \
patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
reconcile_from_bridge(db, 1, _DATE, acc, _cfg(True))
mbc.assert_not_called()
def test_reconciles_account_and_positions(self, tmp_path):
"""mode_b=true + bridge ok → cash/positions 校正 + code 转换 + 持久化。"""
db = _mk_db(tmp_path)
acc = Account(100000.0)
acc.cash = 50000.0 # 模拟账本现金(将被 bridge 覆盖)
with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \
patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
mbc.return_value.get_account.return_value = {
"ok": True, "cash": 80000.0, "frozen": 5000.0,
"market_value": 120000.0, "total": 200000.0,
}
mbc.return_value.get_positions.return_value = [
{"code": "sh600000", "volume": 1000, "can_use": 800, "avg_price": 10.5},
{"code": "sz000001", "volume": 500, "can_use": 500, "avg_price": 15.0},
]
reconcile_from_bridge(db, 1, _DATE, acc, _cfg(True))
# account 资金校正(bridge 为准)
assert acc.cash == 80000.0
assert acc.market_value == 120000.0
# positions 重建(bridge sh/sz 前缀 → 纯数字 key
assert set(acc.positions.keys()) == {"600000", "000001"}
assert acc.positions["600000"].volume == 1000
assert acc.positions["600000"].frozen == 200 # volume - can_use = 1000 - 800
assert acc.positions["600000"].avg_price == 10.5
assert acc.positions["000001"].frozen == 0 # 500 - 500
# 持久化(DB 反映 bridge 校正后状态)
pos = load_positions(db, 1, "account")
assert "600000" in pos and pos["600000"]["volume"] == 1000
bal = load_last_balance(db, 1)
assert bal["cash"] == 80000.0
assert bal["market_value"] == 120000.0
assert bal["total_equity"] == 200000.0
def test_get_account_failure_degrades(self, tmp_path):
"""get_account 返回 None → 不阻断、不覆盖、不持久化(降级模拟账本)。"""
db = _mk_db(tmp_path)
acc = Account(100000.0)
original_cash = acc.cash
with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \
patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
mbc.return_value.get_account.return_value = None
reconcile_from_bridge(db, 1, _DATE, acc, _cfg(True)) # 不抛
assert acc.cash == original_cash # 未被覆盖
assert load_last_balance(db, 1) is None # 未持久化
def test_get_positions_failure_degrades(self, tmp_path):
"""get_positions 返回 None → 不阻断、降级(不持久化 reconcile)。"""
db = _mk_db(tmp_path)
acc = Account(100000.0)
with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \
patch("sanguo_trader.bridge_client.BridgeClient") as mbc:
mbc.return_value.get_account.return_value = {
"ok": True, "cash": 90000.0, "frozen": 0,
"market_value": 50000.0, "total": 140000.0,
}
mbc.return_value.get_positions.return_value = None
reconcile_from_bridge(db, 1, _DATE, acc, _cfg(True)) # 不抛
# 未持久化(降级,DB 保持 live_step step6 的 simulation 状态)
assert load_last_balance(db, 1) is None
def test_exception_does_not_block(self, tmp_path):
"""bridge 调用抛异常 → try/except 兜底,不阻断。"""
db = _mk_db(tmp_path)
acc = Account(100000.0)
with patch.dict("os.environ", {"BRIDGE_TOKEN": "tok"}), \
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)) # 不抛