用gplearn遗传规划挖掘CTA与股票因子:完整流程与实战避坑

发布时间:2026/10/18 14:49:21
用gplearn遗传规划挖掘CTA与股票因子:完整流程与实战避坑 简介面向量化研究与商品交易顾问策略开发这份资源演示如何用遗传规划方法与Python库gplearn自动生成交易因子。不同于传统人工构造技术指标的方法它通过表达式树与选择、交叉、变异等遗传操作在高维市场数据中搜索可解释的因子模型适合有一定Python基础并希望探索因子挖掘的投资者、学生或研究员。资源共53个文件压缩包约87MB以Python源码、pyc字节码、CSV数据、Pickle序列化因子及PDF说明文档为主另含少量图片与可视化图文件代码与数据分层存放便于按数据预处理、因子测试、IC检验、回测和绘图流程复现。目前已有42人学习下载。借助这套资料可直观看到gplearn搭建、因子生成与稳健性验证的关键环节并利用自带数据与脚本快速跑通从因子构造到回测验证的完整链路对理解遗传规划在金融数据分析中的应用具有参考价值。1. 遗传规划也能造因子先搞清楚 gplearn 在量化里到底帮你干了什么做量化的人都知道人工挖因子的瓶颈通常不在模型而在想象力。翻研报、看价量、试各类技术指标本质上都是在已知的表达式空间里打转思维很快会固化。而遗传规划Genetic ProgrammingGP换了一条路它让计算机自己从基础算子出发通过选择、交叉、变异去演化出符合目标要求的数学表达式。配合开源库 gplearn这套流程可以落到实际代码里从原始行情数据一路跑到 IC 评估和回测。这份资源就是这样一套可以直接运行的 CTA 与股票因子生成 Demo代码里既有setupGPlearn.py的 GP 参数配置也有stock_selection_factor.py、timing_factor.py等因子构建模块适合个人学习者完整复现“数据预处理 → 遗传进化 → 因子提取 → IC 验证 → 回测”的整条链路。它不追求开箱即用更像一个能让你亲手拆开、逐段改的实验基底。2. gplearn 的进化机制表达式树、目标函数与进化解读遗传规划的搜索空间不是数值而是程序本身。gplearn 里一段“基因”就是一棵表达式树树的内部节点是运算符叶子节点是输入数据或常数。理解这棵树怎么生成、怎么被评估是后面所有调参和排错的前提。2.1 表达式树gplearn 的“基因”是公式而不是数值传统遗传算法优化的是固定结构里的参数比如一组均线长度遗传规划连结构一起优化。gplearn 中一个个体可以是mul(x1, x2)这样的两变量乘积也可以是sub(x3, div(x4, x5))这种嵌套结构。树的生成受init_depth控制它决定初始个体的最小和最大深度。深度太浅表达式表达力不够深度太深很容易长出包含冗余子树的“肿瘤”程序。# 一个常见配置控制初始树的深度范围 from gplearn.genetic import SymbolicTransformer gp SymbolicTransformer( init_depth(2, 6), # 初始个体深度在 2 到 6 之间 function_set[add, sub, mul, div, sqrt, log, max, min], )init_depth的元组第一项是初始化时的最小深度第二项是最大深度。注意这和最终表达式的复杂度不完全划等号因为交叉和变异会把树继续放大后面还要靠parsimony_coefficient来做复杂度惩罚。function_set是另一个关键入口。默认函数集里包含四则运算、平方根、对数等但实际因子挖掘中我一般会砍掉sin、cos这类周期函数原因很简单行情序列里的“周期”往往不稳定用三角函数拟合出来的因子在样本外表现普遍很差。这个取舍在资源里的gplearn_basic.py中可以直接改删掉几个函数再跑一次对比 IC 差异是对 GP 机制最直观的一课。2.2 目标函数与进化压力没有合适的 fitness进化就是随机游走gplearn 的metric参数决定个体适应度的计算方式可选pearson、spearman分别对应 Pearson 相关系数和 Spearman 秩相关系数。对选股类因子我强烈建议用spearman因为它对极端值不敏感评判的是“顺序关系”而不是线性相关强度。CTA 单品种择时因子则要看场景如果目标是抓趋势段pearson加去极值后的收益序列也能用。配好适应度函数后每一次进化迭代要做三件事锦标赛选择、交叉、变异。锦标赛从当前种群中随机抽一小批个体选适应度最高的作为父代tournament_size控制每次比较的个体数量交叉是交换两棵树的子树p_crossover控制发生该操作的概率变异则细分为子树变异、提升变异、点变异和替代变异分别对应替换子树、把一个子树提升到根、改变单个节点、用新随机子树替换局部。# 典型参数配置控制进化压力与突变强度 gp SymbolicTransformer( population_size2000, # 每代个体数量 generations20, # 进化代数 tournament_size20, # 锦标赛选手数量越小选择压力越大 p_crossover0.9, # 交叉概率 p_subtree_mutation0.01, # 子树变异概率 p_hoist_mutation0.01, # 提升变异概率 p_point_mutation0.01, # 单点变异概率 p_rep_mutation0.01, # 替代变异概率 parsimony_coefficient0.01, # 复杂度惩罚系数越大越偏好短公式 )这些参数之间是联动的。tournament_size设太大高适应度个体很容易垄断种群早熟收敛设太小选择压力过强也会把多样性格掉。parsimony_coefficient新手最容易忽略它直接加到适应度上作为复杂度惩罚项建议从 0.001 开始逐步加大观察 IC 和表达式长度的变化。资源里的setupGPlearn.py其实就是把这些参数集中到一个配置文件里方便反复试。2.3 学会读取进化的产物从_best_programs到表达式字符串gplearn 的SymbolicTransformer在fit之后会把最优个体存在_best_programs属性里但很多第一次接触的人不知道如何把它变成可读的公式。这一步至关重要因为因子的核心价值在于可解释性如果连表达式都读不出来后面任何分析都是黑匣子操作。# 打印最优个体的表达式用于因子落库与人工检查 best_programs gp._best_programs print(Top1 program:, best_programs[0]) print(Top1 expression length:, best_programs[0].length_)每个 program 对象都有__str__方法可以直接把表达式树转成字符串例如mul(sub(X2, X4), sqrt(X1))。length_属性返回表达式节点数这是评估复杂度的硬指标。实际操作中我会把str(program)存到数据库或 JSON 文件里再用自己的解析器在factor_test.py中计算因子值——这一点资源里的代码管线已经做好了你需要做的只是在每次进化结束后手动打印几代最优程序对比它们的结构演化。这里还要提醒一个容易误解的点SymbolicTransformer输出的是经过变换后的新特征矩阵n_components决定保留多少个演化出的程序作为最终特征。它和SymbolicRegressor不同后者直接输出预测值前者更像是“自动特征工程”。因子挖掘场景里绝大多数情况用SymbolicTransformer更合适因为它把“找公式”和“预测收益”分离开你可以先演化出 30 个候选公式再用 IC 排序去粗取精。而SymbolicRegressor则会让 GP 直接拟合未来收益拟合的是含噪声的目标过拟合风险更高。3. 数据预处理与 GP 参数配置先把输入洗干净再谈进化效率gplearn 本身不关心你的数据是什么格式但它对数据质量极其敏感。缺失值、异常值、不同量纲的特征混在一起都会让表达式树往错误方向演化。资源里DataProcess.py承担的就是这一步脏活。3.1 输入数据长什么样从原始行情到特征矩阵如果跑的是 CTA 因子输入数据通常是一个或多个品种的时序数据包含开盘价、最高价、最低价、收盘价、成交量、持仓量等字段。gplearn 的输入特征一般是过去 N 日的技术指标或收益率目标变量是未来一段时间的收益或方向标签。资源里的stock_data.pickle后面就对应一个标准的多股票面板数据包含每只股票的日期、价格和涨跌幅信息。数据组织方式直接决定因子含义。比如你想让 GP 发现“过去 5 日动量 波动率倒数”这类组合就要提前把动量、波动率构造成特征列 X把未来 5 日收益构造成目标 y。gplearn 不会替你生成特征它只负责组合你给它的特征。特征构造越贴近真实交易逻辑演化出的因子越可靠。3.2 数据清洗去极值、Z-Score 与缺失值处理的顺序不能乱# 数据预处理顺序为去极值 - 标准化 - 填充缺失 import numpy as np import pandas as pd def winsorize_series(s, n6): # 去极值超过 n 倍标准差的值截断到边界 mean, std s.mean(), s.std() lower, upper mean - n * std, mean n * std return s.clip(lower, upper) # 对每一列的因子值做处理 df_factors df_factors.apply(winsorize_series, n6) df_factors (df_factors - df_factors.mean()) / df_factors.std() # 截面 Z-Score df_factors df_factors.fillna(0) # 缺失值置 0去极值放在标准化之前这一步顺序错了结果会偏很多。winsorize_series的 n 取 3 到 6 之间n 越小对极值打压越厉害。用多少倍标准差不是固定的商品期货数据波动率突变多我用 6股票日频数据极端值相对少用 4 就够。标准化这里用的是截面 Z-Score即每个截面日期上对所有股票做标准化而不是时序标准化。时序标准化会把不同股票在不同时期的量纲混在一起gplearn 在演化过程中会花大量复杂度去“纠正”这个量纲问题浪费树深。截面标准化之后每个特征在同一天的不同股票之间具有可比性适合做截面选股。缺失值填充放在最后因为去极值和标准化在有 NaN 时会计算出错误均值和方差。3.3 DataProcess.py 输出规范与 pickle 文件的坑处理完的数据一般以 pickle 格式保存资源里data目录下就有stock_data.pickle和factor_data.pickle。pickle 的好处是保留 DataFrame 的索引和 dtype但跨 Python 版本恢复时容易出问题实践中最稳妥的做法是同时存一份 CSV 作为备份。资源里的IC_train.csv和IC_test.csv走的就是这条路径。factor_data.pickle的列结构建议保持规范第一列是股票代码第二列是日期自第三列起是 GP 生成的新特征。不要混入原始价格列因为 gplearn 会把价格类特征直接做加减乘除生成类似price1 - price2的价差因子这类因子在时序上往往非平稳回测里看着有效实盘一换时间段就失效。资源和摘要里反复提到 CTA 因子生成CTA 的核心输入就是一根价格序列的若干变换所以预处理阶段就该把收益率、波动率这类平稳化特征喂进去。参数配置层面setupGPlearn.py里的max_samples参数也值得单独说。max_samples控制在每代个体训练时从样本中抽取多少比例来评估适应度设为 0.8 意味着每棵树只看到 80% 的数据。这不是为了提速而是引入一定随机性来防止过拟合——每一代不同的样本子集相当于天然做了数据扰动。但注意max_samples太低会让适应度评估不稳定同一棵树在相邻两次进化中的排名可能大幅波动通常取 0.7 到 0.9 之间比较合理。4. 三类因子构建实战选股因子、择时因子与 CTA 趋势因子因子挖掘不是一次性生成完就结束而是要区分使用场景用不同的目标函数和数据组织方式驱动 gplearn。资源里把因子的生成拆成stock_selection_factor.py、timing_factor.py加上回测模块backtest.py恰好对应三种主流逻辑截面选股、时序择时、CTA 趋势跟踪。4.1 选股因子截面 IC 驱动n_components 与 hall_of_fame 的取舍选股因子的逻辑是“同一天内哪些股票未来涨得多”评估方式通常用截面 IC。对应到 gplearn就是把特征矩阵按股票和日期对齐目标变量设为未来 N 日收益metricspearman让 GP 直接优化秩相关。# 选股因子生成关注 n_components 与 hall_of_fame from gplearn.genetic import SymbolicTransformer gp_stock SymbolicTransformer( generations20, population_size1000, n_components30, # 保留 30 个演化出的公式 metricspearman, # 截面选股优先用秩相关 hall_of_fame50, # 记录前 50 个历史最优个体 function_set[add, sub, mul, div, sqrt, inv], ) gp_stock.fit(X_train, y_train)n_components决定最终输出多少特征列。设太大会让下游冗余过多设太小又可能在进化结束时没有足够候选公式做筛选。我一般设为 20 到 50 之间然后用相关性聚类去冗余。hall_of_fame很多人以为是多余的但它解决的是 gplearn 的一个重要缺陷最后的_best_programs只保留当代种群的最优个体而进化过程中的历史最优个体可能比末期更好。hall_of_fame会把历史各代的最优个体也缓存下来配合factor_test.py做整体排名。实操中我发现最终入选的優质因子往往有一半以上来自 hall of fame而不是最后一代。4.2 择时因子目标从“截面谁强”变成“时序何时强”择时因子就不再看截面排名了同一品种在不同时间的信号强弱是核心。此时特征矩阵的每一行是一个时间点目标变量是未来 N 日的收益或方向。很多人在这一步直接把选股的代码搬过来结果 IC 惨不忍睹原因在于目标变量没对齐——未来收益必须用shift(-N)前移而不是直接用当期的收益。import pandas as pd def make_ts_dataset(close, feat_names, horizon5): # 构造时序特征过去 K 日收益率 波动率 ret close.pct_change() X pd.DataFrame(indexclose.index) for name in feat_names: X[name] ret.rolling(10).mean() # 示例特征实际按你的设计来 X[vol] ret.rolling(10).std() y close.pct_change(horizon).shift(-horizon) # 未来 horizon 日收益 X, y X.dropna(), y.dropna() return X.align(y, joininner)horizon的取值直接决定因子频率。做日频择时用 5做周频趋势用 20。注意dropna()之后的行数差异——rolling 窗口越大可用的训练样本越少这个在设置generations和population_size时要同步考虑如果样本只有几百行1000 个种群的进化基本没有统计意义。择时因子的metric建议用pearson配合去极值的收益序列因为你要捕捉的是收益率的大小关系秩相关会把尾部信息抹平。但如果你的目标是方向预测涨跌分类那应该在 y 上先做符号化处理再用pearson去拟合这个 0/1 变量。4.3 CTA 趋势因子让 GP 直接构造“突破 动量”组合CTA 因子是资源标题里的核心场景。趋势跟踪的核心逻辑是“强者恒强”经典人工写法是close / rolling_max(close, N) - 1。GP 的优势在于它可以组合出更灵活的结构比如vol_adj_momentum这类由多个基础特征组成的复杂表达式。实际落地 CTA 因子时我习惯把输入特征做成“分层动量”而非单一动量过去 5 日动量、10 日动量、20 日动量、30 日波动率再加一个 60 日最高价突破比例。GP 负责在这些特征里搜索组合方式而不是直接从裸价格出发。裸价格出发的表达式会包含大量price_a / price_b这类比例结构看起来很合理但换品种、换时间区间后稳定性很差。# CTA 因子的落库逻辑把公式字符串和因子值同时保存 factor_dict {} for i, expr in enumerate(selected_programs): formula str(expr) # 形如 mul(div(X1, X3), sqrt(X5)) factor_dict[fGP_CTA_{i}] { formula: formula, length: expr.length_, complexity: expr.length_ * (1 expr.depth_ * 0.1), } # 再把 dict 转成 DataFrame 存 factor_data.pickleexpr.depth_是树的深度属性用“长度 深度惩罚”作为复杂度分是资源之外我自己加的评估维度。原因很简单长度相同但深度不同的两棵树过拟合风险差很多——深树意味着依赖多重嵌套结构边界条件一换就失效。CTA 因子的回测验证要注意手续费和滑点设置backtest.py里一般有这几个参数手续费率、滑点、仓位比例。GP 生成的因子换手率普遍偏高因为表达式里如果出现abs、sign这类函数信号在零轴附近会频繁翻转。如果回测参数里手续费设成 0你看到的夏普比率会虚高一截这一点后面避坑环节会详细展开。5. 验证、回测与避坑IC 分析、过拟合排查与五个高频翻车点因子生成出来不等于能用这是整个流程里最容易栽跟头的一段。IC 计算、分位数测试、回测绕不开但里头埋伏着各种数据对齐和逻辑顺序问题。这一章把资源和摘要里提到的验证链路拆开讲并在最后集中列出五个高频坑。5.1 IC 与 ICIR因子有效性的第一道体检ICInformation Coefficient衡量因子值与未来收益之间的相关性ICIR 是 IC 的均值除以标准差反映因子稳定性。完整的 IC 分析代码在资源里的IC.py核心逻辑如下def calc_ic(factor_series, future_ret, methodspearman): # factor_series 与 future_ret 必须按同一截面日期对齐 return factor_series.groupby(leveldate).apply( lambda g: g.corr(future_ret.loc[g.index], methodmethod) ).mean() # 按年统计 IC观察因子是否在样本内各个子区间都有效 df_ic factor_data.groupby(leveldate).apply(...) ic_by_year df_ic.groupby(df_ic.index.year).mean()IC 评估有个不成文的门槛均值 IC 绝对值超过 0.03 才能考虑纳入后续测试低于 0.02 的基本是噪声。ICIR 一般要求大于 0.5而 IC 序列的正向占比要高于 55%否则不稳定。更严格的验证要看 IC 衰减速度——未来 1 日、3 日、5 日的 IC 是否递减。如果 1 日 IC 高但 5 日 IC 转为负值说明因子捕捉的信息非常短命交易成本会把收益吃掉。5.2 分位数测试与回测从单因子到组合持仓的落地验证# 分位数测试将因子值按五分位分组看每组未来收益是否单调 factor_data[rank] factor_data.groupby(date)[factor].rank(pctTrue) factor_data[quantile] pd.qcut(factor_data[rank], 5, labelsFalse) quantile_ret factor_data.groupby( [date, quantile] )[future_ret].mean().unstack()分位数测试的核心是“单调性”。如果第 5 组收益远高于第 1 组且中间几组排序清晰因子才具备实际区分能力。反之如果只有最高组突出其余四组无差别这种因子本质上是抓极端值风险很大。backtest.py的组合回测要注意它的持仓周期设置。常见的错误是生成因子用未来 5 日收益作为目标但回测时却每日调仓这中间的频率错配会让结果严重失真。我操作的路径是因子生成、IC 验证、分位数测试都统一用 5 日周期回测也每 5 个交易日调一次仓保持周期一致性。5.3 五个高频坑现象、原因与解决第一条训练集 IC 高得离谱0.2 以上测试集 IC 直接变负。原因是generations设太大且parsimony_coefficient设为 0GP 把训练集的噪声当成规律学进去了。解决方法是把惩罚系数从 0.001 往上调同时限代数在 20 代以内再通过max_samples0.8减少评估稳定性偏差。第二条gplearn.fit报ValueError: could not convert string to float。原因是特征矩阵里有字符串列或 NaN 未被处理。gplearn 内部做向量运算时不会帮你清洗数据缺失值一旦进入函数集里的div或sqrt会产生无穷值底层计算直接崩。解决方法是预处理阶段最后执行X.isnull().sum()检查并把 NaN 用截断值或中性值填充。第三条用pearson当 metric生成的因子全在捕捉极端涨跌。现象是 IC 在少数暴涨暴跌日期贡献巨大日常交易日 IC 近乎为零。原因是 Pearson 相关对离群点敏感一个极端点就把相关系数拉高。解决方法是改用spearman或者在目标变量上做 winsorize截断 3 倍标准差之外的收益。第四条backtest.py跑出的年化收益高到不真实。原因多半是样本数据对齐出了未来函数。比如用当日收盘价算因子信号却在同一天的收盘价成交——实际上信号产生后最早次日开盘才能成交。解决方法是统一在信号生成后shift(1)再计算收益严格避免同根 K 线的“偷看”。第五条pickle 文件跨机器加载失败报UnpicklingError。原因是stock_data.pickle和factor_data.pickle由不同 Python 版本或 pandas 版本生成二进制格式不兼容。解决方法是预处理输出时同时写一份 CSV 备份加载时先判断 pickle 是否可用不可用就走 CSV。# 加载数据的稳健写法pickle 失败自动回落 CS import pandas as pd try: df pd.read_pickle(data/stock_data.pickle) except Exception: df pd.read_csv(data/stock_data.csv, index_col0)这一章值得你在跑任何 gplearn 因子实验前反复对照。五个坑我全部踩过其中未来函数那个问题最隐蔽——它不影响 IC 计算但一上回测曲线就直冲上天看着爽实则全是泡沫。6. 让生成的因子真正能上实盘滚动样本外验证与因子防堆积技巧最后一章分享两个我在实战中最常用的技巧它们不改变 gplearn 本身的进化逻辑但能让生成因子更接近真实可用的状态。第一个技巧是滚动 Walk-Forward 验证。静态地把数据切成 train/test 两份然后只在 train 上跑 GPtest 上算 IC这个流程看着合理但忽略了市场状态的变化。CTA 策略的因子往往在不同波动环境下表现差异巨大我一般把样本切成 6 到 8 段每段连续训练 2 段、测试 1 段循环往复最终把所有测试段的 IC 拼成一条完整的样本外 IC 序列。# Walk-forward 验证骨架伪代码逻辑 for i in range(6): train df.iloc[i*seg : (i2)*seg] test df.iloc[(i2)*seg : (i3)*seg] gp.fit(train_X, train_y) test_ic[i] calc_ic(gp.predict(test_X), test_y)只有这条样本外 IC 序列的平均值和稳定性和样本内接近因子才值得保留。用这种验证方式我经常发现一个残酷事实gplearn 的hall_of_fame里排名前十的公式在滚动验证下只有两三个能活下来。第二个技巧是因子表达式去重。gplearn 的演化过程中大量表达式表面上不同数学结构却等价。比如div(X1, sqrt(X2))和mul(X1, inv(sqrt(X2)))完全一样却占掉多个n_components名额。我在保存因子的同时会计算表达式之间的相似度评分两两相关系数超过 0.8 就只保留 IC 更高的那个。这一步能把候选因子从 30 个直接缩到 8 个左右大幅度降低下游的多因子共线性风险。从那次因为未来函数导致回测曲线虚高、实盘季度亏损的翻车之后我养成了一个习惯任何一个 gplearn 生成的因子必须强制走完“Walk-Forward 样本外验证 表达式去重 复杂度惩罚检查”三道关缺一不可。资源里的主流程脚本已经包含因子生成和基础验证顺着这份思路往里加滚动切片和相似度过滤会让这套 GP 流程从一个“玩具 Demo”变成真正接近实战的因子挖掘工具。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询