"""PaperBroker —— 回测中的虚拟撮合 / 账户。 建模 A 股规则: - 100 股整数手(1手=100股) - T+1:当日买入次日才可卖(fast_mode 关闭此约束) - 印花税(卖出)、过户费(双边)、佣金(万1 最低5元)、滑点 - 撮合价:以当根 bar 收盘价近似(阶段1 接 VWAP / 限价单) """ from __future__ import annotations from dataclasses import dataclass, field from ..commission import CostSchedule, DEFAULT, buy_cost, sell_cost from ..domain import Fill, Side LOT = 100 # A股 1 手 = 100 股 @dataclass class PaperBroker: initial_cash: float = 100000.0 schedule: CostSchedule = field(default_factory=lambda: DEFAULT) enable_costs: bool = True enable_t_plus_1: bool = True cash: float = field(init=False) holdings: float = 0.0 # 可卖数量 locked: float = 0.0 # 当日买入(T+1 锁定) avg_price: float = 0.0 fills: list[Fill] = field(default_factory=list) def __post_init__(self) -> None: self.cash = self.initial_cash @property def position(self) -> float: return self.holdings + self.locked def equity(self, price: float) -> float: return self.cash + self.position * price @staticmethod def _to_lots(qty: float) -> int: return int(qty // LOT) * LOT def buy_max(self, ts, price: float) -> Fill | None: """用当前现金买尽可能多的整手。""" if price <= 0: return None rate = ( self.schedule.commission_rate + self.schedule.transfer_fee_rate + self.schedule.slippage_rate ) if self.enable_costs else 0.0 affordable_qty = self.cash / (price * (1 + rate)) qty = self._to_lots(affordable_qty) if qty <= 0: return None return self._execute_buy(ts, price, qty) def sell_all(self, ts, price: float) -> Fill | None: """卖出全部可卖持仓(整手)。""" qty = self._to_lots(self.holdings) if qty <= 0: return None return self._execute_sell(ts, price, qty) def _execute_buy(self, ts, price: float, qty: int) -> Fill: if self.enable_costs: fp, comm, tf = buy_cost(price, qty, self.schedule) else: fp, comm, tf = price, 0.0, 0.0 cost = fp * qty + comm + tf prev_pos = self.position new_pos = prev_pos + qty self.avg_price = (self.avg_price * prev_pos + fp * qty) / new_pos if new_pos else 0.0 if self.enable_t_plus_1: self.locked += qty else: self.holdings += qty self.cash -= cost f = Fill(ts=ts, side=Side.BUY, price=fp, qty=qty, commission=comm, transfer_fee=tf) self.fills.append(f) return f def _execute_sell(self, ts, price: float, qty: int) -> Fill: if self.enable_costs: fp, comm, tf, sd = sell_cost(price, qty, self.schedule) else: fp, comm, tf, sd = price, 0.0, 0.0, 0.0 proceeds = fp * qty - comm - tf - sd self.holdings -= qty self.cash += proceeds if self.position == 0: self.avg_price = 0.0 f = Fill(ts=ts, side=Side.SELL, price=fp, qty=qty, commission=comm, stamp_duty=sd, transfer_fee=tf) self.fills.append(f) return f def release_t_plus_1(self) -> None: """每根 bar 结束时调用:当日锁定转为次日可卖。""" if self.enable_t_plus_1: self.holdings += self.locked self.locked = 0.0