Betalens新手系列 · 12 参数扫描与滚动窗口:mining 模块实战
阅读提示
这篇讲解因子参数优化的高级工具——betalens.factor.mining 模块。它能帮你自动扫描多个参数组合,找出 ICIR 最高或分组收益差距最大的参数配置。适合在做正式回测之前做参数敏感性分析。
导言
任何因子都有可调参数:滚动窗口长度、去极值方法、分组数、调仓频率。这些参数没有”标准答案”,需要结合数据来搜索。
Betalens 的 mining 模块提供两种搜索策略:
- 参数扫描(
ParameterSweepConfig):给定参数范围网格,全量组合搜索。
- 滚动窗口(
RollingMiningConfig):给定窗口长度序列,模拟 Walk-Forward 检验。
参数搜索的产出不是”最优参数”,而是参数敏感性和稳健性报告——帮你判断:这个因子的收益是来自真实规律,还是来自对某组特定参数的过拟合?
参数扫描:run_parameter_sweep
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| from betalens.factor.mining import ( ParameterSweepConfig, run_parameter_sweep, )
config = ParameterSweepConfig( factor_key="股息率(报告期)", param_grid={ "winsorize_method": ["mad", "percentile", "std"], "n_mad": [3.0, 4.0, 5.0], "standardize_method": ["zscore", "rank"], "n_quantiles": [5, 10, 20], }, engine="vector", max_memory_ratio=0.5, cache_dir="_sweep/_cache", )
results = run_parameter_sweep( config, days, date_ranges, code_ranges, start_date="2020-01-01", end_date="2024-12-31", n_jobs=4, )
|
engine 参数:
输出结果
1 2 3 4 5 6 7 8
| print(results.columns)
best = results.loc[results["ICIR"].idxmax()] print(f"最优参数:{best.to_dict()}")
|
结果可视化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import matplotlib.pyplot as plt
pivot = results.pivot_table( values="ICIR", index="n_mad", columns="standardize_method", ) plt.figure(figsize=(8, 6)) plt.imshow(pivot.values, cmap="RdYlGn", aspect="auto") plt.xticks(range(len(pivot.columns)), pivot.columns) plt.yticks(range(len(pivot.index)), pivot.index) plt.colorbar(label="ICIR") plt.xlabel("standardize_method") plt.ylabel("n_mad") plt.title("ICIR 参数敏感性热力图") plt.tight_layout() plt.savefig("sensitivity_heatmap.png")
|
滚动窗口:run_walk_forward
参数扫描的隐含假设是”最优参数在历史上一致有效”。但金融市场结构会变化——2015 年股灾前后的最优参数可能完全不同。滚动窗口(Walk-Forward)通过分段检验来验证参数的稳健性。
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| from betalens.factor.mining import ( RollingMiningConfig, run_walk_forward, )
rolling_config = RollingMiningConfig( window_lengths=[252, 504], steps=[21, 63], out_of_sample_ratio=0.3, param_grid={ "winsorize_method": ["mad"], "n_mad": [3.0, 4.0], "n_quantiles": [5, 10], }, engine="exact", )
rolling_results = run_walk_forward( rolling_config, days, date_ranges, code_ranges, start_date="2015-01-01", end_date="2024-12-31", )
|
窗口长度的生成逻辑:window_lengths × steps 笛卡尔积生成窗口序列,并过滤 step <= window_length(确保步长不超过窗口长度)。
输出结果解读
1 2 3 4 5 6 7 8 9 10
| print(rolling_results.columns)
for _, row in rolling_results.iterrows(): print(f"窗口 {row.train_start.date()}~{row.train_end.date()} " f"→ 测试 {row.test_start.date()}~{row.test_end.date()}: " f"训练 ICIR={row.train_ICIR:.3f}, 测试 ICIR={row.test_ICIR:.3f}")
|
稳健性判断标准:
- 测试期 ICIR > 训练期 ICIR × 0.7:参数稳健性良好。
- 测试期 ICIR > 0.2:即使样本外有所衰减,仍有实际预测价值。
- 测试期 ICIR ≈ 0:参数严重过拟合,训练期 ICIR 不可信。
因子侧的 Hook 机制
如果你要在参数扫描的每个任务里执行自定义逻辑(比如在跑完 IC 之后自动生成报告),可以用 hook 机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from betalens.factor.mining import make_mining_spec, mining_valid_report
def make_mining_spec(params): """把参数字典转成 mining spec。""" return { "winsorize_method": params["winsorize_method"], "n_mad": params["n_mad"], "n_quantiles": params["n_quantiles"], }
def mining_valid_report(params, rank, output_dir, start_date, end_date): """每次有效运行后自动生成报告。""" if rank <= 5: from betalens.analyst import Analyst pass
config = ParameterSweepConfig( ... make_mining_spec_hook=make_mining_spec, mining_valid_report_hook=mining_valid_report, )
|
所有 hook 函数都是可选的,不传就使用默认行为。
开发者侧:内存保护与缓存
参数扫描可能产生大量中间数据(每个参数组合都要跑一次完整流水线)。Betalens 提供了两个保护机制:
1. 内存保护:
1 2 3 4
| config = ParameterSweepConfig( ... max_memory_ratio=0.5, )
|
2. 磁盘缓存:
1 2 3 4
| config = ParameterSweepConfig( ... cache_dir="_sweep/_cache/", )
|
第二次运行同样的参数组合时,直接从缓存读取,不重复计算。
常见错误
1. 参数网格太大导致计算爆炸
1 2 3 4 5 6 7 8 9 10 11
| param_grid={ "winsorize_method": ["mad", "percentile", "std"], "n_mad": [2.0, 3.0, 4.0], "standardize_method": ["zscore", "rank", "minmax"], "n_quantiles": [5, 10, 20], "rebal_freq": ["W", "M", "Q"], }
config = ParameterSweepConfig(param_grid={...}, engine="vector")
|
2. 滚动窗口步长大于窗口长度
1 2 3 4 5 6 7 8 9 10 11
| rolling_config = RollingMiningConfig( window_lengths=[252], steps=[504], )
rolling_config = RollingMiningConfig( window_lengths=[252], steps=[63], )
|
3. 用训练期最优参数做样本外测试
滚动窗口结果里,test_ICIR 是用测试期数据计算的,和训练期参数无关。如果你用 rolling_results["best_params"] 手动重新跑测试期,就会产生”用训练数据选参数再验证训练数据”的逻辑错误。
延伸阅读