"""资金占用成本(borrow cost)归因测试(spec §195)。 每策略占用资金按无风险利率日扣,归因到 per_strategy_pnl;不影响账户总账净值。 """ import json from types import SimpleNamespace import pytest from sanguo_trader.account import Account from sanguo_trader.cta_adapter import PaperCtaEngine from sanguo_trader.engine import PaperEngine from sanguo_trader.models import AccountConfig, MatchSession from sanguo_trader.persistence import init_db, list_daily_balance, save_account from sanguo_trader.position_ledger import PositionLedger from sanguo_trader.strategy_runner import StrategyRunner def _bar(date, o, h, l, c): return SimpleNamespace(open_price=o, high_price=h, low_price=l, close_price=c, datetime=date) class _FakeDataSource: def __init__(self, sections): self.sections = sections def iter_bars(self, symbols, start, end, interval, adjust="qfq", cfg=None): del symbols, start, end, interval, adjust, cfg for date, bars in self.sections: yield date, bars class _NoOrderStrategy: """空策略:on_bar 不下单(仅驱动 step 走完盯市/归因路径)。""" def __init__(self, engine, vt_symbol): self.cta_engine = engine self.vt_symbol = vt_symbol def on_bar(self, bar): pass def _make_position(symbol, volume, avg_price): pos = PositionLedger(symbol) pos.volume = volume pos.avg_price = avg_price return pos def test_daily_borrow_cost_calculation(): # Arrange:200 股 @10 → 占用 2000,rate=0.02 → 日成本 2000*0.02/365 runner = StrategyRunner("s1", symbol="600000") runner.positions["600000"] = _make_position("600000", 200, 10.0) closes = {"600000": 10.0} # Act cost = runner.daily_borrow_cost(closes, risk_free_rate=0.02) # Assert assert cost == pytest.approx(2000 * 0.02 / 365) def test_holding_cost_greater_than_empty(): # Arrange:持仓 runner vs 空仓 runner runner_holding = StrategyRunner("s1", symbol="600000") runner_holding.positions["600000"] = _make_position("600000", 200, 10.0) runner_empty = StrategyRunner("s2", symbol="600000") closes = {"600000": 10.0} # Act cost_holding = runner_holding.daily_borrow_cost(closes, 0.02) cost_empty = runner_empty.daily_borrow_cost(closes, 0.02) # Assert assert cost_empty == 0.0 assert cost_holding > 0 assert cost_holding > cost_empty def test_borrow_cost_attributed_not_affecting_account(tmp_path): # Arrange:预置持仓 200 股 @10(占用 2000),空策略不下单,rate=0.03 db = str(tmp_path / "borrow.db") init_db(db) aid = save_account(db, {"name": "t", "initial_capital": 1_000_000}) cfg = AccountConfig(initial_capital=1_000_000) account = Account(1_000_000) account.cash = 998000 # 已花 2000 买入 account.positions["600000"] = _make_position("600000", 200, 10.0) cta = PaperCtaEngine("s1", match_session=MatchSession.CURRENT_CLOSE) strat = _NoOrderStrategy(cta, "600000.SSE") cta.set_strategy(strat) runner = StrategyRunner("s1", strategy=strat, paper_cta_engine=cta, symbol="600000") runner.positions["600000"] = _make_position("600000", 200, 10.0) sections = [("2024-01-01", {"600000": _bar("2024-01-01", 10.0, 10.5, 9.5, 10.0)})] pe = PaperEngine(account, [runner], _FakeDataSource(sections), cfg, db, aid, symbols=["600000"], start="2024-01-01", end="2024-01-31", risk_free_rate=0.03) cash_before = account.cash # Act pe.run() # Assert:账户总账不受占用成本影响(仅归因记账) assert account.cash == cash_before balances = list_daily_balance(db, aid) assert len(balances) == 1 psp = json.loads(balances[0]["per_strategy_pnl"]) assert "s1" in psp assert psp["s1"]["borrow_cost"] == pytest.approx(2000 * 0.03 / 365)