"""ShadowBroker(影子柜台本地撮合)单元测试。 纯逻辑测试:不依赖 bullet_trade/xtquant,价格由固定 price_getter 注入。 """ from __future__ import annotations import asyncio from datetime import datetime import pytest from sanguo_trader.shadow.broker import ShadowBroker def _mk_broker(cash: float = 100_000.0, **kw) -> ShadowBroker: prices = kw.pop("prices", {"600519.SH": 100.0}) fixed_now = kw.pop("now", datetime(2026, 8, 14, 10, 0, 0)) return ShadowBroker( initial_cash=cash, price_getter=lambda s: prices.get(s), now_provider=lambda: fixed_now, **kw, ) def _buy(b: ShadowBroker, sec: str, amt: int, px: float | None = None): return asyncio.run(b.buy(sec, amt, px)) def _sell(b: ShadowBroker, sec: str, amt: int, px: float | None = None): return asyncio.run(b.sell(sec, amt, px)) def test_buy_fills_with_commission_and_slippage(): b = _mk_broker(cash=100_000, slippage=0.001, commission_rate=0.0003, min_commission=5) oid = _buy(b, "600519.SH", 100) assert b.orders[oid]["status"] == "filled" fill = b.orders[oid]["filled_price"] assert fill == pytest.approx(100.0 * 1.001, abs=0.01) # 买入上浮滑点 # 现金扣减 = 全额 + 佣金(低于最低佣金取 5 元) commission = max(100 * fill * 0.0003, 5.0) assert b.cash == pytest.approx(100_000 - 100 * fill - commission) assert b.positions["600519.SH"]["amount"] == 100 assert b.positions["600519.SH"]["avg_cost"] == pytest.approx(fill) def test_sell_charges_stamp_duty_and_slippage_down(): b = _mk_broker(cash=100_000, slippage=0.001, stamp_duty_rate=0.001) _buy(b, "600519.SH", 200, px=100.0) # 固定委托价,滑点仍生效 cash_after_buy = b.cash # T+1:当日买入不可卖 → 先模拟次日(before_open 清锁) b.before_open() oid = _sell(b, "600519.SH", 200, px=100.0) assert b.orders[oid]["status"] == "filled" fill = b.orders[oid]["filled_price"] assert fill == pytest.approx(100.0 * 0.999, abs=0.01) # 卖出下压滑点 gross = 200 * fill commission = max(gross * 0.0003, 5.0) stamp = gross * 0.001 assert b.cash == pytest.approx(cash_after_buy + gross - commission - stamp) assert "600519.SH" not in b.positions # 清仓移除 def test_t1_blocks_same_day_sell(): b = _mk_broker() _buy(b, "600519.SH", 200, px=100.0) oid = _sell(b, "600519.SH", 200, px=100.0) # 当日卖 → 拒 assert b.orders[oid]["status"] == "rejected" assert "T+1" in b.orders[oid]["reject_reason"] # 次日可卖 b.before_open() oid2 = _sell(b, "600519.SH", 200, px=100.0) assert b.orders[oid2]["status"] == "filled" def test_insufficient_cash_rejects(): b = _mk_broker(cash=5_000) oid = _buy(b, "600519.SH", 100) # 需约 1 万 assert b.orders[oid]["status"] == "rejected" assert "资金不足" in b.orders[oid]["reject_reason"] assert b.cash == 5_000 # 拒单不动账 def test_odd_lot_floors_to_100(): b = _mk_broker(cash=1_000_000) oid = _buy(b, "600519.SH", 250) # → 200 assert b.orders[oid]["status"] == "filled" assert b.orders[oid]["filled_amount"] == 200 oid2 = _buy(b, "600519.SH", 50) # 不足一手 → 拒 assert b.orders[oid2]["status"] == "rejected" def test_no_price_rejects(): b = _mk_broker(prices={}) oid = _buy(b, "600519.SH", 100) assert b.orders[oid]["status"] == "rejected" assert "无参考价" in b.orders[oid]["reject_reason"] def test_on_trade_callback_receives_fills(): seen: list[dict] = [] b = ShadowBroker( initial_cash=100_000, price_getter=lambda s: 10.0, on_trade=seen.append, ) _buy(b, "600519.SH", 100) assert len(seen) == 1 t = seen[0] assert t["side"] == "buy" and t["amount"] == 100 and t["price"] == pytest.approx(10.0) def test_get_account_info_totals(): b = _mk_broker(cash=100_000) _buy(b, "600519.SH", 100, px=100.0) info = b.get_account_info() assert info["available_cash"] == pytest.approx(100_000 - 100 * 100.0 - max(100 * 100 * 0.0003, 5)) assert info["market_value"] == pytest.approx(100 * 100.0) # price_getter=100 assert info["total_value"] == pytest.approx(info["available_cash"] + info["market_value"]) def test_avg_cost_weighted_on_second_buy(): b = _mk_broker(cash=1_000_000, slippage=0.0) _buy(b, "600519.SH", 100, px=100.0) _buy(b, "600519.SH", 100, px=110.0) pos = b.positions["600519.SH"] assert pos["amount"] == 200 assert pos["avg_cost"] == pytest.approx(105.0) def test_cancel_always_false_and_open_orders_empty(): b = _mk_broker() _buy(b, "600519.SH", 100, px=100.0) assert asyncio.run(b.cancel_order("whatever")) is False assert b.get_open_orders() == [] # 即时成交,无挂单 def test_live_engine_protocol_compat(): """LiveEngine 0.9.2 duck-typed 协议完整性(2026-08-14 VPS 实况教训: 缺 supports_account_sync → 引擎 _start_background_jobs 启动即崩,影子进程崩溃循环)。""" from sanguo_trader.shadow.broker import ShadowBroker b = ShadowBroker() for attr in ( "connect", "disconnect", "is_connected", "heartbeat", "before_open", "after_close", "cleanup", "supports_account_sync", "supports_orders_sync", "sync_account", "sync_orders", "get_account_info", "get_positions", "get_open_orders", ): assert hasattr(b, attr), f"LiveEngine 需要 broker.{attr}, ShadowBroker 缺失" assert b.supports_account_sync() is False # 本地柜台,引擎跳过账户同步 assert b.supports_orders_sync() is False assert b.sync_orders() == [] b.cleanup() # 不抛异常