164690373f
- 资金占用成本(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预拉日历)
113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
"""资金占用成本(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)
|