"""模拟盘分户账:每策略持仓 + 已实现盈亏归因(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