Files
sanguo_vnpy_v2/sanguo_trader/position_ledger.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

58 lines
2.6 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.
"""单标的持仓对象(raw 计均价、T+1 冻结)。
mutable,被 Account / StrategyRunner 持有(spec §6.3 双层记账)。
- 均价:移动加权平均,买入刷新;卖出不影响剩余持仓成本(A 股惯例)
- T+1:买入当日 frozen,次日开盘前由 Account.unfreeze_all() 解冻
- 零股:卖出允许零股(退出持仓基本操作),买入由 matcher 保证 100 股整取
"""
class PositionLedger:
def __init__(self, symbol: str, volume: int = 0, frozen: int = 0, avg_price: float = 0.0):
self.symbol: str = symbol
self.volume: int = volume
self.frozen: int = frozen # T+1 当日买入冻结
self.avg_price: float = avg_price
@property
def available(self) -> int:
"""可卖出量 = 总持仓 - T+1 冻结。"""
return self.volume - self.frozen
def apply_buy(self, price: float, volume: int) -> None:
"""买入:刷新移动加权均价,新买入量计入 frozen(T+1)。"""
if volume <= 0 or price <= 0:
raise ValueError(f"price/volume 必须为正: price={price}, volume={volume}")
total_cost = self.avg_price * self.volume + price * volume
self.volume += volume
self.avg_price = total_cost / self.volume if self.volume else 0.0
self.frozen += volume # T+1
def apply_sell(self, price: float, volume: int) -> None:
"""卖出:扣减持仓量。price 保留接口对称(不影响剩余持仓均价)。
清仓时 avg_price 归零(避免下一次买入残留历史成本)。"""
if volume <= 0 or price <= 0:
raise ValueError(f"price/volume 必须为正: price={price}, volume={volume}")
if volume > self.available:
raise ValueError(
f"卖出超过可卖量: want {volume}, available {self.available}"
)
self.volume -= volume
if self.volume == 0:
self.avg_price = 0.0
def apply_split(self, factor: float) -> None:
"""送股/转增:volume ×= factoravg_price /= factor(总市值不变,spec §295)。
factor=(10+送转)/10,对 100 股整数倍持仓结果恒为整数。
factor<=0 非法;空持仓 no-op。
"""
if factor <= 0:
raise ValueError(f"split factor 必须为正: {factor}")
if self.volume <= 0:
return
self.volume = int(round(self.volume * factor))
self.avg_price = self.avg_price / factor
def unfreeze(self) -> None:
"""次日开盘前调用:frozen → available。"""
self.frozen = 0