回测分四步走:枚举 → 价格因子 → 资金 → 决策者模式。
第一步"枚举"做的事很简单——遍历整个回测时间窗口,逐日扫每只股票,看你的策略钩子 has_opportunity 有没有命中。命中了就记一条机会,没命中就跳过。这一步不模拟买卖,只负责"找信号"。
案例:用 RSI v1 跑一次枚举
跑枚举
python cli.py se --strategy rsi_v1se 是 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 Falsersi_value 和 volume 会被保存下来,在报告里可以看到每次触发时 RSI 具体是多少、成交量多大。帮你理解策略在什么条件下触发。