
简介一套基于gplearn模型与遗传规划自动生成量化交易因子的项目源码主要面向量化分析师、CTA策略研究者及具备Python基础的金融爱好者帮助解决人工因子挖掘效率低、非线性关系难捕捉等问题。项目完整实现了从原始行情指标构建、因子表达式进化、IC检验到回测评估的闭环流程通过交叉、变异等遗传算子持续迭代因子种群并能自动适应市场数据变化。压缩包共53个文件以16个Python脚本为主配套CSV与Pickle数据文件、PDF思路梳理文档、可视化图表及缓存文件整体约87MB。目前已有236人学习下载。读者可直接运行各功能模块脚本参考遗传算法实证思路梳理并结合数据预处理、因子测试、绘图等工具快速搭建属于自己的CTA因子生成与验证流程。1. 从标题看gplearn怎么用遗传编程把因子“长”出来做量化选股的人都有过这种时刻手工写了十几个价量因子回测看着还行一上实盘就萎想再找新因子却不知道去哪挖。我遇到过不止一次最后被逼得去翻论文、复现别人的因子效率极低。后来我开始用 gplearn 这个 Python 库把遗传算法里的遗传规划GP搬来生成因子——你给它一批基础数据它自己“进化”出一堆表达式每个表达式就是一个因子。这玩意不是玄学是有明确语法和参数可调的工程工具。这篇就讲清楚它怎么工作、最小代码怎么跑通、参数怎么设以及我踩过的那些坑。适合有 Python 基础、做过一点特征工程或量化建模的读者照着调一轮就能用起来。2. gplearn的遗传规划原理为什么值得用来生成因子2.1 遗传算法和遗传规划不是一回事先搞懂GP的搜索空间遗传算法GA和遗传规划GP经常被混着讲但因子生成场景必须分清。遗传算法里的个体通常是一个固定长度的编码向量比如一组权重或阈值而遗传规划里的个体是一棵可变的树树上每个内部节点是算子叶子是输入变量或常数。gplearn 用的是 GP不是 GA这个区别决定了它能做符号回归——直接生成一个数学表达式。举个例子如果你有一个输入特征 xGP 可能进化出x * 2 1也可能进化出log(x) - x**2。这些表达式会作为新因子被用于建模。正因为表达式是文本形式你可以直接看明白它做了什么而不是像神经网络那样是个黑匣子。这一点对量化领域尤其重要因子需要可解释性否则风控都不敢上。gplearn 的核心对象有两个SymbolicRegressor和SymbolicTransformer。前者用来回归目标变量学到的公式可以当因子后者专门用来做特征变换输入多个原始特征输出一个或多个新特征。因子生成场景我一般用SymbolicTransformer因为它不需要一个已知的“目标”而是在数据里找结构化的变换。当然你也可以用SymbolicRegressor直接预测收益但我更推荐先用 Transformer 生成特征再交给下游模型。2.2 个体、种群、适应度GP的三个核心概念先建立几个概念后面调参全要跟它们打交道。GP 里每一棵表达式树是一个个体一组个体构成种群。每个个体好不好由适应度函数打分。在 gplearn 里适应度默认是均方误差或者相关系数你选不同的目标函数进化方向就不一样。进化过程分三步选择、交叉、突变。gplearn 使用锦标赛选择tournament selection从种群里随机抽几个个体挑适应度最好的进入下一代。交叉是随机换两棵树的部分子树突变是随机改某个节点或叶子。这些操作都发生在你限定的函数集function_set和终端集里。函数集是你允许 GP 使用的算子比如add、sub、mul、div、sqrt、log终端集就是输入变量和常数。gplearn 默认的函数集不算太多但够用。这些概念直接决定了你会生成什么因子。比如你把函数集设成只有add和mul那生成因子就只能是线性组合加上sin、log才能出现非线性因子。终端集也一样你只给它成交量它就永远造不出动量因子。参数parsimony_coefficient又叫做复杂度惩罚系数它控制“树越大扣分越狠”这个系数一旦太小GP 会疯狂长出深树出现过拟合。我在刚接触 gplearn 时把 GA 的思路直接套上去结果发现种群和代数设得很大跑了半小时还没收敛。后来才意识到 GP 的搜索空间比 GA 大得多因为每棵树的结构是可变的深度和形状都在变。你必须在运算代价和搜索充分性之间找平衡否则就是白烧 CPU。3. 用gplearn在本地跑通因子生成最小代码与参数说明3.1 准备示例数据先用模拟数据别急着上真实行情开始之前先强调一点不要拿真实行情直接跑除非你已经严格做了时间序列分割否则因子生成会混进未来信息。建议先构造一份模拟数据跑通了再换真实数据。我一般用 pandas 生成一个简单的 DataFrame包含几个价量特征价格、成交量、日内波动率等。数据量不用大200 行足够看效果。GP 是很耗算力的小数据能把迭代调快。import numpy as np import pandas as pd from gplearn.genetic import SymbolicTransformer # 生成模拟数据 t np.arange(0, 200, 1) price 100 0.5 * t np.sin(t / 5) * 2 np.random.normal(0, 1, len(t)) volume np.random.lognormal(mean10, sigma1, sizelen(t)) high price * (1 np.random.rand(len(t)) * 0.02) low price * (1 - np.random.rand(len(t)) * 0.02) # 构造特征矩阵 X pd.DataFrame({ price: price, volume: volume, high: high, low: low, }) print(X.head())这段代码造出了四个基础特征。price有趋势和周期性volume是对数正态分布high和low是price的上下波动。这样构造的好处是特征之间有结构关联GP 更容易进化出有意义的表达式而不是纯发散。注意这里没有做任何特征预处理比如标准化。gplearn 内部对常数和变量的处理不太在乎尺度但如果某些特征数值特别大会影响初始种群生成时的随机常数也可能影响适应度计算。如果真实数据里量级差异很大建议先做一次标准化或取对数。3.2 最小可用的SymbolicTransformer配置下面这组配置是我日常的起步配置不是最优但能保证不出错。# 定义函数集和参数 function_set [add, sub, mul, div, sqrt, log, neg] gp SymbolicTransformer( population_size500, generations10, tournament_size20, function_setfunction_set, parsimony_coefficient0.01, max_samples0.8, metricspearman, random_state42, n_jobs1, feature_nameslist(X.columns), ) # 拟合生成因子 gp.fit(X.values) print(生成的特征数:, gp._n_features) # 实际这里应该看输出后面有正确方式feature_names只是用来输出展示后面的transform才会真正生成新因子。gp.fit(X.values)会通过进化过程学习一组变换函数这些函数会对输入变量做组合运算输出新特征。这里要特别注意metricspearman我故意选它而不是默认的mse因为因子生成阶段我们更关心单调相关性而不是绝对误差。如果你用mseGP 会试图拟合一个“目标”但SymbolicTransformer并没有显式目标它是用一种自监督的方式来构造新特征。跑完之后用gp.transform(X.values)得到的就是新因子矩阵。每一列对应一个进化出的表达式。为了避免误导我直接说说实际输出transform会返回和X行数相同、列数为n_components的数组默认n_components1也就是只有一个新因子。我这边的配置没有设置n_components所以默认生成一个。3.3 看懂生成的因子并把它转成 DataFrame生成因子后直接扔给模型不太直观我先把它转成 DataFrame然后打印表达式看看到底进化出了什么。new_factors gp.transform(X.values) factor_df pd.DataFrame(new_factors, columns[factor_1]) # 打印表达式 for expr in gp._best_programs: print(expr)逻辑说明gp._best_programs是一个列表里面是进化得到的最佳表达式程序。每个对象可以直接打印成文本比如add(mul(price, volume), log(high))。这才是关键——你终于能看到因子长什么样而不是黑匣子。参数说明_best_programs是 gplearn 内部属性不是公共 API但我在多数版本里都这么用。如果你不想依赖这个也可以用gp._programs但那里面是每一代的历史程序信息更多也更乱。表达式一旦能打印出来就可以人工审查。我看到有些入门教程直接把transform的输出喂给线性回归然后说“因子有效”这不对。你应该先看表达式是否合理——比如有没有除以接近零的div操作有没有对负数取sqrt。gplearn 在div和log上有安全保护但结果仍然可能是个极端值。我在第一次跑的时候因子全是10000或-9999这种就是因为div分母极小保护逻辑把它们置成了一个大数。后面我会讲怎么处理。3.4 怎么看生成的因子有没有用勉强跑通之后自然会问这因子能用吗先看分布再看和某个代理目标的相关性。比如我模拟数据里price有明显趋势那因子应该和price有一定相关性。最简单的方法是算factor_df[factor_1]和price的 Spearman 相关。from scipy.stats import spearmanr corr, p_value spearmanr(factor_df[factor_1], X[price]) print(fSpearman 相关系数: {corr:.4f}, p-value: {p_value:.4f})如果相关接近 0说明 GP 进化出的因子没抓到任何结构可能是参数问题也可能是数据太随机。这里metricspearman就会让 GP 在进化过程中优先提高这个相关指标所以理论上相关系数不会太差。如果还是低大概率是代数和种群数不够。还有用n_components可以一次生成多个因子。我一般设成 3 到 5 个这样能拿到一组表达式再做相关性筛选把彼此高度相关的因子剔除掉。4. 参数调优与常见问题避坑让遗传规划不翻车4.1 过拟合因子在训练集惊艳、测试集翻车我在一次真实数据测试里用SymbolicTransformer在训练集上生成了因子回测和预测都很好。拿到验证集一跑IC 直接掉到 0。这个问题太典型了。GP 的搜索空间极大如果种群里个体复杂度不受控它会专门去拟合训练集里的噪声。现象是训练集上因子和目标变量的相关性高达 0.3验证集上只有 0.02。原因是parsimony_coefficient设置太小generations又太长导致树长得很深表达了太多噪声。解决方式有三个第一把parsimony_coefficient从 0.01 调到 0.05 或 0.1让复杂度惩罚更狠。第二降低generations16 代以上在数据量小时非常容易过拟合我一般控制在 6 到 12 代。第三用交叉验证或时间序列分割去评估生成因子而不是只用一份数据。4.2 运行太久、CPU 被吃满遗传规划的算力坑GP 的运算量随几个参数指数上涨。我第一次用population_size5000、generations20、n_jobs-1单机跑了两个小时没跑完。尤其当function_set里有sqrt、log这种计算成本高的函数每个个体都要算几千次。现象是 CPU 全部占满但迭代数走得很慢。原因是 GP 在每一代要评估所有个体的适应度而且树深度变大后求值时间非线性增长。解决方式是控制种群规模和代数不要盲目贪大。我现在的习惯是先用population_size200、generations5跑一遍看是否收敛再逐步加大。另外max_samples0.8表示每个个体只用 80% 的样本来计算适应度能省不少时间。如果机器有多核可以设n_jobs4这能让每个个体的求值分布在多个进程里。4.3 生成的因子全是常数或退化表达式函数集和惩罚系数失衡有一次我跑完transform后打印输出发现因子列全是同一个数比如0.5或-2。这是 gplearn 在保护机制下生成的一个常数表达式相当于什么都没做。现象是因子方差接近 0没有区分度。原因有两个一个是function_set里没有合适算子GP 只能靠常数混日子另一个是parsimony_coefficient太大导致任何非平凡树都被极限惩罚干脆生成一个简单的常数。解决办法是缩小惩罚系数或者扩大函数集。我常用函数集是[add, sub, mul, div, sqrt, log, neg, abs]其中abs能构建一些绝对值类因子。div和log虽然有用但要小心它们容易触发保护逻辑生成极端值。碰到这种情况先打印几代的最优程序看是不是提前陷入了局部最优。4.4 数据泄漏因子生成必须走时间序列分割这是最危险的一个坑不报错但会让你后续所有结果失效。我用真实行情数据做例子如果把未来一段时间的价格或收益信息放进 X 矩阵去训练 GP那么生成的因子天然就带有未来信息回测当然“神一样”。现象是模型在线下回测 IC 极高但模拟盘一上就崩。原因是数据没按时间顺序分割训练集和测试集混在一起GP 直接从测试集上偷学到了结构。解决方式很明确对时间序列数据一定要按时间切分比如前 80% 的数据用来拟合gp.fit(X_train)后 20% 的数据用来gp.transform(X_test)。绝对不能把所有数据拿去fit之后再随机切分。我这边踩过一次之后现在所有因子生成代码都强制带一个cut_idx int(len(X) * 0.8)的前置逻辑。4.5 随机种子不固定结果不可复现gplearn 的进化过程包含大量随机操作比如初始种群、锦标赛选择、变异。如果不设随机种子你会发现自己同一份数据、同一个参数跑出来的因子每次都不一样。现象是同一配置跑两次打印出来的表达式完全不同。原因是 GP 自身就是随机搜索没有固定random_state每次初始化不同。解决方式非常简单在SymbolicTransformer和SymbolicRegressor里设置random_state42或任意固定值。还有如果你用了n_jobs1进程之间的随机状态可能会与单进程不同这一点对我影响不大但如果要严格复现建议用n_jobs1并固定种子。5. 因子验证与迭代从生成到可用的最后一步生成因子只是第一步能不能用于实盘还要过验证这一关。我的习惯是每次生成后都跑三件事第一是用 IC 分析算因子对未来收益的秩相关第二是分层回测把因子分成十组看单调性第三是换一段样本外时间窗口再验证一次。这里的关键是因子必须和你的交易周期匹配。# 用生成的因子做最简单的 IC 检验 import numpy as np from scipy.stats import spearmanr future_return X[price].shift(-5) / X[price] - 1 # 未来5期收益 ic spearmanr(factor_df[factor_1].dropna(), future_return.dropna())[0] print(IC 值:, ic)这段代码用的是模拟数据真实场景里你要换成收盘价序列。IC 值绝对值超过 0.03 才勉强值得关注超过 0.05 才谈得上信号。我做这一步时还会把因子本身做去极值和标准化避免极端值干扰相关计算。经过这一套流程我基本能判断这个方案值不值得投入。gplearn 的定位不是给你一个现成的因子库而是帮你扩展因子搜索空间。当你手工因子挖空时它确实能带来新东西但代价是算力和调参成本。我的个人习惯是永远从最小配置开始先跑通再调大所有参数都留记录。另外强烈建议把进化出的表达式存入文本文件方便以后复盘。希望这篇能帮到你让你少交点遗传规划的学费。本文还有配套的精品资源点击获取