Files
sanguo_vnpy_v2/tests/trader/test_borrow_cost.py
T
claude_dev 164690373f feat(trader): C期分期项收尾—资金占用成本+分红送股+_restore_ledger修复
- 资金占用成本(spec§195): StrategyRunner.daily_borrow_cost(used×risk_free/365)
  归因per_strategy_pnl(不碰account总账, account.equity真实净值不变);
  config risk_free_rate=0.02; engine.step mark_to_market后计扣; =0向后兼容跳过
- 分红送股(spec§295): dividend_source.py(akshare stock_history_dividend_detail,
  实测600000/000001纯现金分红); PositionLedger.apply_split(volume×factor/avg÷factor);
  Account.apply_cash_dividend; engine._apply_dividends(除权日调整,现金先split后);
  mark_to_market停牌prev_close兜底(今收→前收→均价); _run_replay注入dividends日历
- 修_restore_ledger预存bug: PositionLedger.__init__加volume/frozen/avg_price参数
  (原只symbol, live_orchestrator跨日恢复4参数调用会TypeError, 首次step空仓未暴露)
- 139 passed(119基准+20分红+3占用成本), 无回归
- live_step dividends注入待分期项(每日拉全市场分红慢, 需run_daily_update预拉日历)
2026-07-10 08:44:35 +08:00

113 lines
3.9 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.
"""资金占用成本(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():
# Arrange200 股 @10 → 占用 2000rate=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)