
简介基于gplearn遗传规划算法构建的量化投资因子生成完整框架面向量化研究者与Python中级开发者解决传统因子设计依赖专家经验、主观性强且难以覆盖非线性规律的问题。算法可自动演化时序因子与选股因子并配套因子生成、回测评估和可视化分析全流程同时兼容股票与期货市场适合用于CTA策略及多因子选股研究。压缩包共54个文件、87.02MB主体为16个Python脚本及13个pyc编译文件覆盖主程序、数据预处理、因子测试、IC分析、回测与绘图等模块另含5份CSV与4份pickle数据文件、3幅因子回测结果图、3份PDF说明及遗传算法实证思路梳理文档目录结构清晰便于按模块调用。已有46人学习。1. 从手写因子到 gplearn 遗传规划为什么我更愿意让程序自己找答案在量化研究里手写因子公式是性价比极低的一环gplearn 遗传规划算法要解决的正是把“找因子”这件事自动化。你翻来覆去能想到的动量、波动率、量价背离市面上早有一堆人在用了剩下的全是排列组合式的搜索。我第一次跑这个方向时以为丢进去一个 AutoML 睡一觉就能拿到新因子结果出来的表达式长得吓人回测曲线像过山车。后来才意识到问题不在算法本身而在于因子生成、回测评估和可视化分析没有在一个闭环里对齐。这个项目标题描述的正是那个闭环用 gplearn 自动生成时序因子和选股因子覆盖回测评估与可视化同时支持股票和期货市场。你需要的其实是一个量化投资助手式的框架而不是只盯着 IC 数字反复调参。下面我把原理、实现、回测、避坑到进阶按自己踩过的路子拆开讲。2. 遗传规划因子生成的原理与选型GP 是怎样“长”出因子的2.1 为什么是 gplearn而不是 AutoML 或神经网络做因子生成先得想清楚选型。AutoML 类库优化的是模型超参和 pipeline对“特征表达式”本身无能为力神经网络确实能拟合非常复杂的非线性关系但产出是黑匣子没法转成可读、可审计、可落库的因子字段。gplearn 属于遗传规划Genetic Programming它直接在表达式树上做进化最后给你的是能看懂的符号表达式比如(close / open - 1) * log(volume)这种东西。有了表达式你可以写入行情库、转成 C/Rust 部署到实盘信号里也可以随手丢给同事 review这是神经网络给不了的。gplearn 在因子生成场景里的定位很明确它不做模型预测只做特征工程。它内置三个核心类SymbolicRegressor 面向连续目标、SymbolicClassifier 面向分类目标、SymbolicTransformer 用来生成新的数值特征。我做因子时用得最多的是 SymbolicTransformer因为它对 y 的要求最宽松只需要一个数值向量进化目标就是让新特征与 y 的相关性尽量高这天然贴合多因子研究里“找和未来收益有相关性的特征”这件事。选择 gplearn 还有一个实际原因它能直接控制表达式复杂度。手写因子时你很难系统性地控制“这个因子是不是太复杂了”GP 里的 parsimony_coefficient 参数就是对复杂度的正则项每多一个节点就扣一点适应度。这比一个人拍脑袋写公式再人工判断要可重复得多。2.2 GP 的进化循环初始化、适应度、选择与三个遗传算子理解 gplearn 之前先看遗传规划的基本循环因为所有参数都围绕这五步展开。第一步是初始化种群gplearn 默认用 ramped half-and-half 方式生成初始程序树一半树深较浅、一半树深随机确保初始种群里有足够的多样性。第二步是评估适应度每个个体在训练数据上算一次 metric 值SymbolicTransformer 默认用 Pearson 相关系数也可以指定 spearman。第三步是选择gplearn 用锦标赛选择也就是从种群里随机抽 tournament_size 个个体让适应度最好的那个进入下一代。第四步是遗传操作。gplearn 支持三种交叉crossover交换两棵树的子树变异mutation随机替换某个子树还有 subtree replacement 直接替换整棵子树。交叉是搜索的主力变异负责跳出局部最优。第五步是重复上述过程直到跑满 generations。整个流程就是这样一层层迭代每代保留 hall_of_fame 个精英个体防止好解丢失。这里有个容易被忽略的细节gplearn 的变异概率和交叉概率不是直接暴露的参数它内部维护了一套概率分配默认交叉 0.9、变异 0.1。你不需要每次去调这两个值真正要调的是种群规模、进化代数和复杂度惩罚系数。种群太小搜不全表达式空间种群太大每代评估成本指数级上涨。我一般先跑一个 1000 种群、5 代的小实验看表达式长什么样再决定要不要加码到 5000 甚至 8000。2.3 时序因子与选股因子同一个引擎差别全在特征矩阵很多人第一次拿到这个项目标题会问既然 gplearn 只是一个符号回归工具它怎么区分时序因子和选股因子答案可能会让你意外GP 本身完全不感知时间序列和横截面的区别它只认输入矩阵 X 和输出向量 y。所谓时序因子和选股因子差别全在你喂给它的特征矩阵怎么组织。做时序因子时X 的行是时间点列是同一个标的过去若干期的滚动统计量比如 5 日动量、20 日波动率、量比、最高价位置。GP 在这些列上做代数运算产出的表达式天然就是“对历史窗口做变换”的时序指标。做选股因子时X 的行变成“日期-股票”的二元组列是各个股票在同一时刻的横截面特征比如 PE、PB、过去 20 日收益、分析师预期调整。同一棵表达式树会在所有股票上同时计算输出一个横截面排序分数这就是选股因子。理解了这一点你在阅读这个项目的代码时就不会困惑真正的核心不是遗传规划算法本身而是数据准备和特征工程。很多人在这一步翻车比如把所有股票数据堆在一起却忘了按股票分组做 rolling导致时序特征跨股票串数据。另一个常见误区是想让 GP 自己“发现”窗口长度但 gplearn 的叶子节点只接受你提供的列它不会内生地生成close.pct_change(10)这种带窗口偏移的节点。所以你需要提前准备好多个窗口的特征列GP 才能组合出有意义的时序表达式。3. 因子生成实现从原始行情到一组可复用因子表达式3.1 先造特征矩阵把行情变成 GP 能下嘴的原料动手写 GP 之前第一步是做特征矩阵。我一般从日线 CSV 读入数据列名至少包含 ts_code、trade_date、open、high、low、close、vol。这里的关键是排序必须先按股票代码再按交易日期排序否则后面 rolling 会乱。import pandas as pd import numpy as np df pd.read_csv(daily.csv, parse_dates[trade_date]) df df.sort_values([ts_code, trade_date]).reset_index(dropTrue) # 构造 GP 的叶子变量不同窗口的基础特征 df[ret_1] df[close].pct_change() df[ret_5] df[close].pct_change(5) df[vol_ratio] df[vol] / df[vol].rolling(20).mean() df[max_high] df[high].rolling(20).max() df[std_20] df[close].pct_change().rolling(20).std()这段代码的逻辑很直白把行情转换成一组有业务含义的数值列这些列会作为 GP 表达式树的叶子节点。注意pct_change(5)会产生前 5 个 NaN后面 fit 之前要统一 dropna。我的习惯是先把特征列和标签列拼到一个 DataFrame 里统一 dropna再拆成 X 和 y避免 gplearn 在 fit 时因为 NaN 直接报错。参数上窗口大小我一般取 5、10、20、60 四档覆盖短中长周期。不要贪多30 个特征列会让搜索空间变得很大GP 在有限代数里很难搜出干净表达式。另外所有特征计算只能用当根 K 线及之前的数据这是底线后面避坑章节会专门讲。3.2 最小可用代码用 SymbolicTransformer 跑出第一组因子特征矩阵就绪后核心训练代码很短。SymbolicTransformer 的 fit 需要 X 和 yy 在这里是未来 5 日收益GP 会在进化过程中寻找与 y 相关性最高的特征表达式。from gplearn.genetic import SymbolicTransformer feature_cols [ret_1, ret_5, vol_ratio, max_high, std_20, close, vol] # 标签未来 5 日收益。注意 shift 方向防止未来函数 df[y_future_5] df[close].shift(-5) / df[close] - 1 model_data df.dropna(subsetfeature_cols [y_future_5]).reset_index(dropTrue) X model_data[feature_cols] y model_data[y_future_5] gp SymbolicTransformer( generations6, population_size4000, hall_of_fame50, n_components10, function_set[add, sub, mul, div, sqrt, log, max, min], parsimony_coefficient0.01, metricspearman, random_state42, n_jobs4, verbose1, ) gp.fit(X, y) # 生成新因子并拼回 DataFrame gp_features gp.transform(X) for i in range(gp.n_components): model_data[fgp_{i}] gp_features[:, i]这里几个参数要单独解释。metricspearman是我强烈建议的选项因为多因子研究里我们更关心因子值和未来收益的排序关系而不是线性相关Spearman 秩相关正好对应 IC 的计算方式。parsimony_coefficient0.01表示每多一个表达式节点就扣减 0.01 的适应度值越大表达式越短。function_set我刻意没有放abs和neg这类算子因为max/min和mul已经能表达大部分逻辑算子越少搜索空间越小。逻辑说明SymbolicTransformer 的 fit 过程不是在做回归预测它是在为 X 寻找一个非线性变换使得变换后的向量与 y 的秩相关最大。所以训练完成后我们不是拿它来预测而是拿 transform 出来的新列当因子。正因为这一点y 的绝对大小不重要重要的是排序这也解释了为什么 5 日收益比 20 日收益更常被用来做短周期因子训练。3.3 把表达式落地从 gplearn 程序对象到可复用因子函数训练完只是第一步真正要让因子进入投研流程你得把生成它的表达式导出来。gplearn 的每个程序对象可以直接转字符串我一般把它打印出来看结构再决定要不要用 sympy 化简。# 导出最优的 n_components 个表达式 for i, prog in enumerate(gp._best_programs): print(fgp_{i}: {prog})如果版本提示没有_best_programs属性换成gp.programs即可两种写法在不同版本里都存在过。打印出来你会看到类似mul(ret_1, div(max_high, close))这样的嵌套结构这就是可以保存的因子表达式。为了让它更方便地用于后续回测我一般会做一层符号化简把 GP 生成的冗余结构精简掉import sympy as sp x1, x2, x3, x4, x5, x6, x7 sp.symbols( ret_1 ret_5 vol_ratio max_high std_20 close vol ) expr sp.sympify(str(gp._best_programs[0])) print(sp.simplify(expr))这里有前提gplearn 的变量名必须能被 sympy 正确解析所以我建特征列时故意用简单的下划线命名避免空格和特殊符号。化简后的表达式可以用来生成 Python 函数也可以落库保存。实际操作中我建议把最终选中的因子表达式连同参数一起存成 JSON方便后续做版本对比和回归测试。3.4 参数怎么调一张表讲清 8 个关键参数gplearn 的参数数量不多但每个都直接影响搜索结果。这里给一张我常用的参数表按推荐程度排序参数作用我的常用配置说明population_size每代种群个体数2000-8000样本小时 1000 也能跑但搜索空间覆盖差generations进化迭代代数5-10别一上来就 50容易把时间耗在过拟合上n_components最终保留的因子数量10-30每个因子是一个独立表达式parsimony_coefficient表达式复杂度惩罚0.001-0.01调大表达式变短调小容易得到复杂到没法用的公式function_set算子集合add/sub/mul/div/sqrt/log/max/min不要全放算子越多搜起来越慢metric适应度函数spearman因子研究首选其次考虑 msetournament_size锦标赛选择的参赛人数20默认值就够不用频繁改random_state随机种子固定不是为了复现结果是为了排查问题时可重复参数调整有一条稳定路径先用小种群、少代数、大复杂度惩罚跑通全流程确认因子表达式能正常生成并落到回测再逐步加大种群和代数观察表达式长度和适应性指标的边际变化。不要一开始就追求“最好结果”gplearn 的搜索是随机的第一次能跑通比跑出漂亮因子重要得多。4. 回测评估从因子 IC 到 backtrader 多股回测的完整链路4.1 先算因子指标IC、RankIC、IR 与分组收益生成因子之后先别急着上回测框架应该先算因子有效性指标这一步成本最低、排查问题最快。IC 是因子值与未来收益的相关系数RankIC 是秩相关系数IR 是 IC 均值除以 IC 标准差。计算方式很直接# 假设 model_data 里已有因子列 gp_0 和未来收益 y_future_5 ic_series ( model_data.groupby(trade_date) .apply(lambda g: g[gp_0].corr(g[y_future_5], methodspearman)) ) ic_mean ic_series.mean() ic_std ic_series.std() ir ic_mean / ic_std print(fRankIC mean: {ic_mean:.4f}, std: {ic_std:.4f}, IR: {ir:.4f})这段代码按 trade_date 分组每天算一次因子值和未来收益的截面秩相关得到一条逐日 IC 序列。RankIC 均值代表因子整体预测能力绝对值超过 0.03 就算初步可用IR 代表稳定性IR 大于 0.5 说明因子在多数时间有效不是靠某几天的极端行情撑着。只看一个指标容易误判比如 IC 均值是正的但标准差很大这种因子实盘会非常磨人。除了 IC分组收益是更直观的检验。把因子值按每日截面排序分成 5 组或 10 组计算每组未来 5 日的等权收益看多头组和空头组之间有没有单调性。如果分组收益完全无序那 IC 再高也要怀疑是计算错位或者极端值干扰。这一步做完因子单看没问题再进回测。4.2 用 backtrader 做多股因子回测最小可运行代码因子验证通过之后我用 backtrader 做多股回测验证交易层面的表现。这里要特别说明backtrader 本身只负责撮合和资金管理因子值需要预先计算好并作为数据线传入策略。下面是一个最小可运行的多股因子调仓策略按因子值选前 N 只股票等权持有每 5 个交易日调仓一次。import backtrader as bt class TopNFactorStrategy(bt.Strategy): params dict( rebalance_days5, top_n5, ) def __init__(self): self.rebalance_counter 0 def next(self): self.rebalance_counter 1 if self.rebalance_counter % self.params.rebalance_days ! 1: return # 按当日因子值排序取前 top_n ranked [] for data in self.datas: factor_value data.factor[0] # 因子值通过 addfield 扩展传入 if not math.isnan(factor_value): ranked.append((factor_value, data)) ranked.sort(keylambda x: x[0], reverseTrue) selected {d for _, d in ranked[: self.params.top_n]} # 对每只股票设置目标仓位选中 1/top_n未选中 0 for data in self.datas: target 1.0 / self.params.top_n if data in selected else 0.0 self.order_target_percent(datadata, targettarget)这段策略的回测逻辑是典型的横截面因子回测。data.factor不是 backtrader 内置字段需要你在创建数据源时用addfield扩展把因子值序列绑定到每个股票的数据线上。order_target_percent会自动根据当前市值和目标权重下单调整回测更贴近真实交易。调仓频率设 5 天是短周期因子常见配置如果因子是月频调仓逻辑改成 20 天即可。跑这个回测前一定要先确认因子值在数据源里是按日期对齐的。最常见的错误是把因子值序列错位了一天导致回测结果虚高。我的排查办法是随机抽 3 天打印因子值和实际收盘价人工核对日期是否对齐。这一点虽然原始但比任何高级调试手段都管用。4.3 期货市场适配主力连续、跳空与低样本量标题里明确支持期货市场但期货回测和股票回测有本质差异。先说数据期货不能用单个合约直接回测因为合约会到期需要使用主力连续合约或者按换月规则拼接。拼接方式直接影响因子计算如果用后复权价格需要注意跳空被抹平的位置如果直接用原始价格拼接换月当天的巨大跳空会被因子误判为极端行情。我的习惯是把主力合约切换日单独标记出来在计算滚动特征时剔除换月日前后各一根 K 线或者用换月调整因子对价格序列做复权。第二是期货横截面样本少常见品种只有 30 到 50 个按日算截面 IC 的样本量不够稳定所以期货因子我更习惯做单品种时序 IC 检验也就是在同一个品种上比较因子值和未来收益的时序相关性。第三是交易制度期货有保证金和杠杆order_target_percent的语义在期货上要重新理解不能简单按股票等权逻辑处理需要把合约乘数和保证金比例纳入仓位计算。这三点不是回测代码本身能解决的需要在数据准备阶段就处理干净。否则你看到的回测收益曲线再漂亮放到仿真环境里也会因为换月跳空和保证金计算细节直接翻车。5. 避坑遗传规划因子项目最常翻车的 5 个点5.1 因子计算错位导致前视偏差现象因子回测 IC 奇高RankIC 均值能到 0.08 以上但实盘模拟盘连续亏损曲线和回测完全脱节。原因这是最典型的未来函数问题。特征计算时用了未来数据或者标签和特征没有对齐。比如有人写df[ret_5] df[close].pct_change(-5)这是拿未来 5 天数据算当前因子回测当然“精准预测”未来。解决所有特征列和标签列在构造时必须明确时间方向。特征是过去到当下的信息用pct_change(5)和rolling(20)标签是未来收益用shift(-5)。构造完以后打印一列数据核对最近几行的值是否合理比如最后几行的 y_future_5 应为 NaN因为未来还没有数据。5.2 过拟合GP 搜出来的每个公式都可能是“数据记忆”现象训练集上 IC 很高测试集上 IC 直接归零甚至变负换一段历史区间结果完全不一样。原因gplearn 的搜索空间极大即使种群只有 4000、进化 6 代也会搜出大量恰好拟合训练段噪声的表达式。这本质上是遗传规划的自由度太高。解决用 walk-forward 方式验证把样本按时间切成训练段、验证段、回测段只在训练段上做 GP 进化再用验证段筛选表达式。另外限制复杂度把parsimony_coefficient从 0.01 提高到 0.05让表达式更短更朴素。我自己的经验是一个能用的 GP 因子表达式长度最好控制在 10 个节点以内再长基本就是过拟合的产物。5.3 函数集和常数范围太大导致搜索空间爆炸现象训练耗时长到不可接受gp 跑了几小时还没有结束或者出来的表达式里全是div(div(div(...)))这种毫无意义的深层嵌套。原因function_set放了十几个算子又没有限制const_range常数随机范围设成了(-100, 100)导致表达式树任意组合的可能性指数级膨胀GP 在有限代数里根本搜不到稳定解。解决先压缩算子集合只保留 add、sub、mul、div 四个核心算子加一两个非线性算子比如 sqrt 或 log常数范围收窄到(-1, 1)。先跑一个小规模的实验确认表达式分布合理后再逐步放宽。这个过程不需要一次到位反复试几次就能找到当前数据规模下的平衡点。5.4 因子 IC 很高但回测不赚钱往往缺了中性化现象因子单测时 IC 和分组收益都很漂亮但放到多因子模型里没有增量或者只买多头的策略跑不出超额收益。原因很多股票类因子在原始的数值里隐含了市值和行业暴露。比如 GP 在特征组合里用到了close或vol大市值股票的绝对价格和成交量天然就大因子值排名实际是在按市值排名。市值因子的 IC 看着高但它本身已经被市场充分定价了。解决对因子值做市值和行业中性化。用因子值对市值和行业虚拟变量做线性回归取残差作为最终的因子值。这一步在股票多因子体系里是标配期货市场相对少用因为期货横截面样本少行业中性化容易把自由度耗光。5.5 期货主连换月导致跳空被当成收益现象同一套代码在股票上表现正常换成期货主力连续数据后因子值和收益都出现极端异常值IC 序列在每月固定几天出现尖峰。原因主力连续合约在换月日价格会跳空如果复权处理不到位跳空会被当成真实的收益或亏损因子计算也被污染。解决在拼接主力连续时记录换月日对跳空位置做后复权处理。计算滚动特征前先把换月日前后两根 K 线标记为 NaN 或者用前复权后的价格序列重算。回测时还需要注意换月当天的信号不能用旧合约的价格下单到新合约上要在策略里显式处理换月日的目标头寸迁移。6. 进阶把 GP 因子从“看着有效”做到“敢实盘”6.1 Walk-forward 重拟合让因子持续进化静态生成一批因子然后一直用在A股和期货市场都撑不过半年。我现在的做法是 walk-forward每 20 个交易日重跑一次 GP但不是在全部历史上跑而是只取最近 120 个交易日作为训练窗口每次生成新表达式后用最近的 20 个交易日做验证把 IC 衰减的因子剔除保留表现稳定的进入因子池。这个节奏对日线数据比较合适既不会让因子过期又不会因为重拟合太频繁产生过高计算成本。重拟合时一定要固定 random_state否则每次生成的表达式之间没有延续性因子池会像换了个人在选股。6.2 因子正交化去掉市值和行业这层皮股票因子上线前我强制做一步正交化。具体做法是把因子值对市值对数、行业虚拟变量做多元线性回归用残差作为新的因子值。这一步能显著提升因子在多因子模型里的增量贡献。import statsmodels.api as sm # model_data 里需要包含 log_market_cap 和 industry_dummies X_orth sm.add_constant( model_data[[log_market_cap] industry_cols] ) model sm.OLS(model_data[gp_0], X_orth).fit() model_data[gp_0_orth] model.resid正交化之后的因子值和原始因子比IC 往往略有下降但换手率和大小市值风格漂移会明显改善。实盘中稳定比惊艳更重要这也是我把这步放进进阶的原因。6.3 一个具体技巧滚动 IC 加权组合多个 GP 因子单因子总有失效期我一般会同时保留 10 到 20 个 GP 因子用过去 60 个工作日的滚动 IC 作为权重线性组合。计算权重时只取 IC 为正的因子IC 为负的因子直接淘汰。每隔 20 个交易日重算一次权重这样因子池本身在进化组合权重也在动态调整。整个流程跑熟以后真正让我受益的不是某一个漂亮因子的诞生而是这套框架让“因子发现”变成了流水线数据进来表达式出去IC 不合格就换参数重跑再不合格就放弃而不是靠拍脑袋等灵感。我从这个项目方向里学到最重要的习惯是先做时间对齐检查再做中性化最后才看 IC 和回测曲线。很多参数问题都能靠这个顺序排查掉。希望帮到你。本文还有配套的精品资源点击获取