Skip to content

逐日推进回测方案

本文档定义 zelqor 下一阶段的回测内核演进方向:把当前“整段区间一次性跑完”的最小回测,进一步拆成:

  • 状态对象
  • 单日 step 执行
  • 历史回测驱动器

目标不是立即做成模拟盘,而是先让回测内核具备更强的“逐日推进”能力,为后续模拟盘 runtime 复用打基础。

1. 为什么要做这一层

当前 zelqor 已经具备两条可用回测链路:

  • run_precomputed_backtest(...)
  • run_selector_backtest(...)

它们内部已经按交易日循环执行,但仍然属于“批处理回测”:

  • 先拿完整 trade_dates
  • 一次性跑完全部区间
  • 最后返回 BacktestResult

这种模式适合快速回测,但对后续这两类能力不够友好:

  • 更细粒度的逐日调试
  • 向模拟盘 runtime 演进

因此需要新增一层明确的“逐日推进模式”。

2. 目标

这一阶段希望把回测内核演进成三层:

  1. step 级日频执行内核
  2. 基于历史交易日驱动的回测执行器
  3. 未来可复用的模拟盘驱动基础

最终希望达到:

  • 每次只推进一个交易日
  • 每次推进都能拿到当天结果
  • 账户、持仓、交易记录保存在统一 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(...)

继续保留,但内部改成:

  1. 初始化 state
  2. 遍历 trade_dates
  3. 每个交易日调用 step_backtest_day(...)
  4. 最后汇总 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,例如:

  • PaperRuntimeState
  • on_market_event(...)
  • on_trade_day_open(...)
  • on_trade_day_close(...)

也就是说:

  • 历史回测使用“历史日期驱动 step”
  • 模拟盘使用“实时事件驱动 step / runtime”

两者共享底层交易执行语义。

8. 非目标

这一阶段明确不做:

  • 分钟级或 tick 级执行
  • 挂单队列和撤单系统
  • 部分成交
  • 长生命周期运行时恢复
  • 模拟盘完整 runtime
  • 策略文件动态加载

9. 推荐实施顺序

建议按以下顺序推进:

  1. 抽出 BacktestState
  2. 抽出 BacktestStepConfig
  3. 抽出 DayStepResult
  4. 实现 step_backtest_day(...)
  5. run_precomputed_backtest(...) 改成基于 step 驱动
  6. 保持现有测试通过
  7. 新增一个 examples/run_step_backtest.py

10. 预期收益

完成这一阶段后,zelqor 会得到这些收益:

  • 回测逻辑更清晰,可测试性更强
  • 逐日行为更容易调试
  • 逐日回调语义更自然
  • 更容易扩展不同驱动器
  • 为模拟盘 runtime 留出更干净的复用边界

一句话总结:

逐日推进回测模式 不是为了替代现有高层回测 API,而是为了把当前批处理回测拆成可复用、可推进、可演进的内核。