gplearn遗传规划量化因子挖掘:从因子生成到回测评估全流程 简介这份资源是一套基于gplearn遗传规划算法的量化投资因子生成框架面向具备一定Python与量化基础的投资者和研究者用于解决人工设计因子主观性强、覆盖面有限的问题。项目通过遗传规划自动演化时序因子与选股因子并配套回测评估与可视化分析支持股票和期货两个市场。压缩包共54个文件约87.02MB以py脚本为主体辅以pyc缓存、csv与pickle数据文件、jpg/png图表、pdf说明及gv结构图等涵盖因子生成、回测、IC检验、数据处理与绘图等模块。已有46人学习下载。读者可据此获得从因子挖掘、策略回测到结果可视化的完整流程参考理解遗传规划在量化选股与CTA策略中的落地方式并借助现成脚本与数据快速复现实验、拓展自己的因子研究思路。1. 用 gplearn 遗传规划做量化因子挖掘为什么它比手工造因子更值得投入手工造因子的瓶颈不在灵感而在验证速度。你想到一个「量价背离」的逻辑写成代码、跑回测、看 IC一轮下来半天没了最后发现这个因子和已有因子相关性 0.9白干。gplearn 遗传规划算法做的事情就是把「想因子」这个环节自动化——它把算子、特征、常数丢进进化框架让程序自己组合出成千上万个候选因子再用适应度函数筛出有效的。这个项目标题指向的就是这样一套完整框架因子生成、回测评估、可视化分析全流程支持股票和期货市场。适合两类人一是手里有因子库但迭代慢的量化研究员二是想系统入门因子挖掘的工程师。核心热搜词 gplearn、遗传规划算法、量化投资、因子生成、回测评估后面会逐个落到代码和参数上。2. gplearn 遗传规划生成因子的原理与最小可跑通代码2.1 遗传规划到底在搜索什么空间遗传规划Genetic Programming, GP和遗传算法GA的区别在于GA 搜索的是参数向量GP 搜索的是表达式树。一棵表达式树就是一个因子公式叶子节点是特征变量或常数内部节点是算子。比如(close - open) / (high - low 1e-8)这棵树根节点是除法左子树是减法右子树是加法。gplearn 的SymbolicTransformer和SymbolicRegressor就是干这个的。搜索空间由三个东西决定函数集function_set、终端集特征列 常数、以及树的深度限制。函数集常见的有add、sub、mul、div、sqrt、log、abs、neg、max、min。注意div在 gplearn 里是保护除法分母为 0 时返回 1这避免了除零崩溃但也意味着你拿到的因子在某些样本上可能是常数后面要处理。适应度函数决定了进化方向。做因子挖掘时适应度通常用因子值和未来收益的 IC 绝对值或者 Rank IC。gplearn 默认的SymbolicTransformer用皮尔逊相关系数但量化场景下 Rank IC 更稳健需要自定义metric。2.2 用 SymbolicTransformer 生成因子的最小脚本import numpy as np import pandas as pd from gplearn.genetic import SymbolicTransformer from gplearn.functions import make_function from scipy.stats import spearmanr # 自定义 Rank IC 适应度函数 def _rank_ic(y, y_pred, w): if np.std(y_pred) 0: return 0.0 ic, _ spearmanr(y, y_pred) return abs(ic) if not np.isnan(ic) else 0.0 rank_ic make_function(function_rank_ic, namerank_ic, arity2) # 假设 df 是特征矩阵forward_return 是未来 N 日收益 feature_cols [open, high, low, close, volume, vwap, ret_1, ret_5, ret_20, turnover, amplitude] X df[feature_cols].values y df[forward_return].values # 关键参数population_size 越大搜索越充分但越慢 # generations 控制进化代数hall_of_fame 是最终保留的精英因子数 est SymbolicTransformer( population_size2000, generations20, hall_of_fame100, n_components20, function_set(add, sub, mul, div, sqrt, log, abs, neg), metricrank_ic, parsimony_coefficient0.001, max_samples0.8, p_crossover0.7, p_subtree_mutation0.1, p_hoist_mutation0.05, p_point_mutation0.05, random_state42, n_jobs-1, verbose1 ) est.fit(X, y) # 输出因子表达式 for i, prog in enumerate(est._best_programs[:5]): print(fFactor {i}: {prog})这段代码的逻辑SymbolicTransformer会进化出一批表达式每个表达式对 X 做变换后输出一列因子值。n_components20表示最终保留 20 个因子。hall_of_fame100是每代保留的最优个体池最后从里面选相关性低的 20 个。parsimony_coefficient是复杂度惩罚项越大越倾向于短表达式防止过拟合。参数说明几个关键的population_size低于 1000 时搜索空间覆盖不足容易早熟收敛generations一般 15 到 30再多边际收益递减max_samples0.8是每次评估只用 80% 样本类似 bagging能提升泛化p_crossover0.7是交叉概率保持较高值让好表达式组合各种 mutation 概率加起来控制在 0.2 左右。2.3 生成后的因子去重与筛选gplearn 输出的 20 个因子之间可能高度相关。直接全丢进模型会引入多重共线性。常见做法是算因子间的 Spearman 相关矩阵设定阈值 0.7用贪心法保留 IC 最高的那个。from scipy.stats import spearmanr factors est.transform(X) # shape: (n_samples, n_components) factor_df pd.DataFrame(factors, columns[fgp_{i} for i in range(factors.shape[1])]) # 计算因子间相关性 corr_matrix factor_df.corr(methodspearman).abs() # 贪心去重按单因子 IC 排序依次保留与已选因子相关性低于阈值的 ic_scores {} for col in factor_df.columns: ic, _ spearmanr(factor_df[col], y) ic_scores[col] abs(ic) if not np.isnan(ic) else 0 sorted_factors sorted(ic_scores, keyic_scores.get, reverseTrue) selected [] threshold 0.7 for f in sorted_factors: if all(corr_matrix.loc[f, s] threshold for s in selected): selected.append(f) print(fSelected {len(selected)} factors: {selected})逻辑说明先按单因子 IC 绝对值降序排列然后逐个检查与已选因子的最大相关性低于 0.7 才保留。这样能在保留预测力的同时控制冗余。阈值 0.7 是经验值因子库大时可以放宽到 0.8因子少时收紧到 0.6。3. 把遗传规划因子接进回测评估IC 衰减、分层和换手率3.1 因子评估的三个核心指标生成因子只是第一步评估才是决定要不要用的关键。三个指标必须看IC 均值与 ICIR、分层单调性、换手率。IC 均值衡量预测方向准确性一般日频因子 |IC| 0.03 就算可用 0.05 算优秀。ICIR IC 均值 / IC 标准差衡量稳定性 0.3 算合格。分层测试把股票按因子值分 5 组或 10 组看多空收益差是否单调。换手率决定交易成本遗传规划生成的因子如果包含高频价量数据换手率可能很高扣费后收益大打折扣。def evaluate_factor(factor_values, forward_returns, n_groups5): 评估单因子IC 序列、分层收益、换手率 df pd.DataFrame({factor: factor_values, ret: forward_returns}) df df.dropna() # 按时间截面计算 IC假设已有 date 列 ic_series df.groupby(date).apply( lambda x: spearmanr(x[factor], x[ret])[0] ).dropna() ic_mean ic_series.mean() ic_std ic_series.std() icir ic_mean / ic_std if ic_std ! 0 else 0 # 分层每个截面按因子值分 n_groups 组 df[group] df.groupby(date)[factor].transform( lambda x: pd.qcut(x, n_groups, labelsFalse, duplicatesdrop) ) group_ret df.groupby([date, group])[ret].mean().unstack() long_short group_ret[n_groups-1] - group_ret[0] # 换手率因子分组变化的频率 turnover (df.groupby(date)[group].apply( lambda x: (x.diff().abs() 0).mean() )).mean() return { IC_mean: round(ic_mean, 4), ICIR: round(icir, 4), long_short_annual: round(long_short.mean() * 252, 4), turnover: round(turnover, 4) }参数说明n_groups5是常用分层数股票池大时用 10。long_short_annual是多空组合年化收益的粗略估计实际回测还要考虑手续费和滑点。换手率这里用分组变化比例近似精确计算需要跟踪每期持仓变化。3.2 回测框架的对接方式遗传规划因子本质是一列数值对接回测框架有两种方式。一是作为因子直接输入多因子模型比如用alphalens做单因子分析或者用backtrader、zipline做组合回测。二是把因子离散化成信号比如因子值 截面 80 分位数做多 20 分位数做空。常见做法是用alphalens的get_clean_factor_and_forward_returns接口它自动处理截面标准化、分位数分组和收益对齐。注意期货市场和股票市场的处理差异期货要处理主力合约换月因子计算要用复权后的连续合约价格股票要处理停牌和涨跌停因子值在停牌日应设为 NaN 而不是 0。import alphalens as al # factor 是 MultiIndex (date, asset) 的 Series # prices 是 DataFrameindex 为 datecolumns 为 asset factor_data al.utils.get_clean_factor_and_forward_returns( factorfactor_series, pricesprices, quantiles5, periods(1, 5, 10), max_loss0.5 ) ic al.performance.factor_information_coefficient(factor_data) mean_return_by_quantile al.performance.mean_return_by_quantile(factor_data) turnover al.performance.factor_rank_autocorrelation(factor_data)periods(1, 5, 10)表示同时评估 1 日、5 日、10 日持有期的表现。max_loss0.5允许最多 50% 的数据因缺失被丢弃超过就报错防止数据质量问题被掩盖。3.3 股票和期货市场的参数差异股票市场因子评估通常用全市场或指数成分股中性化处理是必须的——市值中性化和行业中性化。期货市场品种少截面宽度不够IC 计算要按品种池分组比如黑色系、化工系、农产品分别算。期货因子更关注期限结构、持仓量变化、波动率遗传规划的终端集要相应调整。维度股票市场期货市场截面宽度3000 只50-80 个品种中性化市值行业板块波动率换月处理无主力合约拼接因子方向量价基本面量价持仓期限结构评估周期日频为主日频/分钟频4. 遗传规划因子挖掘的避坑与排查清单4.1 因子在样本外 IC 暴跌现象训练集 IC 0.08样本外 IC 0.01 甚至为负。原因过拟合。遗传规划搜索空间极大如果适应度函数直接优化全样本 IC它一定会找到噪声。解决用滚动窗口训练每 6 个月重新进化一次适应度函数用 ICIR 而不是 IC 均值parsimony_coefficient调大到 0.003-0.01限制树深度max_depth4或 5。4.2 生成的因子全是常数或近似常数现象est.transform(X)输出的某些列标准差接近 0。原因保护除法div在分母接近 0 时返回 1导致表达式退化成常数或者log、sqrt作用在负数上产生 NaN。解决在特征工程阶段对输入做截断比如close除以close.mean()做归一化自定义log和sqrt的保护版本负数返回 0检查输出因子的nunique()低于 10 的直接丢弃。4.3 因子间相关性过高导致模型不稳定现象20 个因子丢进 LightGBM特征重要性分散样本外表现波动大。原因遗传规划在进化过程中会收敛到相似的表达式结构hall_of_fame里的个体可能高度同质。解决在SymbolicTransformer里设置n_components小于hall_of_fame让框架自己选低相关子集或者手动做贪心去重阈值 0.7还可以在适应度函数里加相关性惩罚项。4.4 回测收益高但实盘亏钱现象回测年化 30%实盘跑三个月亏 5%。原因前视偏差、幸存者偏差、交易成本低估。遗传规划因子如果用了未来数据比如用当日收盘价算的因子在当日开盘交易回测会虚高。解决严格对齐时间戳因子计算用 T 日数据交易用 T1 日开盘价股票池用历史成分股而不是当前成分股换手率高的因子扣 0.2% 单边手续费再评估。4.5 进化过程太慢跑不完现象population_size5000, generations50跑了一整夜没结果。原因gplearn 是单机多线程n_jobs-1用满 CPU 但内存也可能爆。解决先用小样本比如 500 只股票、2 年数据快速试参数确定function_set和parsimony_coefficient后再上全量max_samples0.5减少每次评估的数据量用verbose1观察每代耗时如果某代突然变慢可能是表达式膨胀调大parsimony_coefficient。5. 让遗传规划因子真正可用的两个进阶技巧5.1 用多目标进化控制因子复杂度和相关性gplearn 原生只支持单目标适应度但因子挖掘本质是多目标IC 要高、表达式要短、因子间要低相关。一个实用技巧是把这三个目标加权成一个标量。比如fitness |IC| - 0.001 * expression_length - 0.1 * max_correlation_with_existing。其中expression_length可以从prog.length_拿到max_correlation需要维护一个已选因子池每次评估时计算当前表达式与池中因子的最大 Spearman 相关。class MultiObjectiveFitness: def __init__(self, existing_factorsNone, length_penalty0.001, corr_penalty0.1): self.existing existing_factors self.length_penalty length_penalty self.corr_penalty corr_penalty def __call__(self, y, y_pred, w): ic abs(spearmanr(y, y_pred)[0]) if np.isnan(ic): return 0.0 # 复杂度惩罚用预测值的唯一值数量近似表达式复杂度 complexity len(np.unique(y_pred)) / len(y_pred) penalty self.length_penalty * (1 - complexity) # 相关性惩罚 if self.existing is not None: max_corr max(abs(spearmanr(y_pred, f)[0]) for f in self.existing) penalty self.corr_penalty * max(0, max_corr - 0.5) return ic - penalty这个适应度函数在 IC 相近时偏好输出值分布更分散、与已有因子相关性更低的表达式。length_penalty和corr_penalty需要调一般从 0.001 和 0.1 起步观察进化出的因子平均长度和相关性再调整。5.2 滚动窗口进化与因子衰减监控遗传规划因子会衰减。市场结构变化后三年前有效的因子可能失效。我一般会做两件事一是每季度用最近 3 年数据重新跑一次进化把新因子和旧因子放一起评估保留 ICIR 排名前 80% 的二是监控因子的滚动 60 日 IC如果连续 20 个交易日 IC 为负触发预警。def rolling_ic_monitor(factor_series, forward_returns, window60, threshold-0.02): 滚动 IC 监控返回预警信号 ic_series factor_series.rolling(window).apply( lambda x: spearmanr(x, forward_returns.loc[x.index])[0] ) warning (ic_series threshold).rolling(20).sum() 20 return ic_series, warningwindow60是滚动窗口长度日频因子用 60 个交易日约 3 个月。threshold-0.02是 IC 预警线低于这个值且持续 20 天就认为因子失效。这个监控要放在实盘系统里每天跑不能等回测才发现问题。最后说一个血泪经验遗传规划生成的因子表达式越简单越可靠。我见过 IC 0.12 的复杂因子实盘三个月就归零也见过 IC 0.04 的简单因子(close - vwap) / close跑了两年还有效。进化的时候把parsimony_coefficient调大一点宁可要短表达式不要为了训练集那点 IC 提升引入一堆嵌套。希望帮到你。本文还有配套的精品资源点击获取