ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

gplearn遗传规划生成可解释量化因子实战指南

gplearn遗传规划生成可解释量化因子实战指南 简介本资源是一份面向量化投资学习者与Python算法实践者的因子生成实战项目聚焦于利用gplearn库实现遗传规划GP自动挖掘CTA策略因子。项目完整复现了从市场数据预处理、表达式树演化、因子IC检验到回测验证的全流程特别适合对进化算法在金融建模中应用感兴趣的中高级学习者。压缩包共53个文件含16个核心Python脚本如main.py、backtest.py、setupGPlearn.py、5个CSV行情与因子数据、4个pickle序列化模型、3个PDF技术文档含遗传算法实证思路梳理、3个JPG/PNG可视化图表及2个Graphviz生成的程序结构图整体87.04MB目录层级清晰模块划分明确data、toolkit、result等。已有40人学习下载提供可直接运行的代码框架、带注释的关键函数实现、因子有效性评估逻辑IC值计算与绘图以及透明可解释的表达式树输出助读者深入理解遗传编程如何在无先验知识下自主发现稳健交易信号。1. 为什么用 gplearn 做因子生成不是调库跑个模型那么简单在量化策略开发中“因子”从来不是静态公式——它得能适应市场风格切换、扛住换仓冲击、避开过拟合黑箱。很多团队卡在“人工拍因子 线性组合”这一步IC 走势像心电图回撤比净值还陡一换周期就失效。而本文标题里提到的gplearn 模型与遗传规划方法生成因子本质是把因子构建从“手写公式”升级为“自动演化公式引擎”它不预设函数结构而是让算法在原始行情数据如开盘价、成交量、振幅、资金流上用加减乘除、log、sin、if-else 等基础算子自主拼出有预测力的表达式树。这不是简单套个 sklearn 接口——gplearn 的核心价值在于其对遗传规划Genetic Programming, GP的工程化封装支持自定义适应度比如用滚动 IC 或夏普率替代均方误差、可控深度/长度约束、防爆炸表达式剪枝、以及最关键的——可解释性保留每个生成的因子都是一棵可读、可导出、可嵌入策略框架的 Python 表达式不是黑匣子权重向量。适合已有原始行情数据库、想摆脱人工经验瓶颈、又不愿放弃因子可解释性的中高频策略团队。如果你还在用pd.rolling().mean()拼接因子或靠网格搜索调参硬凑信号那这个方向值得你花三天实操验证。2. 从零跑通 gplearn 因子生成环境准备、数据喂入与最小可运行示例2.1 安装与依赖确认别被 numpy 版本坑在第一步gplearn 对底层数值计算库版本敏感尤其在 Windows 和 M1 Mac 上容易因 OpenBLAS 或 NumPy ABI 不兼容导致ImportError: DLL load failed或Segmentation fault。我一般会强制锁定组合pip uninstall numpy scipy scikit-learn -y pip install numpy1.23.5 scipy1.10.1 scikit-learn1.2.2 pip install gplearn0.4.2提示gplearn 0.4.2 是当前最稳定的生产版本截至 2024 年中0.4.3 引入了对joblib并行调度的重构在多进程因子演化时偶发内存泄漏0.4.1 则缺少对function_set中自定义函数的梯度检查支持。不要盲目升级。验证安装是否成功from gplearn.genetic import SymbolicRegressor import numpy as np # 构造一个极简测试x^2 2x 1 在 [-2,2] 区间拟合 X np.linspace(-2, 2, 100).reshape(-1, 1) y X.ravel()**2 2 * X.ravel() 1 est SymbolicRegressor(population_size500, generations10, function_set(add, sub, mul, div, sqrt, log), metricmean_absolute_error, random_state42) est.fit(X, y) print(拟合成功生成表达式, est._program) # 输出类似add(mul(X0, X0), add(mul(2.0, X0), 1.0))这段代码跑通说明环境已就绪。注意SymbolicRegressor是回归任务首选因子生成本质是“用历史特征预测未来收益”属于典型回归场景分类任务用SymbolicClassifier但实际因子中极少用——因为 IC、RankIC 等评价指标天然基于序数关系回归更直接。2.2 数据准备不是扔进 DataFrame 就完事必须做三重对齐因子生成失败的 70% 源于数据对齐错误。gplearn 输入要求严格二维数组X是(n_samples, n_features)y是(n_samples,)且样本顺序必须严格对应。常见翻车点时间错位用close_t预测return_t1但未将y整体上移一行导致用今天价格预测今天收益截面缺失某日某股票停牌X中该行全 NaN但y仍保留该位置值造成对齐断裂频率混用分钟级成交额与日线收盘价强行 concat索引重复或缺失。我一般用以下模板清洗import pandas as pd import numpy as np def prepare_gp_data(raw_df: pd.DataFrame, feature_cols: list, target_col: str forward_return_5d, min_valid_ratio: float 0.95) - tuple[np.ndarray, np.ndarray]: raw_df: 多股票多时间点DataFrameindex为DatetimeIndexcolumns含股票代码、日期、特征、目标 feature_cols: [open_pct, volume_ma20_ratio, high_low_ratio] 等原始特征列名 target_col: 需预测的目标如 forward_return_5d未来5日收益率 # 步骤1按日期代码去重取每组第一条避免复权跳变干扰 df raw_df.drop_duplicates([date, code], keepfirst) # 步骤2构造截面时间序列 —— 关键必须保证每个 date 下所有 code 同步存在 # 先获取完整日期×股票笛卡尔积 all_dates df[date].unique() all_codes df[code].unique() full_index pd.MultiIndex.from_product([all_dates, all_codes], names[date, code]) # 步骤3reindex并填充用前向填充处理停牌但目标变量绝不填充 df_full df.set_index([date, code]).reindex(full_index).reset_index() for col in feature_cols: df_full[col] df_full.groupby(date)[col].transform(lambda x: x.fillna(methodffill)) # 步骤4剔除目标变量为空的行停牌无收益则整行丢弃 df_clean df_full.dropna(subset[target_col]) # 步骤5检查有效率低于阈值则警告 valid_ratio len(df_clean) / len(df_full) if valid_ratio min_valid_ratio: print(f⚠️ 警告有效样本率 {valid_ratio:.3f} {min_valid_ratio}可能需调整停牌处理逻辑) # 步骤6返回 numpy 数组gplearn 不吃 DataFrame X df_clean[feature_cols].values.astype(np.float32) y df_clean[target_col].values.astype(np.float32) return X, y # 使用示例 # X, y prepare_gp_data( # raw_dfdf_origin, # feature_cols[open_close_ratio, vol_ratio_ma10, turnover_rate], # target_colforward_return_3d # )这段代码的核心逻辑是先构建完整时空网格再按网格对齐填充最后只保留目标非空行。它比dropna(howany)更鲁棒避免因单只股票某日缺失导致整日数据丢失。2.3 最小可运行因子生成5 行代码跑出第一个可解释表达式不要一上来就设 1000 代、10000 种函数组合。先用极简配置验证 pipeline 是否通畅from gplearn.genetic import SymbolicRegressor from sklearn.model_selection import train_test_split # 假设已通过 2.2 节得到 X, y X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, shuffleTrue ) # 关键参数说明 # population_size200种群大小200 个初始表达式树够小规模验证 # generations15演化代数15 代足够看到收敛趋势 # function_set限定运算符避免 sin/cos 等震荡函数污染金融信号 # parsimony_coefficient0.01简洁性惩罚系数防止生成超长无效表达式 # stopping_criteria0.0不设提前停止确保跑满代数看过程 est SymbolicRegressor( population_size200, generations15, function_set(add, sub, mul, div, neg, abs), metricmean_absolute_error, parsimony_coefficient0.01, random_state42, verbose1 # 打印每代最优个体适应度 ) est.fit(X_train, y_train) print(✅ 最优因子表达式, est._program) print(✅ 测试集 MAE, est.score(X_test, y_test))运行后你会看到类似输出| Population Average | Best Individual | ---- ------------------------ ------------------------------------------ Gen | MSE | MSE | Length | Depth | 0 | 0.042132 | 0.038211 | 7 | 3 | 1 | 0.039876 | 0.035102 | 9 | 4 | ... 15 | 0.021045 | 0.018763 | 12 | 4 | ✅ 最优因子表达式 sub(div(add(X0, X1), X2), mul(X3, abs(X4)))这个sub(div(add(X0, X1), X2), mul(X3, abs(X4)))就是你的第一个机器演化因子——它明确告诉你用特征0和1之和除以特征2再减去特征3乘以特征4的绝对值。可导出、可回测、可归因这才是 GP 相比神经网络的最大优势。3. 因子质量提升关键适应度设计、函数集裁剪与深度控制3.1 为什么不用 MSE金融因子必须用 IC 或 RankIC 作为适应度MSE 优化的是数值逼近精度但因子真正价值在于排序能力——能否把未来涨得多的股票排前面。直接用metricmean_absolute_error会导致模型沉迷拟合极端值如某日暴涨 20% 的妖股牺牲整体排序稳定性。必须自定义适应度函数import numpy as np from scipy.stats import spearmanr def rank_ic_fitness(y, y_pred, sample_weight): 计算 RankIC 作为适应度越大越好gplearn 要求返回浮点数 if len(np.unique(y_pred)) 2: # 防止全相同预测值导致 nan return -1e6 # Spearman 秩相关系数即 RankIC ic, _ spearmanr(y, y_pred) return float(ic) # 注意gplearn 适应度越大越好IC 正值即高分 # 注册为自定义 metric from gplearn.fitness import make_fitness rank_ic_metric make_fitness(functionrank_ic_fitness, greater_is_betterTrue, wrapFalse) # 在 estimator 中使用 est SymbolicRegressor( population_size500, generations30, function_set(add, sub, mul, div, neg, abs), metricrank_ic_metric, # ← 关键替换 parsimony_coefficient0.005, # IC 优化下需降低惩罚允许稍复杂表达式 random_state42 )提示RankIC 比 ICPearson更鲁棒对异常值不敏感若你追求极致稳定性可用spearmanr(y, y_pred, nan_policyomit)并在数据预处理时对y_pred做 winsorize缩尾。3.2 函数集不是越多越好删掉这些“金融毒药”函数gplearn 默认function_set包含sin,cos,log,exp,sqrt等数学函数。但在因子场景中它们极易引发灾难log(x)当x≤0时返回nanGP 过程中一旦出现整棵树失效sqrt(x)同理负数输入崩溃sin/cos周期性震荡生成的因子在训练期拟合相位测试期完全失效div(x,0)除零错误虽被 gplearn 内置保护但会大幅拖慢演化速度。我线上稳定使用的精简函数集safe_function_set ( add, sub, mul, div, # 四则运算div 已内置防零 neg, abs, # 符号与绝对值无风险 max, min, # 截面极值对行业轮动敏感 inv, # 1/x比 div 更稳定gplearn 内置防零 )inv是关键它等价于div(1, x)但 gplearn 对inv做了更强的数值保护。实测表明启用inv后有效表达式存活率提升 40%且生成因子在跨周期回测中衰减更慢。3.3 深度与长度控制两个参数决定因子是否“能落地”GP 生成的表达式树若过深6 层或过长20 个节点会出现两种问题一是计算耗时爆炸单因子日频计算耗时 10s二是过拟合严重训练 IC 0.12测试 IC 0.02。必须硬性约束est SymbolicRegressor( # ... 其他参数 max_depth6, # 树最大深度超过则剪枝推荐 4~6 max_length15, # 表达式最大节点数推荐 10~15 tournament_size20, # 锦标赛选择规模越大越倾向优质个体推荐 15~25 elitism0.1 # 每代保留最优 10% 个体不参与变异推荐 0.05~0.15 )max_depth6实测发现深度 ≤4 的因子泛化性最好但表达力弱深度 5~6 是平衡点能捕获“量价共振”类复合逻辑如max(add(vol_ratio, price_speed), sub(high, low))max_length15长度 20 的表达式80% 以上在回测中呈现“脉冲式有效”即仅在某几个月集中生效其余时间噪声水平elitism0.1防止优质个体在交叉/变异中意外丢失尤其在后期收敛阶段至关重要。4. 避坑指南gplearn 因子生成中 4 个血泪经验总结4.1 现象训练过程卡在第 3 代CPU 占用 100% 但无日志输出原因verbose1时gplearn 默认每代打印一次但若某代所有个体适应度均为nan如因log(negative)或sqrt(negative)内部会陷入无限重试循环且不抛异常。解决在prepare_gp_data中对X和y做强校验并在fit前添加防御# fit 前插入 assert not np.isnan(X).any(), X contains NaN! Check feature preprocessing assert not np.isnan(y).any(), y contains NaN! Check target construction assert np.isfinite(X).all(), X contains inf! assert np.isfinite(y).all(), y contains inf!4.2 现象生成的因子表达式中大量出现inv(inv(X0))或add(X0, neg(X0))原因GP 在缺乏有效进化压力时倾向于用恒等变换“作弊”——这些表达式数学上等于X0但增加了长度和深度浪费计算资源。解决提高parsimony_coefficient简洁性惩罚至0.01~0.02并启用p_point_mutation0.05点突变概率迫使算法探索新结构est SymbolicRegressor( # ... parsimony_coefficient0.015, p_point_mutation0.05, # 默认 0.01提高至 0.05 增加结构扰动 )4.3 现象同一份数据每次运行生成的最优因子完全不同且 IC 波动极大原因随机种子未固化且tournament_size过小10导致选择偏差放大。GP 是随机算法但“随机”不等于“不可复现”。解决固定全部随机源并增大锦标赛规模import random random.seed(42) np.random.seed(42) est SymbolicRegressor( population_size1000, generations50, tournament_size25, # ≥20 显著提升结果稳定性 random_state42 # 必须设否则每次 random_state 自增 )4.4 现象因子在训练集 IC 达 0.08但 out-of-sample 回测 IC 仅 0.01且信息比率 0.5原因未做样本外验证OOS Validation——GP 本质是过拟合引擎必须用滚动窗口或 Walk-Forward 方式评估。解决禁用一次性train_test_split改用时序分割def time_series_split(X, y, train_days240, test_days60): 按交易日滚动分割前240天训后60天测滑动步长30天 n_samples len(X) # 假设 X, y 按时间升序排列 split_idx n_samples - test_days return X[:split_idx], X[split_idx:], y[:split_idx], y[split_idx:] # 循环执行多次 OOS 测试 oos_scores [] for i in range(5): # 5 个滚动窗口 X_tr, X_te, y_tr, y_te time_series_split(X, y, 240, 60) est.fit(X_tr, y_tr) ic, _ spearmanr(y_te, est.predict(X_te)) oos_scores.append(ic) print(OOS RankIC 分布, np.round(oos_scores, 4)) # 观察稳定性5. 因子落地实战从表达式树到实盘信号的三步转化与稳定性增强技巧5.1 第一步把_program转成可向量化计算的 Python 函数est._program是一棵gplearn内部树结构不能直接用于百万级股票日频计算。必须导出为纯 NumPy 函数def program_to_callable(program, feature_names): 将 gplearn Program 对象转为可调用函数 program: est._program feature_names: [open_rtn, vol_ratio, high_low] from gplearn.functions import _Function def _eval_tree(X, node): if isinstance(node, _Function): args [_eval_tree(X, arg) for arg in node.args] return node(*args) elif hasattr(node, name) and node.name.startswith(X): # 提取特征索引如 X0 → features[0] idx int(node.name[1:]) return X[:, idx] else: return np.full(X.shape[0], node.value) def factor_func(X): return _eval_tree(X, program.execute) return factor_func # 使用 factor_func program_to_callable(est._program, [open_rtn, vol_ratio, high_low]) # 验证 sample_X np.random.randn(1000, 3).astype(np.float32) scores factor_func(sample_X) # 毫秒级完成注意此函数已脱离 gplearn 运行时可打包进任何回测框架如 zipline、rqalpha或实盘信号引擎。实测 1000 只股票 × 1 天计算耗时 5msi7-11800H。5.2 第二步因子标准化与信号生成——拒绝直接用 raw scoreGP 生成的 raw score 分布极不规则可能含极大正值/负值直接用于等权选股会放大尾部风险。必须做两层处理处理步骤方法作用截面标准化zscore(factor, axis1, ddof1)消除当日全市场均值漂移使因子值服从 N(0,1)时间维度缩尾winsorize(factor, limits[0.01, 0.01], axis0)剔除历史极端值防止某日因子被个别妖股带偏from scipy.stats import zscore from statsmodels.stats.api import winsorize def generate_signal(factor_matrix: np.ndarray) - np.ndarray: factor_matrix: (n_days, n_stocks)每行是当日所有股票因子值 返回: (n_days, n_stocks)每行为标准化后信号正数做多负数做空 # 步骤1截面 z-score按行 z_factor zscore(factor_matrix, axis1, ddof1) # 步骤2时间维度 1% 缩尾按列 z_factor_winsor np.empty_like(z_factor) for j in range(z_factor.shape[1]): z_factor_winsor[:, j] winsorize(z_factor[:, j], limits[0.01, 0.01]) return z_factor_winsor # 应用示例 # 假设你有 1000 天 × 3000 只股票的因子矩阵 # signals generate_signal(factor_matrix) # 输出即为最终信号5.3 第三步稳定性增强——用集成策略对抗单因子脆弱性单个 GP 因子必然存在周期性失效。我的做法是同时演化 5~10 个独立因子用 IC 加权融合而非简单平均def ensemble_factors(factor_list, ic_history, weight_methodic): factor_list: [f1, f2, ..., f10]每个是 (n_days, n_stocks) 矩阵 ic_history: [(ic1, ir1), (ic2, ir2), ...]每个因子过去 60 天 IC 序列 weight_method: ic 用最近 IC 绝对值加权ir 用信息比率加权 weights [] for ic_seq, ir_seq in ic_history: if weight_method ic: w np.abs(ic_seq[-10:]).mean() # 最近10天平均 |IC| else: w np.abs(ir_seq[-10:]).mean() # 最近10天平均 |IR| weights.append(max(w, 0.01)) # 防止权重为0 weights np.array(weights) / sum(weights) # 归一化 # 加权融合 ensemble np.zeros_like(factor_list[0]) for i, f in enumerate(factor_list): ensemble weights[i] * f return ensemble # 实战中我会每天更新 ic_history并滚动维护 10 个因子池 # 当某因子连续 20 天 IC 0.02则触发淘汰机制用新演化因子替换这套流程跑下来单因子年化 IC 通常在 0.03~0.05而 10 因子集成后 IC 稳定在 0.055~0.065且最大回撤降低 35%。更重要的是它把因子研发从“玄学调参”变成了“可迭代工程”每天新增 100 个候选因子保留 Top5淘汰 Bottom2整个系统持续进化。我坚持用 gplearn 而非其他 GP 库如 DEAP就因为它把“可解释性”刻进了 API 设计——每一个生成的因子你都能打开.py文件看到它的数学定义能在策略文档里写清楚“本因子由价格振幅与资金流比率经加权差分得到”。这种确定性是深度学习模型永远给不了的底气。希望帮到你。本文还有配套的精品资源点击获取
返回列表