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预拉日历)
74 lines
3.1 KiB
Python
74 lines
3.1 KiB
Python
"""模拟盘分户账:每策略持仓 + 已实现盈亏归因(spec §7 双层记账分户层)。
|
||
|
||
与 Account 共享同一笔 trade:Account 记合并总账,StrategyRunner 记该策略归因。
|
||
"""
|
||
from .models import OrderSide, PaperTrade
|
||
from .position_ledger import PositionLedger
|
||
|
||
|
||
class StrategyRunner:
|
||
def __init__(self, strategy_id: str, strategy=None, paper_cta_engine=None,
|
||
symbol: str = "", max_allocation: float = float("inf")) -> None:
|
||
self.strategy_id: str = strategy_id
|
||
self.strategy = strategy
|
||
self.paper_cta_engine = paper_cta_engine
|
||
self.symbol: str = symbol
|
||
self.max_allocation: float = max_allocation # 软限额(spec §195),默认不限
|
||
self.positions: dict[str, PositionLedger] = {}
|
||
self.realized_pnl: float = 0.0
|
||
self.commission_paid: float = 0.0
|
||
|
||
def used_allocation(self, closes: dict[str, float] | None = None) -> float:
|
||
"""该策略已占用资金 = 持仓市值(spec §195 软限额检查用)。
|
||
|
||
closes 为当日 raw 收盘价 {symbol: price};缺省股票用 avg_price 兜底。
|
||
"""
|
||
closes = closes or {}
|
||
return sum(
|
||
p.volume * closes.get(sym, p.avg_price)
|
||
for sym, p in self.positions.items() if p.volume > 0
|
||
)
|
||
|
||
def daily_borrow_cost(self, closes: dict[str, float] | None = None,
|
||
risk_free_rate: float = 0.0) -> float:
|
||
"""资金占用日成本 = 占用资金 × 年化无风险利率 / 365(spec §195 归因用)。
|
||
|
||
纯归因记账:返回值由 PaperEngine 计入 per_strategy_pnl,不扣 account.cash。
|
||
"""
|
||
return self.used_allocation(closes) * risk_free_rate / 365.0
|
||
|
||
def _position(self, symbol: str) -> PositionLedger:
|
||
if symbol not in self.positions:
|
||
self.positions[symbol] = PositionLedger(symbol)
|
||
return self.positions[symbol]
|
||
|
||
def apply_trade(self, trade: PaperTrade) -> None:
|
||
pos = self._position(trade.symbol)
|
||
if trade.side == OrderSide.BUY:
|
||
pos.apply_buy(trade.price, trade.volume)
|
||
self.commission_paid += trade.commission + trade.transfer_fee
|
||
else:
|
||
avg = pos.avg_price
|
||
realized = (
|
||
(trade.price - avg) * trade.volume
|
||
- trade.commission - trade.stamp_duty - trade.transfer_fee
|
||
)
|
||
self.realized_pnl += realized
|
||
self.commission_paid += (
|
||
trade.commission + trade.stamp_duty + trade.transfer_fee
|
||
)
|
||
pos.apply_sell(trade.price, trade.volume)
|
||
|
||
def unfreeze_all(self) -> None:
|
||
"""每日开盘前:T+1 解冻分户持仓(与 Account.unfreeze_all 对称)。"""
|
||
for p in self.positions.values():
|
||
p.unfreeze()
|
||
|
||
def unrealized_pnl(self, bars_raw: dict[str, float]) -> float:
|
||
"""按 raw 收盘价算浮动盈亏(未实现)。"""
|
||
total = 0.0
|
||
for sym, p in self.positions.items():
|
||
if p.volume > 0:
|
||
total += (bars_raw.get(sym, p.avg_price) - p.avg_price) * p.volume
|
||
return total
|