Files
stock/backend/app/backtest/broker.py
2026-08-07 16:08:34 +08:00

105 lines
3.5 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.
"""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