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预拉日历)
76 lines
3.0 KiB
Python
76 lines
3.0 KiB
Python
"""模拟盘总账:资金 T+0 / 合并持仓 / 净值盯市(spec §7 双层记账总账层)。
|
||
|
||
- cash 资金 T+0:卖出回笼资金当日即可再买(A 股硬规则)
|
||
- 股票 T+1:买入持仓由 PositionLedger.frozen 管,次日 unfreeze_all 解冻
|
||
- transfer_fee:matcher 已出双向总额(review H3),Account 直接用,不再 ×2
|
||
- 资金检查 cash_enough 在买单 apply 前由 PaperEngine 调用
|
||
"""
|
||
from .models import OrderSide, PaperTrade
|
||
from .position_ledger import PositionLedger
|
||
|
||
|
||
class Account:
|
||
def __init__(self, initial_capital: float) -> None:
|
||
self.cash: float = float(initial_capital)
|
||
self.positions: dict[str, PositionLedger] = {}
|
||
self.market_value: float = 0.0
|
||
|
||
def _position(self, symbol: str) -> PositionLedger:
|
||
if symbol not in self.positions:
|
||
self.positions[symbol] = PositionLedger(symbol)
|
||
return self.positions[symbol]
|
||
|
||
def cash_enough(self, trade: PaperTrade) -> bool:
|
||
"""买单检查现金是否足够(卖出不检查,持仓检查在 PositionLedger)。"""
|
||
if trade.side != OrderSide.BUY:
|
||
return True
|
||
cost = trade.price * trade.volume + trade.commission + trade.transfer_fee
|
||
return self.cash >= cost
|
||
|
||
def apply_trade(self, trade: PaperTrade) -> None:
|
||
pos = self._position(trade.symbol)
|
||
if trade.side == OrderSide.BUY:
|
||
cost = trade.price * trade.volume + trade.commission + trade.transfer_fee
|
||
self.cash -= cost
|
||
pos.apply_buy(trade.price, trade.volume)
|
||
else:
|
||
proceeds = (
|
||
trade.price * trade.volume
|
||
- trade.commission - trade.stamp_duty - trade.transfer_fee
|
||
)
|
||
self.cash += proceeds # 资金 T+0:卖出现金立即可用
|
||
pos.apply_sell(trade.price, trade.volume)
|
||
|
||
def unfreeze_all(self) -> None:
|
||
"""每日开盘前:T+1 解冻所有持仓。"""
|
||
for p in self.positions.values():
|
||
p.unfreeze()
|
||
|
||
def apply_cash_dividend(self, symbol: str, per_share: float) -> None:
|
||
"""现金分红到账:cash += per_share × 持仓量(spec §295)。
|
||
|
||
按 A 股惯例按除权日前一交易日持仓量派发;无持仓/非正值 no-op。
|
||
"""
|
||
if per_share <= 0:
|
||
return
|
||
pos = self.positions.get(symbol)
|
||
if pos is None or pos.volume <= 0:
|
||
return
|
||
self.cash += per_share * pos.volume
|
||
|
||
def mark_to_market(self, bars_raw: dict[str, float],
|
||
prev_close: dict[str, float] | None = None) -> None:
|
||
"""按 raw 收盘价重估合并持仓市值(spec §295 停牌盯市兜底)。
|
||
|
||
bar 缺失(停牌)→ 用前日 close 兜底,再退到 avg_price。
|
||
"""
|
||
prev_close = prev_close or {}
|
||
self.market_value = sum(
|
||
p.volume * bars_raw.get(sym, prev_close.get(sym, p.avg_price))
|
||
for sym, p in self.positions.items()
|
||
)
|
||
|
||
@property
|
||
def equity(self) -> float:
|
||
return self.cash + self.market_value
|