Skip to content

Stateful Step 示例详解

本文档详细说明仓库内这个示例是如何运作的:

  • examples/run_stateful_step_limit_up_then_limit_up.py

它演示的是一种最小但完整的 stateful step 策略模式:

  • 第一次涨停时,不立刻买入
  • 先把股票放进待入池列表
  • 下一交易日只做待确认,收盘后才正式进入观察池
  • 后续某天再次涨停时,才进入当天买入池
  • 买入和卖出仍由统一回测执行内核负责

这个示例的重点不是策略收益,而是帮助理解:

  • run_stateful_step_backtest(...) 到底做了什么
  • 策略对象应该负责什么
  • 回测引擎应该负责什么
  • 为什么这种模式比“每个策略都自己写逐日主循环”更适合复用

1. 这个模式解决什么问题

对于简单 selector,策略只需要回答:

  • 今天选谁

但对于复杂一点的日频策略,往往还需要记住前几天的状态,例如:

  • 某只股票之前已经触发过一次信号
  • 某只股票已经在观察池里
  • 某只股票在观察池里待太久了,需要移除
  • 某只股票已经买过,不能重复买

这类策略的关键不只是“今天选谁”,而是:

  • 今天状态怎么推进
  • 今天基于这个状态应该买谁

这就是 stateful step 的适用场景。

2. 示例文件里的几个角色

这个示例文件里主要有四类对象。

2.1 参数对象

StrategyParams

职责:

  • 收拢示例策略用到的参数
  • 让策略对象初始化时更清晰

当前参数包括:

  • watch_window
  • close_rate_threshold
  • max_candidates
  • max_positions

2.2 当日候选筛选函数

select_limit_up_hits_from_market_frame(...)

职责:

  • 从某个交易日的全市场日线里找出“收盘涨停”的股票
  • 返回结构化候选列表

这里它只做“当天筛选”,不维护任何跨日状态。

也就是说:

  • 这是策略里的纯函数部分
  • 它负责把市场数据变成当天候选

2.3 状态策略对象

LimitUpThenLimitUpStrategy

这是整个示例的核心。

它自己维护:

  • watchlist
  • watchlist_added_index
  • pending_watchlist
  • pending_confirmation_watchlist
  • debug_by_date

这几个字段共同表达了策略自己的状态机。

其中:

  • pending_watchlist 表示今天第一次涨停、当天刚命中的股票
  • pending_confirmation_watchlist 表示次日处于待确认阶段、当天不是买点的股票
  • watchlist 表示已经在观察池里、等待再次涨停的股票
  • watchlist_added_index 用来控制观察池超期移除
  • debug_by_date 用来保存逐日调试输出

2.4 统一 runner

run_stateful_step_backtest(...)

它不负责定义策略规则,它负责:

  • 逐日驱动策略对象
  • 调用统一回测执行内核
  • 汇总成标准 BacktestResult

也就是说:

  • 示例策略决定今天买入池是什么
  • runner 决定如何按交易日调用策略并执行回测

3. 一天是怎么推进的

这个模式下,一个交易日的处理顺序可以概括成:

  1. 进入策略的 on_day(...)
  2. 策略先推进自己的内部状态
  3. 策略读取当天市场数据
  4. 策略产出当天的 pool_codes
  5. runner 调用 step_backtest_day(...)
  6. 引擎完成买卖、持仓、权益更新
  7. 如果策略实现了 on_after_step(...),再把真实成交结果回写给策略

可以把它理解成:

  • on_day(...) 负责“策略决策前半段”
  • step_backtest_day(...) 负责“交易执行”
  • on_after_step(...) 负责“策略状态回写”

4. 这个示例里 on_day(...) 做了什么

示例策略的 on_day(...) 主要做五件事。

4.1 把昨天完成待确认的股票转入观察池

如果 pending_confirmation_watchlist 里有股票:

  • 说明这些股票昨天已经走完“次日待确认”
  • 今天开始正式进入观察池

于是它们会被加入:

  • watchlist

并记录加入观察池的交易日索引。

4.2 清理超期观察池

如果某只股票进入观察池后超过 watch_window 天仍未再次涨停买入:

  • 就从观察池移除

这一步说明策略对象完全可以维护自己的时间窗口规则。

4.3 读取当天市场并筛出“再次涨停”的标的

策略通过:

  • context.get_market_bars_polars()

拿到当天全市场日线,再交给:

  • select_limit_up_hits_from_market_frame(...)

筛出当天收盘涨停的标的。

然后再和观察池求交集:

  • 在观察池里
  • 且今天再次涨停

这些股票就构成:

  • today_buy_pool

4.4 把今天第一次涨停的新股票记为待入池

当天涨停候选里,如果某只股票:

  • 既不在观察池
  • 也不在待入池列表
  • 也不在待确认列表

那就说明这是它“第一次涨停被策略捕捉到”。

这些股票会进入:

  • pending_watchlist

注意这里不是立即进入 watchlist

这个示例的完整时序是:

  • T 日第一次涨停:进入 pending_watchlist
  • T+1 日:转入 pending_confirmation_watchlist,当天不是买点
  • T+1 日收盘后:完成待确认
  • T+2 日开始:正式进入 watchlist

4.5 产出 StrategyDayDecision

最后策略返回:

  • pool_codes=today_buy_pool

这表示:

  • 今天真正允许引擎去尝试买入的股票列表

此时策略并没有自己下单,也没有自己算资金。

5. 这个示例里 on_after_step(...) 做了什么

当引擎完成当天执行后,runner 会把 step_result 回传给策略。

这个示例里,on_after_step(...) 主要做一件事:

  • 如果某只股票今天真的买成了,就从观察池里移除

为什么要在这里做,而不是在 on_day(...) 里做?

因为 on_day(...) 只能知道:

  • 今天想买谁

但它还不知道:

  • 有没有真的成交

真正的成交结果只有在 step_backtest_day(...) 执行完以后才知道。

所以:

  • on_day(...) 面向“策略意图”
  • on_after_step(...) 面向“执行结果回写”

这就是这个模式很关键的一个分界点。

6. runner 内部做了什么

run_stateful_step_backtest(...) 的内部逻辑可以理解成:

for trade_date in trade_dates:
    context = StrategyStepContext(...)
    decision = strategy.on_day(context)
    step_result = step_backtest_day(..., pool_codes=decision.pool_codes)
    strategy.on_after_step(context, decision, step_result)

它额外还统一处理了这些公共工作:

  • 初始化 BacktestState
  • 构造 BacktestStepConfig
  • 生成 date_to_index
  • 传递 on_day 回调
  • 汇总 BacktestResult

所以相比手写脚本主循环,优化点在于:

  • 每个 stateful 策略不需要重复写逐日驱动外壳
  • 回测执行层和策略状态层的职责更清晰
  • 测试和未来 loader 都更容易复用这条标准链路

7. 为什么这个示例适合教学

这个示例比更复杂的真实策略更适合讲解 stateful step,因为它同时具备:

  • 明确的跨日状态
  • 简单可理解的规则
  • 逐日推进的可视化输出
  • 与引擎职责的清晰边界

它没有引入太多额外复杂性,例如:

  • 多阶段历史窗口判断
  • 太多数据结构转换
  • 太多策略特有术语

所以它更适合作为:

  • run_stateful_step_backtest(...) 的第一份教学样板

8. 如何运行和观察输出

运行方式:

uv run --python .venv\Scripts\python.exe examples/run_stateful_step_limit_up_then_limit_up.py

你会看到逐日输出,包括:

  • observe_pool
  • pending_new
  • pending_confirm
  • expired
  • buy_pool
  • buys
  • sells
  • equity

这个示例当前采用的是:

  • 每个交易日执行完成后,立即通过 on_day 回调实时打印

而不是:

  • 等整段回测结束后再统一打印

可以这样理解这些字段:

  • observe_pool:今天开始时,策略正在观察哪些股票
  • pending_new:今天第一次涨停、刚进入待入池列表的股票
  • pending_confirm:今天处于待确认阶段、收盘后下一交易日才会进入观察池的股票
  • buy_pool:今天允许交给引擎尝试买入的股票
  • buys / sells:引擎真实执行出来的成交结果

9. 一句话总结

这个示例说明了 stateful step 模式的核心心智:

  • 策略对象负责维护自己的跨日状态机
  • runner 负责逐日驱动策略并调用统一回测执行内核
  • 引擎负责成交、持仓、资金和权益

也就是说,stateful step 不是“策略自己包办一切”,而是:

  • 把“策略状态”
  • 和“账户执行”

拆成两层,再用统一的逐日 runner 把它们连接起来。