简介在量化投资研究中如何从海量数据中高效挖掘有效因子是核心挑战之一。传统人工挖掘因子依赖经验与试错难以覆盖复杂的非线性关系。遗传规划作为一种进化算法通过模拟自然选择过程将因子构造转化为符号表达式搜索问题自动探索因子表达式空间从而发现可解释、可复用的alpha因子。基于gplearn库可以实现从数据清洗、特征工程、因子生成到回测评估的全流程框架支持股票截面因子与期货时序因子两类场景。该方案以秩相关系数IC作为适应度结合分层回测与可视化分析有效解决了因子挖掘效率低、可解释性差、易过拟合等工程实践问题。本文完整分享从设计思路到代码实现及避坑经验为量化研究者提供一套可直接落地的因子自动挖掘方案。 做量化投资的朋友应该都有类似的感受人工挖因子前期靠经验拍脑袋中期写代码验证后期还要面对因子失效的焦虑。我之前在股票和期货策略上吃过不少亏轮换过各种因子也试过用机器学习模型做端到端预测最后发现最稳妥的做法还是从数据里生成可解释的因子表达式。后来我把目光放到遗传规划算法上基于gplearn搭了一套自动生成因子的完整框架。这个框架涵盖了数据清洗、因子生成、回测评估、可视化分析整个流程支持股票选股因子和期货时序因子两种场景。今天把这套框架从设计思路到代码实现再到踩坑记录完整分享出来。如果你正在为因子挖掘发愁或者想了解遗传规划在量化研究中的落地方式这篇文章应该能提供一份可以直接复用的参考。1. 为什么选择gplearn做因子挖掘1.1 人工挖掘因子的瓶颈在哪里人工挖掘因子这件事本质是在一个巨大的数学表达空间里做搜索。比如经典的动量因子无非就是过去N日收益或者均线比稍微复杂一点的量价因子可能涉及成交量与价格的协变关系但人的思维往往会受到已有知识框架的限制很难跳出几个固定模式。我最早做股票alpha因子的时候先写了几十个技术指标再加估值、情绪等维度每加一个因子都要手工去试。时间一长就发现效率太低不说因子之间的相关性也很高组合起来并没有产生多少增量信息。更麻烦的是当市场风格切换原来有效的因子突然不灵了又要从头开始找新因子。人工挖掘很难系统性地覆盖所有可能性尤其是那些非线性组合比如“成交量变化率除以波动率再乘以价格加速度”这类表达式人在正常情况下几乎不会主动去想。遗传规划算法恰好能解决这个问题它把因子的构造当成一个程序搜索问题用达尔文式的进化过程去自动探索大量表达式保留好的片段淘汰差的片段最终逼近一个相对最优的因子表达式。1.2 gplearn背后的遗传规划原理gplearn是一个基于Python的遗传规划库核心思路是符号回归。它把候选表达式表示成一棵语法树比如表达式(close - ts_mean(close, 20)) / ts_std(close, 20)可以拆成根节点为除法左子树是减法右子树是标准差每个叶子节点是原始数据变量或常量。遗传规划的迭代流程包括选择、交叉、变异三个操作每一代种群中的个体都会被评估适应度适应度高的个体有更大概率被选为父本交叉操作交换两棵子树变异操作随机改变某个节点或子树产生新个体如此反复。gplearn提供了SymbolicRegressor、SymbolicClassifier、SymbolicTransformer三个核心类。前两个做有监督的回归和分类第三个用来做特征构造。我最终采用的是SymbolicTransformer加外部因子评估的组合方式因为因子挖掘本质上不是简单的回归拟合我们更关心因子和未来收益之间的秩相关性也就是IC而不是预测误差最小。这里有个好消息gplearn的SymbolicTransformer可以设置metricspearman直接以因子值与目标值的秩相关作为适应度这正好是量化里最常用的IC度量。所以我不需要自己写复杂的遗传循环只要把特征矩阵和未来收益标签准备好就能让gplearn以最大化IC为方向搜索因子表达式。1.3 为什么不用深度学习或线性回归有一部分朋友可能会问现在深度学习这么流行为什么还用遗传规划我自己的经验是深度学习模型做预测可以很准但模型内部是一个黑盒很难直接解释这个模型在用什么逻辑做决策。量化投资对可解释性要求很高你不知道模型为什么赚钱就很难判断它什么时候会失效更没法做风险控制。遗传规划生成的表达式是明文公式可以直接从数学上分析它代表的市场逻辑比如“它是动量因子还是反转因子”“它对成交量是否敏感”这些在风控和归因中非常有用。线性回归和逻辑回归虽然可解释但表达能力有限只能做加权组合没法自动发现变量之间的复杂交互。遗传规划的表达空间大得多它可以在算子和变量之间自由组合出很多非线性关系。当然遗传规划对算子和终端集的选择非常敏感这也是为什么后面要仔细设计函数集。我自己做过一个简单的横向对比方法可解释性表达能力自动化程度过拟合风险计算成本人工因子挖掘高低低低低线性回归高低中低低深度学习低高高高高遗传规划(gplearn)高高高中中综合下来遗传规划在“可解释”和“表达能力强”之间取得了比较好的平衡。这也是我选择它作为核心引擎的原因。2. 框架的整体架构与关键模块设计2.1 数据模块统一处理股票和期货行情题目里强调了支持股票和期货两个市场这个“统一”其实是最容易踩坑的地方。股票的选股因子往往是横截面性质的同一时间点比较所有股票的特征值然后看未来收益的排名关系。期货的时序因子则更关注单个品种自身的历史规律一个品种过去N日的趋势、波动、量能变化是否能预测未来价格方向。两者样本构造方式不同回测评估方式也不同。我的做法是把两种市场抽象成了统一的“特征面板”概念。股票数据按交易日和股票代码对齐成宽表每只股票一个id每个交易日一个时间戳期货数据则每个品种一个id同样按时间戳对齐。数据清洗时统一处理复权、涨跌停、停牌、缺失值。股票端要考虑分红送转使用前复权价格期货端要注意主连合约的换月跳空问题我通常会对价格做后复权处理或者用收益率序列代替价格序列避免换月带来的虚假信号。所有输入特征都使用历史数据计算严格禁止使用未来信息。框架里写了一个公共函数专门给样本数据打标签比如股票选股用未来5日收益t1到t5的累计收益期货时序用未来1日或5日收益。打标签时每个样本的标签一定是在该样本时间点之后生成的实际操作中我会在日期上做一次shift保证因子值和标签在时间轴上严格错开。2.2 特征工程先构造基础特征再进化gplearn虽然可以自动组合特征但它仍然需要一组基础特征作为“叶子节点”。基础特征的质量直接决定了因子表达式的上限。根据我自己的实践基础特征不建议太多二三十个左右就够了。特征太多会大幅扩大搜索空间导致训练时间成倍增加而且容易过拟合。我一般从五个维度来构建基础特征价格、成交量、波动、流动性、历史收益。举个例子对一只股票或期货品种我会计算过去5日、10日、20日的收益率动量过去5日、10日、20日的平均成交量比值过去10日的波动率收益率标准差最高价与最低价的比值当日收盘价相对过去N日均线的偏离度成交量异常度当日成交量/过去20日平均成交量这些特征本身已经有部分预测能力但单独用效果有限。gplearn要做的是把它们的非线性组合找出来。为了让组合更贴近量化的常用逻辑我还会自定义一些时序算子比如ts_mean滚动均值、ts_std滚动标准差、ts_rank滚动排名、ts_corr滚动相关性、delay历史N日值、delta当前值减历史N日值。这些算子可以直接作为函数集传给gplearn但实际使用中我更推荐把需要滚动窗口的指标都放在特征工程阶段完成因为gplearn的树结构处理滚动窗口的效率不高而且容易在样本外失效。函数集里保留加减乘除、开方、对数、绝对值、最大值最小值这类基础数学算子就够了。2.3 因子生成模块gplearn的两层用法框架里对gplearn的使用分两层。第一层是直接用SymbolicTransformer做特征构造输入基础特征矩阵和未来收益标签设置metricspearman让算法自动生成与未来收益秩相关最高的表达式。SymbolicTransformer本质上会搜索多个program我们可以通过best_programs拿到一组候选因子然后逐一计算它们的历史IC。第二层是外部评估循环把每个生成的因子都送入回测评估模块计算IC、ICIR、分层收益等指标然后根据得分筛选淘汰掉不合格的表达式。这个双层结构既利用了gplearn的进化能力又避免了单纯依赖单一指标导致的偏差。因为gplearn在进化时只看到训练集如果某个表达式在训练集上IC很高但在验证集上失效外部评估循环就会把它筛掉。为了减少过拟合我还会在每次进化后把最高IC的表达式记录下来形成一份“进化日志”内容包括代数、最高IC、表达式复杂度、用到的特征列表。这样不仅能观察模型收敛情况还能在因子失效时回溯当时搜索的历史。2.4 回测评估模块单因子到组合的完整校验回测评估是这套框架里最费心思的部分因为“因子有效”和“因子能赚钱”之间还隔着很远。我把它拆成两个层级单因子评估和组合信号评估。单因子评估主要看IC和ICIR。对股票选股场景每个交易日按因子值对全部股票排序计算因子值与未来收益的Spearman秩相关系数然后对时间序列上的IC取均值和标准差ICIR就是均值除以标准差。一般IC均值绝对值在0.02以上ICIR在0.3以上才值得进一步研究。对期货时序场景IC可以定义为因子值与未来收益的相关系数但信号生成通常直接看因子值的正负方向。组合信号评估则更贴近实盘。股票端我会做分层回测把股票按因子值从大到小分成5层或10层每层等权配置观察分层净值的单调性以及多头层和空头层的对冲收益。期货端我会把因子转成多空信号因子值为正做多为负做空同时扣除手续费和滑点计算净值曲线的夏普比率和最大回撤。这个模块还负责检查因子的换手率如果换手过高实盘的成本会吃掉大部分收益这种因子即使IC很高也不应该入选。2.5 可视化模块让因子表现一目了然可视化对于因子挖掘非常重要因为数字指标容易骗人但图表可以立刻暴露问题。框架里提供了一整套可视化函数用matplotlib和seaborn实现主要包括五类图因子值分布直方图、IC时序曲线、IC滚动均值曲线、分层回测净值曲线、因子相关性热力图。每一类图都有它存在的意义。因子值分布直方图用来检查因子是否有异常尖峰或重尾如果分布严重偏态可能是数据中有极端值需要做缩尾处理。IC时序曲线能看到因子在不同市场环境下的稳定性如果某个时段IC大幅转负说明当时的市场风格可能和因子逻辑相悖。分层净值曲线是判断单调性的最直观手段——5条净值线如果整齐地按层序排列说明因子区分能力强如果交叉混乱说明因子对收益的解释能力不足。因子相关性热力图则用于因子池管理避免选出的一堆因子其实是同一个逻辑的变体。3. 从零到一完整实操演示3.1 环境准备与依赖安装整个框架基于Python 3.8核心依赖包括gplearn、pandas、numpy、scipy、matplotlib、seaborn以及可选的alphalens和plotly。安装很简单pip install gplearn pandas numpy scipy matplotlib seaborn如果要做更复杂的归因分析可以再装alphalens不过alphalens对数据格式要求比较严格我实际使用中发现它更适合股票截面因子期货时序因子用它反而不方便。框架里自研的可视化函数已经覆盖了大部分场景所以alphalens不是必须的。3.2 准备样本数据以期货单品种为例为了演示流程我用一个期货品种的日线数据包括open、high、low、close、volume。数据预处理时先计算基础特征再把特征按时间窗口切分成样本。这里用最简单的实现import pandas as pd import numpy as np # 假设df包含date, open, high, low, close, volume df[return_1] df[close].pct_change() df[return_5] df[close].pct_change(5) df[return_10] df[close].pct_change(10) df[volatility_10] df[return_1].rolling(10).std() df[volume_ratio] df[volume] / df[volume].rolling(20).mean() df[high_low_range] (df[high] - df[low]) / df[close] df[close_to_ma20] df[close] / df[close].rolling(20).mean() - 1 # 未来5日收益作为标签注意shift(-5)代表未来数据 df[label] df[close].shift(-5) / df[close] - 1 # 去掉NaN行 df df.dropna().reset_index(dropTrue) # 特征列和标签列 feature_cols [return_1, return_5, return_10, volatility_10, volume_ratio, high_low_range, close_to_ma20] X df[feature_cols].values y df[label].values这里最需要注意的是标签的shift方向和滚动窗口的边界。pct_change(5)默认用的是过去第5根K线到当前的价格不包含未来信息label的shift(-5)则是在当前时刻记录未来第5根K线收盘价相对于当前的收益率作为训练标签是安全的因为在训练阶段所有样本都已是历史数据预测阶段我们只需要使用因子表达式不再依赖label。3.3 训练遗传规划模型接下来定义函数集并初始化SymbolicTransformer。因为我在特征工程阶段已经把滚动窗口转换成了静态特征函数集里就不需要再放自定义时序算子直接使用gplearn内置的运算符from gplearn.genetic import SymbolicTransformer function_set [add, sub, mul, div, sqrt, log, abs, neg, inv, max, min] gp SymbolicTransformer( generations20, population_size2000, function_setfunction_set, parsimony_coefficient0.01, max_samples0.8, metricspearman, random_state42, n_jobs-1, verbose1, )训练前最好先划分训练集和验证集用时间顺序切分而不是随机打乱split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] gp.fit(X_train, y_train)metricspearman会让gplearn直接优化因子与未来收益的秩相关这比默认的pearson相关更符合量化投资的IC定义因为它对极端值不敏感。训练结束后我们可以从best_programs里取出候选表达式并计算它们在验证集上的样本外ICfor program in gp.best_programs: factor_test program.transform(X_test) if factor_test.ndim 1: factor_test factor_test[:, 0] ic_test pd.Series(factor_test).rank().corr(pd.Series(y_test).rank()) print(f表达式: {program}) print(f样本外IC: {ic_test:.4f})这里有个注意点best_programs是根据训练集表现排序的虽然它们在训练集里IC高但样本外表现需要重新验证。我通常会把样本外IC排名和训练集IC排名结合选择两者都比较靠前的表达式作为最终因子。3.4 因子回测与可视化有了因子值之后接下来做分层回测。为了展示效果我们把验证集部分还原成DataFrame然后按时间分组对因子值分5层计算每层的未来收益均值从而画出分层净值曲线。df_test df.iloc[split:].copy() df_test[factor] factor_test df_test[label] y_test def calc_layer_return(sub_df): sub_df sub_df.dropna(subset[factor]) if len(sub_df) 5: return None try: sub_df[layer] pd.qcut(sub_df[factor], 5, labelsFalse, duplicatesdrop) except ValueError: return None return sub_df.groupby(layer)[label].mean() layer_returns df_test.groupby(date).apply(calc_layer_return)上面这段代码只是为了说明计算逻辑实际项目里我会用更高效的方式处理。分层回测的核心是看层数之间的单调性如果layer 0到layer 4的收益均值大致递增或递减说明因子方向比较稳定如果中间某层突然反转说明因子和收益之间不是线性关系可能需要配合方向判断使用。可视化的部分我用matplotlib画出5层累计净值曲线。每层净值等于该层每日收益均值加1后累乘import matplotlib.pyplot as plt layer_cum (1 layer_returns.fillna(0)).cumprod() layer_cum.plot() plt.xlabel(date) plt.ylabel(cumulative return) plt.title(Layered Net Value) plt.legend([fL{i} for i in range(5)]) plt.show()如果5条曲线从上到下按层序依次排列说明因子选股能力强如果曲线来回交叉说明层次区分度不够。这个图比任何指标都直观我每次筛选因子都会先看这张图再做定量判断。3.5 因子表达式解析与保存训练结束后最重要的一个环节是把选中的因子表达式保存下来。表达式本身是一个字符串比如mul(add(return_5, volatility_10), inv(close_to_ma20))。我会把它保存为文本文件同时把gplearn的program对象用pickle保存方便后续直接transform新数据。import pickle best_program gp.best_programs[0] with open(best_factor.txt, w) as f: f.write(str(best_program)) with open(best_factor.pkl, wb) as f: pickle.dump(best_program, f)还要生成一份因子报告包含表达式、训练集IC、验证集IC、ICIR、分层收益单调性、换手率等。这份报告可以直接导出成CSV方便归档和横向比较多个因子。4. 常见问题与避坑指南4.1 过拟合遗传规划最擅长的事就是“死记硬背”遗传规划搜索空间大如果不加限制很容易生成一个在训练集上完美但在样本外完全失效的因子。我在实际项目中遇到过训练集IC高达0.15样本外IC直接变成-0.03。解决过拟合的方法有几条第一严格时间序列分割用前80%训练、后20%验证不要随机打乱。第二用parsimony_coefficient控制表达式复杂度惩罚过深的树。这个系数越大树越倾向短小但系数太大会导致所有表达式都太简单搜索不足。第三控制代数、种群大小、树深度。不要一上来就设population_size5000、generations50先小规模跑通再逐步放大。第四结合多段样本外验证比如滚动训练快照保证因子在不同时间段都稳定。第五对因子值做行业中性化和市值中性化避免因子只是在捕捉行业或市值偏差。我自己的经验是parsimony_coefficient从0.001到0.1之间多试几个值然后在验证集IC相近时优先选复杂度更低的那一个。4.2 前视偏差最隐蔽的坑很多新手容易在生成因子时不小心用到当天的收盘价去预测当天的收益或者用未来数据进行截断。举个例子如果计算“过去5日收益”时误把未来5日的收益也包含进来IC会虚高得离谱实盘却完全不是那么回事。真正要遵守的原则只有一条因子计算所用数据必须不晚于信号产生时间。统一做法是把所有因子值在时间轴上向后shift一个周期也就是说用t日收盘后能计算出的因子值去预测t1日到tN日的收益。框架里我在数据模块强制开启shift(1)避免偷看未来。4.3 运行效率低怎么调优gplearn参数gplearn默认支持多核并行如果single进程跑速度会很感人。但如果不加节制population_size设成5000、generations设为50训练一个中等规模数据集可能要跑好几个小时。我常用的参数组合是population_size2000generations20max_samples0.8每次抽样80%样本n_jobs-1。这样单次训练能控制在几分钟到十几分钟。如果还觉得慢可以减小函数集和特征数量或者先用较小样本跑通流程再逐步放大。另外function_set里尽量不要放太多函数。函数越多搜索空间呈指数增长。我通常控制在10个以内像sin、cos这种三角函数对金融时序的贡献不大反而会让表达式难以分析不建议常用。log对数函数在遇到接近0的输入时会产生极大值需要配合abs使用。4.4 因子数量膨胀与相似性问题gplearn每次会生成多个最佳程序这些程序可能高度相关。比如一个表达式是return_5/volatility_10另一个是sqrt((return_5/volatility_10)^2)虽然数学上不完全等价但因子排序几乎相同。因此框架在筛选因子时加入了相关性过滤计算所有候选因子之间的Spearman相关性矩阵如果相关系数大于0.5只保留IC更高的那个。这个步骤能有效避免后续组合因子时的冗余。4.5 生成表达式不可解读怎么办虽然遗传规划的表达式是树结构但随着迭代可能变得很长很复杂人很难一眼看出它的市场逻辑。我的经验是解析表达式时不要试图逐项理解而是关注几个关键点哪些基础特征被高频使用、主要符号是加还是乘、是否存在某个条件变量主导。比如如果表达式中出现大量的return_10和volatility_10那它大概率是一个“动量加波动调整”类因子。如果表达式中出现了close/open可能是在捕捉跳空缺口。通过统计每个终端变量的出现频率可以快速归纳因子逻辑。我在框架里加了一个小工具输入一个表达式自动统计每个特征变量出现的次数并高亮频率最高的几个变量。这样哪怕表达式很复杂也能快速抓住它的核心驱动因素。这套框架从雏形到完善我前前后后迭代了几个月最大的感受是遗传规划不是一个“一键生成圣杯因子”的银弹它更像一个高效的候选因子矿工。它能帮你把人工不可能枚举到的表达式空间快速扫一遍但最终是否好用还是要靠严格的回测和逻辑判断去把关。如果你准备在量化研究里引入这套方法我建议先从单一品种、少量特征、较短代数的配置开始等流程跑通后再逐步扩展。另外所有回测都必须建立在严格的历史数据划分上否则再漂亮的结果都是自欺欺人。最后分享一个小技巧把gplearn每次进化后的最高IC、最优表达式都记录下来形成一份“进化日志”后续分析模型收敛情况和因子方向变化会非常有帮助本文还有配套的精品资源点击获取