运行回测中第一步:机会枚举以及它的报告解读

回测分四步走:枚举 → 价格因子 → 资金 → 决策者模式。

第一步"枚举"做的事很简单——遍历整个回测时间窗口,逐日扫每只股票,看你的策略钩子 has_opportunity 有没有命中。命中了就记一条机会,没命中就跳过。这一步不模拟买卖,只负责"找信号"。

案例:用 RSI v1 跑一次枚举

跑枚举

python cli.py se --strategy rsi_v1

se 是 strategy_enumerate 的缩写。跑完后终端会输出机会总数、触发股票数等摘要信息。

 

报告在哪

枚举产物在策略目录下:

userspace/extensions/strategies/rsi_v1/results/simulations/{version_id}/enum/
├── overall_report.json       # 总体报告
├── entity_list.json          # 逐股摘要
├── performance.json          # 运行性能
├── runtime_env.json          # 运行环境元数据
├── entity_ids.txt            # 参与回测的股票列表
└── entities/
    ├── 000001_stock_investments.csv      # 每只股票的投资记录
    ├── 000001_signal_snapshots.csv       # 信号快照
    ├── 000001_goal_achievements.csv     # 目标达成记录
    └── ...

 

读总体报告

打开 overall_report.json,关注 summary 里的这几个数:

机会概览

total_opportunities: 342       # 3 年里一共发现 342 次机会
total_stocks: 4500             # 参与回测的股票总数
trigger_stocks: 280            # 其中 280 只至少触发过一次
trigger_ratio: 0.062           # 只有 6.2% 的股票有过信号
avg_per_stock: 1.22            # 平均每只有信号的股票产生了 1.22 次机会

先看 trigger_ratio——如果只有 1% 的股票触发过,说明策略条件很严格,信号稀少;如果 50% 都触发过,说明条件太松,几乎每只股票都有信号。RSI v1 这种超买超卖策略,6% 左右是合理的。

 

完成情况

completed_count: 310            # 310 次机会已经走完(达到止盈/止损/过期)
unfinished_count: 32            # 32 次机会到回测结束还没平仓
completed_ratio: 0.906         # 90.6% 的机会已经走完

unfinished_count 不一定是坏事——可能是回测窗口末尾产生的机会,还没来得及走到出场条件。但如果未完成比例很高,说明你的过期策略可能太宽松了。

 

机会分布

opportunity_count_labels: ["0", "1-2", "3-5", "6-10", "10+"]
opportunity_count_stock_counts: [4220, 180, 65, 30, 5]

这告诉你机会集中度。大部分股票(4220 只)0 次机会,少数股票产生了多次信号。如果有个别股票产生了 10+ 次机会,值得关注——可能是这只股票反复震荡,也可能策略对它过于敏感。

 

节奏与分散度

mean_gap: 45.2              # 平均每股机会间隔 45 天
mean_duration: 12.3         # 平均每次机会持续 12 天
cv: 0.38                    # 变异系数
dispersion_conclusion: "均匀"   # 机会分布均匀

dispersion_conclusion 有三档:均匀(cv < 0.45)、中等聚集(cv < 0.8)、较集中(cv >= 0.8)。均匀是好事——说明策略不依赖某段特定行情。如果"较集中",可能策略只在某种行情下有效,换个时间窗口就不行了。

 

可交易性

buy_at_limit_up_count: 12       # 12 次因为涨停买不进去
limit_up_buy_ratio: 0.035      # 占比 3.5%
sell_at_limit_down_count: 3    # 3 次因为跌停卖不出去
limit_down_sell_ratio: 0.009   # 占比 0.9%

涨停买不进去、跌停卖不出去是 A 股的常态。如果这个比例很高(比如 20%+),说明策略经常在极端行情触发,实盘可执行性存疑。

读逐股报告

entity_list.json 是每只股票一行:

entity_id: "000001"
stock_name: "平安银行"
opportunities: 3              # 这只股票产生了 3 次机会
completion_rate: 1.0          # 100% 都走完了
avg_gap_days: 48.5            # 平均间隔 48 天

用这个表可以快速定位"哪些股票贡献了最多机会"和"哪些股票的机会间隔最短"。

其他可用配置

执行模式

settings.py 里的 simulation.mode 控制枚举怎么跑:

"simulation": {
    "mode": "slice_based",       # 按时间切片,每片处理全部股票
    # 或
    "mode": "entity_based",     # 按股票分组,每组独立跑完
}
  • slice_based:把时间切成段,每段处理所有股票。适合多股票、中等时间窗口
  • entity_based:把股票分组,每组独立跑完整时间线。适合少股票、长时间窗口

 

回测窗口

"simulation": {
    "start_date": "20220101",
    "end_date": "20241231",
}

 

入场价格

"simulation": {
    "enter_price": "close",       # 用收盘价作为触发价
    # 或
    "enter_price": "next_open",   # 用次日开盘价
}

 

抽样

如果您不想对全市场进行回测,也可以抽样:

"sampling": {
    "use_sampling": True,
    "sampling": {
        "amount": 500,            # 只跑 500 只
    },
}

抽样是分层抽样,按"上市状态 × 板块 × 交易所"分层,保证样本有代表性。开发调试时用它,正式回测跑全量。

 

信号快照

每次 has_opportunity 返回 True 时,策略钩子里的 capture() 调用会被记录到 signal_snapshots.csv。你在钩子里写的:

def has_opportunity(self, ctx):
    rsi = ctx.data("klines").rsi(14)
    if rsi < 30:
        ctx.capture("rsi_value", rsi)
        ctx.capture("volume", ctx.data("klines").volume)
        return True
    return False

rsi_value 和 volume 会被保存下来,在报告里可以看到每次触发时 RSI 具体是多少、成交量多大。帮你理解策略在什么条件下触发。