逐日推进回测方案
本文档定义 zelqor 下一阶段的回测内核演进方向:把当前“整段区间一次性跑完”的最小回测,进一步拆成:
- 状态对象
- 单日 step 执行
- 历史回测驱动器
目标不是立即做成模拟盘,而是先让回测内核具备更强的“逐日推进”能力,为后续模拟盘 runtime 复用打基础。
1. 为什么要做这一层
当前 zelqor 已经具备两条可用回测链路:
run_precomputed_backtest(...)run_selector_backtest(...)
它们内部已经按交易日循环执行,但仍然属于“批处理回测”:
- 先拿完整
trade_dates - 一次性跑完全部区间
- 最后返回
BacktestResult
这种模式适合快速回测,但对后续这两类能力不够友好:
- 更细粒度的逐日调试
- 向模拟盘 runtime 演进
因此需要新增一层明确的“逐日推进模式”。
2. 目标
这一阶段希望把回测内核演进成三层:
step级日频执行内核- 基于历史交易日驱动的回测执行器
- 未来可复用的模拟盘驱动基础
最终希望达到:
- 每次只推进一个交易日
- 每次推进都能拿到当天结果
- 账户、持仓、交易记录保存在统一 state 中
- 历史回测只是循环调用 step
- 以后模拟盘也可以复用相同的 step 或其下层 broker / portfolio 逻辑
3. 设计原则
这一阶段遵循以下原则:
- 不打碎现有高层 API
- 先抽“日频逐步执行内核”,不先做完整模拟盘
- 先保留日线级语义,不先扩展到分钟级
- 逐日接口负责执行,不负责策略文件加载
- 逐日接口优先服务历史回测,再服务未来模拟盘
4. 推荐分层
4.1 状态层
新增一个明确的运行时状态对象,例如:
@dataclass
class BacktestState:
current_trade_date: str | None
cash: float
positions: dict[str, Position]
trades: list[TradeRecord]
equity_curve: list[EquityPoint]
daily_pools: list[DailyPoolRecord]
daily_decisions: list[DailyDecisionRecord]
positions_by_date: list[PositionSnapshot]
warnings: list[str]
metadata: dict[str, object]
职责:
- 保存账户现金
- 保存当前持仓
- 保存历史成交记录
- 保存权益曲线
- 保存逐日股票池与决策结果
- 保存运行过程中的 warnings
这个对象的意义是:把“运行中的回测状态”从一个大函数内部变量,提升为显式数据结构。
4.2 单日 step 层
新增单日推进接口,例如:
def step_backtest_day(
*,
state: BacktestState,
trade_date: str,
pool_codes: list[str],
provider: MarketDataProvider,
config: BacktestStepConfig,
) -> DayStepResult:
...
职责:
- 处理当日到期卖出
- 处理当日股票池买入
- 更新现金和持仓
- 计算当日权益
- 生成当天摘要
- 原地更新
state
这个接口是整个方案的核心。
4.3 历史回测驱动层
当前的:
run_precomputed_backtest(...)run_selector_backtest(...)
继续保留,但内部改成:
- 初始化 state
- 遍历
trade_dates - 每个交易日调用
step_backtest_day(...) - 最后汇总
BacktestResult
也就是说,高层 API 不变,但底层逻辑从“大循环函数”改成“驱动 step 的执行器”。
5. 推荐对象
5.1 Step 配置
@dataclass
class BacktestStepConfig:
holding_days: int = 1
max_positions: int = 10
allocation: str = "equal_weight"
buy_timing: str = "close"
sell_timing: str = "next_open"
costs: CostsConfig | None = None
职责:
- 收敛单日执行所需参数
- 避免
step_backtest_day(...)参数表继续膨胀
5.2 单日结果
@dataclass
class DayStepResult:
trade_date: str
pool_codes: list[str]
buys: list[str]
sells: list[str]
cash: float
market_value: float
equity: float
warnings: list[str]
职责:
- 返回当天执行结果摘要
- 方便日志、调试、回调、未来 UI 展示
6. 建议 API
6.1 低层 API
推荐新增:
def create_backtest_state(initial_cash: float, *, metadata: dict[str, object] | None = None) -> BacktestState:
...
def step_backtest_day(...) -> DayStepResult:
...
def finalize_backtest_result(
*,
state: BacktestState,
provider_name: str,
strategy_type: str,
) -> BacktestResult:
...
6.2 高层 API
保留现有:
run_precomputed_backtest(...)run_selector_backtest(...)
但让它们内部调用新的低层 step 接口。
7. 与模拟盘的关系
这套方案不是模拟盘本身,但对模拟盘帮助很大。
可以直接复用或高度参考的部分:
BacktestState的账户和持仓结构- broker 费用和执行规则
- portfolio 持仓更新逻辑
- 逐日回调与日志能力
- selector 和股票池生成方式
未来模拟盘更适合在此基础上新增一层 runtime,例如:
PaperRuntimeStateon_market_event(...)on_trade_day_open(...)on_trade_day_close(...)
也就是说:
- 历史回测使用“历史日期驱动 step”
- 模拟盘使用“实时事件驱动 step / runtime”
两者共享底层交易执行语义。
8. 非目标
这一阶段明确不做:
- 分钟级或 tick 级执行
- 挂单队列和撤单系统
- 部分成交
- 长生命周期运行时恢复
- 模拟盘完整 runtime
- 策略文件动态加载
9. 推荐实施顺序
建议按以下顺序推进:
- 抽出
BacktestState - 抽出
BacktestStepConfig - 抽出
DayStepResult - 实现
step_backtest_day(...) - 让
run_precomputed_backtest(...)改成基于 step 驱动 - 保持现有测试通过
- 新增一个
examples/run_step_backtest.py
10. 预期收益
完成这一阶段后,zelqor 会得到这些收益:
- 回测逻辑更清晰,可测试性更强
- 逐日行为更容易调试
- 逐日回调语义更自然
- 更容易扩展不同驱动器
- 为模拟盘 runtime 留出更干净的复用边界
一句话总结:
逐日推进回测模式 不是为了替代现有高层回测 API,而是为了把当前批处理回测拆成可复用、可推进、可演进的内核。