first commit

This commit is contained in:
2026-08-07 16:08:34 +08:00
commit e0b5228008
51 changed files with 5175 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
"""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