简介面向量化交易研究者与因子挖掘工程师一份基于gplearn遗传规划自动生成CTA因子的完整Python项目压缩包。核心解决传统因子依赖人工经验、难以捕捉非线性关系的问题通过选择、交叉、变异等遗传算子不断迭代因子表达式输出可用于预测资产价格变动的数学模型兼顾非线性探索与自动化迭代。包体共53个文件含16个Python脚本主程序、数据预处理、IC测试、回测、绘图等、13个编译缓存、5个CSV数据文件及4个pickle序列化结果另有3份PDF文档含遗传算法实证思路梳理、项目说明与若干图表压缩包约87MB目录按data、toolkit、result分层便于对照复现。已有236人学习。借助压缩包内代码可快速上手gplearn因子生成流程掌握遗传规划调参、因子有效性检验与回测评估方法并基于展示的股票因子与择时因子框架扩展自己的策略。1. 让机器自己写因子遗传规划在CTA策略里的落地价值做CTA策略最烦的就是写因子价格、成交量、持仓量、基差翻来覆去就是那几个表达式换参数换到吐换完之后样本外一测直接归零。这也是为什么这几年很多人开始转向遗传规划Genetic Programming自动挖因子。gplearn是Python里最成熟的GP库它让机器自己去组合算子、拼表达式迭代几十代之后给你吐一串数学公式人只需要做筛选和验证。我这次拆的是一个专门用于CTA因子生成的完整项目代码组织结构清晰从数据预处理、GP训练、IC检验到回测一条链路都在里面适合已经写过一段时间因子、想换自动化思路的量化从业者也适合刚入门但熟悉pandas和sklearn的开发者照着跑通全流程。2. 读懂gplearn的代码地图genetic、functions、fitness各管哪一段2.1 从文件结构反推项目设计思路拿到这个项目压缩包别急着跑main.py先把目录结构看一遍。它在根目录下放了main.py、IC_test.csv、IC_train.csv、factor_data.pickle和stock_data.pickletoolkit目录里才是重头戏setupGPlearn.py是GP参数配置入口DataProcess.py负责原始数据加工IC.py和factor_test.py负责因子有效性检验backtest.py做信号回测my_plot.py画IC和净值曲线。还有stock_selection_factor.py和timing_factor.py两个文件分别对应选股型因子和择时型因子的生成逻辑。gplearn目录是库本身的源码副本里面genetic.py、functions.py、fitness.py、_program.py各司其职。这个结构说明项目作者把流程分成了至少四层数据层pickle和DataProcess、训练层setupGPlearn和gplearn核心、检验层IC和factor_test、回测层backtest。这跟我平时搭因子研究框架的习惯一致数据、训练、验证、回测必须解耦不然每次调参都要从头跑数据非常浪费时间。你拿到这个资源后建议直接在这个骨架上面加自己的数据源和算子不要推翻重来。2.2 gplearn的核心运行机制从程序树到适应度评估gplearn里的个体不是一个固定维度的向量而是一棵程序树。_program.py里定义了这棵树的结构树节点是算子函数叶子是特征变量和常量。genetic.py实现了整个进化流程包括种群初始化、锦标赛选择、交叉和变异。functions.py提供了add、sub、mul、div、sqrt、log、abs、neg、max、min等基础算子也可以自己注册自定义算子。这个项目用gplearn生成因子本质就是把这棵树去拟合一个目标比如未来N周期的收益率。适应度函数在fitness.py里定义默认是均方误差但这项目里应该改成了IC相关的度量因为因子生成关心的不是回归精度而是因子值和未来收益的排序相关性。我看了下toolkit里的IC.py它实现的是rank IC也就是spearman相关系数这个比pearson鲁棒不会因为极端值被带偏。跑通这个流程需要理解一个关键点gplearn的fit接口和sklearn一样传入X和y但X不是原始行情数据而是一个已经构造好的特征矩阵y是未来收益标签。项目里factor_data.pickle里存的大概率就是这种已经对齐好的特征和标签。如果你自己造数据X的每一列要是一个基础特征比如动量、波动率、均线乖离等y是未来N期收益N取决于你的持仓周期。from gplearn.genetic import SymbolicRegressor from gplearn.functions import make_function # 自定义一个delta算子计算特征的一阶差分 def _delta(x1, x2): return x1 - x2 delta make_function(function_delta, namedelta, arity2) # 配置GP回归器 gp SymbolicRegressor( population_size2000, # 种群大小越大搜索空间越广但耗时线性增长 generations20, # 进化代数20代是一个性价比比较高的起点 function_set[add, sub, mul, div, sqrt, log, delta], parsimony_coefficient0.01, # 复杂度惩罚系数越大表达式越简洁 metricspearman, # 适应度用spearman兼容IC逻辑 tournament_size20, # 锦标赛选择规模越大选择压力越大 random_state42, # 固定种子保证实验可复现 n_jobs-1 # 并行核数-1表示用全部核 ) gp.fit(X_train, y_train) print(gp._program)这段代码里需要注意几个参数的联动关系。function_set里我加了delta用来构造差分特征但它不是很常用更多是用div和sqrt来构造比值类和开方类算子。parsimony_coefficient这个参数很容易被忽略它直接控制表达式复杂度系数太小会得到一串上百个节点的树看似IC很高实则在样本外一塌糊涂。我一般先用0.01跑一轮看程序树深度再决定要不要往0.05调。metricspearman是这里比较关键的选择它让gplearn直接优化因子排序能力而不是预测误差。如果用的是默认mse生成的因子可能更像是回归模型而非因子表达式。3. 数据预处理从原始行情到可喂给gplearn的特征矩阵3.1 DataProcess.py做了什么main.py跑之前先看DataProcess.py因为因子研究里数据切分和特征构造决定了GP能搜到什么表达式。DataProcess.py的核心功能是把原始行情表转成gplearn需要的格式步骤基本是去极值、缺失值填充、构造基础特征、按时间序列切分训练集和测试集。stock_data.pickle应该就是处理完之后的全量数据包含日期、品种代码、基础特征和未来收益标签。这里有个容易被忽视的坑切分不能随机切必须按时间切。项目里IC_train.csv和IC_test.csv两个文件就是为了保证训练和测试在时间上是严格先后关系的。随机切分在时序数据里一定引入前视偏差因为训练集里有未来信息。所以我拿到任何因子项目第一件事就是看它的切分逻辑是不是按日期排序后切而不是用train_test_split。import pandas as pd import numpy as np # 加载原始数据这里假设stock_data.pickle里是一张明细表 df pd.read_pickle(stock_data.pickle) # 按时间排序防止数据乱序 df.sort_values([datetime, symbol], inplaceTrue) # 构造基础特征过去20日动量、波动率、均线偏离度 df[mom_20] df[close] / df[close].shift(20) - 1 df[vol_20] df[return].rolling(20).std() df[ma_bias] df[close] / df[close].rolling(20).mean() - 1 # 构造未来收益标签这里用未来5日收益作为y df[y] df[close].shift(-5) / df[close] - 1 # 剔除NaN防止GP训练时遇到缺失值直接崩 df.dropna(inplaceTrue) # 按时间切分前80%训练后20%测试 split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx] test_df df.iloc[split_idx:] # 特征列和标签列分离 feature_cols [mom_20, vol_20, ma_bias] X_train train_df[feature_cols].values y_train train_df[y].values X_test test_df[feature_cols].values y_test test_df[y].values这段代码里最需要注意的是shift的方向。计算mom_20的时候shift(20)是往前取数据因为用的是历史数据没问题。但y的构造是shift(-5)这引入了未来5日的信息作为标签这是合理的因为监督学习本来就需要未来信息做标签。最大的坑在于dropna之前必须确保shift产生的NaN是在序列首尾如果数据里有停牌跳空或者复权异常NaN会出现在中间位置dropna会删掉中间样本直接破坏时间顺序。我一般会在dropna之前检查NaN的位置分布确认只在首尾才继续。3.2 特征池怎么选才不浪费GP的搜索能力特征池是GP搜索的基础池子里的特征如果全是同类型的那搜出来的表达式多样性会很差。这个项目里没有明确列出特征池文件但看stock_data.pickle的字段至少包含了价格、成交量、持仓量、均线相关的特征。CTA因子常用的特征池我一般是分五类价格类收盘价、开盘价、最高最低价的比值关系动量类不同周期收益率比如1日、5日、20日、60日动量波动类ATR、标准差、振幅、已实现波动率量仓类成交量变化率、持仓量变化率、量仓比结构类均线乖离、布林带位置、RSI特征不是越多越好。gplearn的搜索空间随特征数量指数增长特征太多会导致几十代都搜不到有效表达式。我的经验是控制在10到15个基础特征以内而且特征之间要尽量不相关。比如mom_5和mom_20相关性通常比较高留一个就行。可以先算一遍相关性矩阵把相关系数超过0.7的特征删掉一批再进GP。还有一点特征的标准差不能太大否则div算子很容易产生数值溢出或除以接近0的异常值。我一般在构造特征之后做一个z-score标准化但这只对无截距的表达式有效。如果你希望GP搜出来的表达式是带有原始量纲的那标准化就别做这会影响表达式的可解释性。这个项目在DataProcess.py里做了标准化处理然后用IC去评估因子说明它更关注排序关系而非绝对值。4. 核心流程实战从setupGPlearn.py到因子落地4.1 三个脚本的分工逻辑toolkit目录里setupGPlearn.py、stock_selection_factor.py、timing_factor.py三个文件是三个递进层次。setupGPlearn.py是公共配置定义了GP的所有超参数和公共函数stock_selection_factor.py是横截面因子生成也就是在同一时间截面上比较不同品种的特征找出预测截面收益差异的表达式timing_factor.py是时序因子生成针对单一品种的历史序列寻找择时信号表达式。这个分层是CTA策略里很常见的设计选股因子管买什么择时因子管什么时候买。横截面因子和时序因子在gplearn里的差别体现在X和y的构造方式上。横截面因子里每个样本是一个品种在某一天的快照X是品种特征y是品种未来收益时序因子里每个样本是单一品种在某一天的快照X是历史特征窗口y是该品种未来收益。这个项目把两个方向拆开写说明作者希望用户能清晰区分两种因子逻辑不要混在一起。# setupGPlearn.py 里的公共配置示例 import numpy as np from gplearn.genetic import SymbolicRegressor from gplearn.functions import make_function # 统一的随机种子和并行配置保证可复现 SEED 42 N_JOBS 8 def get_gp_instance(population_size3000, n_generations15, max_depth6): gp SymbolicRegressor( population_sizepopulation_size, generationsn_generations, tournament_size20, max_samples0.9, # 每代只抽90%样本评估增加随机性 max_depthmax_depth, # 限制树深度防止表达式无脑膨胀 function_set[add, sub, mul, div, sqrt, log, abs, neg], parsimony_coefficient0.01, metricspearman, random_stateSEED, n_jobsN_JOBS ) return gpmax_samples这个参数值得单独说一下。默认是1.0也就是每代全量样本参与适应度评估但这样做有两个问题一是计算量大二是会过早收敛到局部最优。设置成0.9意味着每代随机抽取90%的样本来评估这就像随机森林里的bootstrap增加了种群多样性。但这个值不宜太低低于0.8会导致适应度评估噪声太大算出的IC不可信。4.2 用gplearn生成因子后的表达式解析训练完成后gplearn会把最优个体保存在gp._program里。直接print的话会输出一长串类似add(mul(X0, X1), div(X2, X3))的嵌套表达式。这个表达式就是因子你可以把它写成函数在任意时间点计算因子值。项目里factor_test.py应该做的就是这件事它把GP输出的程序表达式解析成可批量调用的函数然后在全样本上计算因子序列。# 把gp._program转成可计算公式并计算因子值 from gplearn.genetic import SymbolicRegressor def program_to_function(program, feature_names): 把gplearn程序对象转成lambda函数feature_names要和训练时X的列顺序一致 # 直接调用program对象的execute方法传入特征矩阵 def factor_func(X): return program.execute(X) return factor_func # 在测试集上计算因子值 factor_func program_to_function(gp._program, feature_cols) test_factor factor_func(X_test) # 检查因子值的分布看是否有异常常数或极端值 print(f因子均值: {np.mean(test_factor):.4f}) print(f因子标准差: {np.std(test_factor):.4f}) print(f因子IC: {spearmanr(test_factor, y_test).statistic:.4f})程序对象直接execute的时候X必须是二维数组列顺序要和训练时完全一致。这是最容易翻车的地方训练X和测试X是不同批次构造的列顺序没对齐的话GP表达式里的X0、X1指向的就是完全不同的特征算出来的因子值没有任何意义。所以我通常会把特征列名和顺序保存成一个json文件之后每次加载数据都要校验列顺序一致再计算。还有一个细节gplearn的program输出可能会包含log和div导致的NaN值比如log(负数)或者除以零。在因子计算时需要处理这种情况否则IC计算会报错或者得到错误结果。项目里factor_test.py大概率有异常值处理逻辑自己写的代码里也要注意在execute之后加一步np.nan_to_num或者直接把非有限值替换成该列中位数。4.3 训练流程的参数调优方向跑通一遍之后实际工作中最花时间的是调参。gplearn主要参数有population_size、generations、tournament_size、max_depth、parsimony_coefficient和function_set它们的优先级并不一样。population_size和generations决定搜索广度但收益递减明显。2000到3000的种群、20代左右已经能搜到不少合格因子继续加大只会让训练时间成倍上升。tournament_size控制选择压力20是比较常见的值越大越容易早熟收敛越小越随机。max_depth控制表达式复杂度6到8层是合理区间太深了过拟合风险很高虽然训练集IC好看但测试集IC直接掉没。parsimony_coefficient和max_depth是两把剪刀前者在适应度上加复杂度惩罚后者硬性限制树深度我一般先固定max_depth再微调parsimony_coefficient。function_set的选择影响最大。默认的add、sub、mul、div四个算子只能构造线性表达式要捕捉非线性关系必须加sqrt、log这类非线性算子。但算子越多搜索空间越大常见误区是把所有算子都丢进去结果生成一堆套着log和sqrt的复杂嵌套解释性极差。我建议第一轮只用add、sub、mul、div、sqrt等确认流程没问题了再加其他算子。5. 实战避坑gplearn生成因子的五个常见踩坑记录5.1 训练到一半内存爆掉进程直接被杀现象population_size设到5000之后训练跑到第10代左右内存占用飙升最后进程被OOM Killer杀掉一点日志都没留下。原因gplearn的种群是多个程序树同时驻留内存的每棵树都有独立的节点对象。种群越大、树越深、功能算子越多内存增长越明显。还有个隐性原因如果函数算子里面有梯度类操作比如自定义算子内部产生了临时大数据内存会成倍放大。解决先降population_size到2000max_depth限制到5跑通之后再逐步加大。另外检查是不是开了n_jobs-1在数据量大的机器上多进程会把内存翻倍。我一般用n_jobs4配合3000的种群内存控制在8GB以内。5.2 IC很高但回测曲线一路向下现象生成的因子在训练集上IC有0.06测试集也有0.03看起来不错结果扔进backtest.py回测净值曲线几乎是直线下跌找不到任何趋势性。原因这是典型的因子和价格序列之间存在前视偏差。最常见的情况是标签构造用了shift(-N)但特征里不小心包含了未来N期的数据比如用当天的收盘价去预测当天收盘价的未来收益特征本身和标签是同源的。另外一个常见原因是使用了全局标准化参数标准化均值方差包含了未来统计量。解决检查特征列里有没有用了未来数据的列最简单的方法是随机打乱标签再算一遍IC如果打乱后IC仍然显著为正说明数据存在泄漏。另外把特征构造和标签构造全部移到同一段代码里用shift的方向一致性来约束。从那以后我每次构造标签之后都会做一步泄漏检测然后再进GP。5.3 生成表达式复杂到完全没法解释现象训练出来的最优程序有七八十层嵌套里面重复了十几个相同的子表达式看起来像是把整个特征池都用了一遍。原因parsimony_coefficient设成了0或者max_depth设置过大。gplearn默认是0.01但如果手动改成0适应度函数里就没有复杂度惩罚项程序树就会无脑膨胀搜出来的表达式虽然训练集IC很高但基本是过拟合产物。解决固定max_depth6把parsimony_coefficient调到0.02以上再跑一轮。如果表达式还是太长可以再加一层人工约束把最终因子表达式中出现次数最少的特征删掉重训这样可以有效降低冗余度。5.4 换到新数据集后同一套参数完全失效现象项目自带的pickle数据里跑得好好的参数换到自己的期货数据上IC从0.03变成0.001甚至变负。原因每个数据集的统计特性不一样。项目自带的数据可能是日线级别的股指数据切到分钟级别或者商品数据后噪声结构完全不同。gplearn的参数像parsimony_coefficient和max_depth都有数据适应性不能跨市场通用。解决换数据集后先做一个基线测试用最简单的线性因子算出IC中位数和标准差如果线性因子的IC中位数就是0.005那GP搜出来的因子再怎么优化也很难超过这个数量级说明数据本身可预测性很低。此时应该回头检查特征构造而不是继续调GP参数。5.5 多次运行结果差异巨大现象随机种子固定了但每次跑完最优表达式都不一样IC范围在0.01到0.05之间大幅波动。原因random_state固定只能保证同一份数据和同一套参数下结果可复现但是没法保证收敛到同一最优解。遗传算法本身有随机性初始种群不同会导致最终结果落在不同的局部最优附近。解决不要只跑一轮就定因子用同一个配置跑10次每次保存最优表达式然后用独立的样本外数据去做IC检验IC最稳定那个才进入因子库。我一般会维护一个因子池每轮训练产生的因子先放进池子里观察两周能和已有因子相关性低于0.3的才保留。6. 验证与进阶IC曲线怎么看因子落地还要走哪两步因子从gplearn出来只是万里长征第一步后面至少还有两步独立验证和组合评估。项目里的IC.py、my_plot.py和backtest.py分别对应这两个环节。IC.py输出IC_train.csv和IC_test.csvmy_plot.py画IC曲线backtest.py做信号回测。IC曲线是因子有效性最直观的表达。我的习惯是画两条线一条是全样本的滚动20日IC均值曲线另一条是累计IC曲线。滚动IC曲线如果大部分时间在0轴上方说明这个因子在不同市场环境下都有预测力如果曲线频繁穿越0轴正负不明说明因子只在特定市况下有效。累计IC曲线则能看出因子的稳定性斜率高且不回撤的因子更值得信任。组合评估这一步我通常会拿生成因子和已有因子库里的代表因子做相关性矩阵如果新的因子和某个已有因子相关性超过0.6说明它并没有提供增量信息留着只会增加组合过拟合风险。只有在相关性低且测试集IC显著的情况下才纳入因子库。回测时还要注意手续费和滑点的敏感性测试CTA因子交易频率越高对成本越敏感一个因子在0.01%手续费下盈利但在0.03%下亏损这个因子就得放弃。# 按顺序跑通全流程的参考命令 python DataProcess.py # 1. 数据处理生成stock_data.pickle python setupGPlearn.py # 2. GP配置加载 python stock_selection_factor.py # 3. 生成选股因子表达式 python timing_factor.py # 4. 生成择时因子表达式 python factor_test.py # 5. 在测试集上计算因子值 python IC.py # 6. 计算IC并输出csv python my_plot.py # 7. 画IC曲线和因子净值曲线 python backtest.py # 8. 基于因子构建策略回测这套流程里最容易被跳过的是第二步加载配置很多人喜欢在训练脚本里直接硬编码所有参数结果换一个数据集就要改一坨代码。setupGPlearn.py把参数集中管理这个习惯值得保留。从那以后我每次新建因子研究项目都先建一个config.py把数据路径、参数、种子全部放进去训练脚本不做任何硬编码。这算是我从拆这个项目里学到的一个好习惯也希望这个流程对你有帮助。本文还有配套的精品资源点击获取