☰
遗传规划选股因子挖掘:gplearn实战流程与避坑指南
2026/9/30 1:10:30 网站建设 项目流程

简介:这份华泰证券金工深度研究报告聚焦遗传规划在选股因子挖掘中的应用,面向量化投资研究者、因子开发人员及金融工程方向的学习者,帮助解决传统人工构建因子难以突破思维局限的问题。资源包内含1个PDF文件,大小约3.32MB,完整呈现25页研究报告内容。报告系统梳理了遗传规划的核心知识,包括公式的树形结构表示、适应度计算、选择、交叉、变异与终止条件等总体流程,并深入讲解gplearn程序包的定制改进思路,如扩充函数集、引入单因子测试中性化、采用并行运算加速因子矩阵计算等。测试部分以个股20个交易日后的收益率为预测目标,挖掘出6个具有增量信息的选股因子,在剔除行业、市值、换手率等风格影响后仍保持较稳定的RankIC。报告还客观讨论了遗传规划因子可解释性降低的局限,并提示读者可依据自身数据源、股票池与调仓周期灵活调整框架。目前已有930人学习,适合希望拓展因子研究方法论的读者参考。

1. 遗传规划选股因子挖掘:从一份 25 页研报到一个能跑通的 gplearn 流程

很多人第一次听到「遗传规划选股因子挖掘」,脑子里浮现的是机器学习那套调参玄学,觉得离自己很远。其实它要解决的问题非常朴素:手工构造因子太慢,一个分析师一年能试几百个公式就算高产,而遗传规划(Genetic Programming,GP)能在同样的数据上自动演化出成千上万个候选公式,再按适应度筛出真正有超额收益的那批。华泰这份 2019 年的研报把这件事讲成了可复现的工程流程,核心工具就是 Python 生态里的 gplearn。它适合两类人:一类是已经有日频行情和财务数据、想批量生产因子的量化研究员;另一类是刚学完 Python 基础语法、想找一个真实项目练手的入门者。这篇笔记不逐页翻译研报,而是把「遗传规划怎么定义因子、gplearn 怎么配、因子怎么验证」这条链路拆成能照着敲的步骤,顺带把血泪踩坑点标出来。

遗传规划的本质是让计算机自己写公式。它把每个候选因子表示成一棵表达式树,叶子节点是原始特征(开盘价、成交量、净资产等),内部节点是算子(加减乘除、Rank、Delay、Correlation 等)。一代一代地交叉、变异、选择,适应度高的树活下来。放到选股场景里,适应度就是「这个因子值和未来收益的相关性」或者「用它分层回测的多空收益」。gplearn 是 scikit-learn 风格的实现,API 和RandomForestRegressor几乎一样,fit、predict、transform三件套,学习成本低。但要注意,gplearn 原生只支持逐样本的符号回归,做截面选股因子需要自己包一层「按日期分组」的逻辑,这是后面最容易翻车的地方。

2. 遗传规划挖因子的原理与 gplearn 的算子体系

2.1 表达式树、适应度与遗传算子到底在做什么

先把概念立住,不然后面调参全靠猜。遗传规划的一次「进化」包含四步:初始化种群、计算适应度、选择、交叉变异。种群就是一堆表达式树,比如(close - open) / open是一棵三节点的树,Rank(Correlation(volume, close, 10))是嵌套更深的树。初始化常用grow或half and half方法,前者生成不规则树,后者混合满树和随机树,gplearn 里用init_method控制。

适应度函数是灵魂。研报里用的是因子值与下期收益的 Rank IC(秩相关系数),因为 IC 对异常值不敏感,且直接对应「因子能不能排序股票」。gplearn 默认的适应度是mean_absolute_error,做回归预测用的,选股场景必须换成自定义的make_fitness。选择环节用锦标赛选择(tournament selection),每次随机抽tournament_size个个体,留适应度最高的,这个参数越大选择压力越大,种群多样性掉得越快。

交叉是把两棵树的子树互换,变异是随机替换一个子树或一个节点。gplearn 的p_crossover、p_subtree_mutation、p_hoist_mutation、p_point_mutation四个概率加起来要等于 1,这是硬约束,配错了直接报错。理解这四步之后,调参就有方向了:想探索更复杂的公式就提高p_subtree_mutation,想稳定收敛就提高p_crossover。

2.2 gplearn 内置算子与选股常用算子的对应关系

gplearn 自带add, sub, mul, div, sqrt, log, abs, neg, inv, max, min, sin, cos, tan这些函数,还有protected版本防止除零和负数开方。但选股里高频出现的Rank、Delay、Correlation、Ts_Std它一个都没有,必须自己写。这是整个流程里工作量最大的一块,也是决定因子质量的关键。

算子含义选股用途是否 gplearn 内置
Rank截面排序百分位消除量纲,做中性化否,需自定义
Delay取 N 期前的值构造动量、反转否
Correlation滚动窗口相关系数量价背离类因子否
Ts_Std滚动窗口标准差波动率类因子否
Div保护除法比率型因子是
Log对数压缩右偏分布是

自定义算子的写法有讲究。gplearn 要求函数接收 numpy 数组,返回同形状数组,并且要处理 NaN 和 inf。截面算子(Rank)和时序算子(Delay、Correlation)的输入维度不同:截面算子在每个交易日横截面上算,时序算子在每只股票的时序上算。如果直接把整个面板数据塞进去,两种算子会互相污染。常见做法是先把数据整理成「日期 × 股票」的宽表,每个交易日单独调用一次gp.fit,或者用groupby按日期分组后并行。研报里采用的是后者,按日截面挖掘,这样 Rank 天然可用,但计算量大,25 页里提到的加速技巧就是并行和种群精简。

3. 用 gplearn 跑通一次因子挖掘的最小流程

3.1 环境准备与数据格式约定

先装环境。Python 3.8 以上,gplearn 用 pip 装即可,依赖 numpy、scipy、scikit-learn、joblib。注意 gplearn 对 scikit-learn 版本敏感,0.22 到 1.0 之间比较稳,太新的版本可能因为_validate_data改名报错,这是血泪经验。

pip install gplearn==0.4.2 pip install numpy pandas scikit-learn==0.24.2 joblib

数据格式必须统一。我一般准备三张表:行情表(date, stock, open, high, low, close, volume, amount)、财务表(date, stock, roe, eps, bps 等)、收益表(date, stock, next_ret)。全部转成宽表,index 是日期,columns 是股票代码,值是对应字段。这样每个交易日切片出来就是一个 Series,可以直接喂给 gplearn。缺失值用前向填充加截面中位数兜底,不要用 0,0 在比率型因子里会制造假信号。

import pandas as pd import numpy as np def load_panel(path): df = pd.read_parquet(path) df['date'] = pd.to_datetime(df['date']) # 转宽表:每个字段一张 date x stock 的矩阵 panel = {} for col in ['open','high','low','close','volume','amount','next_ret']: panel[col] = df.pivot(index='date', columns='stock', values=col) # 前向填充,再截面中位数兜底 for k in panel: panel[k] = panel[k].ffill().apply(lambda s: s.fillna(s.median()), axis=1) return panel

这段代码的关键在pivot和填充顺序。先ffill再按行填中位数,保证同一交易日所有股票都有值,否则 gplearn 遇到 NaN 会直接抛异常。next_ret是下期收益,必须提前对齐,不能有未来函数,这是因子挖掘的红线。

3.2 自定义 Rank、Delay、Correlation 算子

gplearn 的make_function用来注册自定义算子。截面 Rank 最简单,时序算子需要闭包保存窗口长度。下面三个是选股里最常用的。

from gplearn.functions import make_function def _rank(x): # 截面排序,返回 0~1 百分位 return pd.Series(x).rank(pct=True).values def _delay(x, n=5): # 时序滞后,这里用固定窗口,实际按日切片时 x 是单日截面,需另做处理 return np.roll(x, n) def _corr(x, y, n=10): # 滚动相关,输入两个等长序列 return pd.Series(x).rolling(n).corr(pd.Series(y)).values rank_func = make_function(function=_rank, name='rank', arity=1) delay_func = make_function(function=_delay, name='delay', arity=1) corr_func = make_function(function=_corr, name='corr', arity=2)

这里有个大坑:make_function注册的算子默认作用在「单个样本」上,而 gplearn 的fit是把所有样本堆成一个二维数组。如果按日切片逐日fit,那x就是当日所有股票的一维数组,rank直接可用,但delay和corr需要历史数据,单日切片拿不到。解决办法是提前把时序算子算好,作为额外特征列喂进去,让 GP 只负责组合,不负责时序计算。研报里也是这么做的,把Delay(close,5)、Correlation(volume,close,10)预先算成列,GP 的搜索空间就变成这些预计算因子的代数组合。这样既保留了时序信息,又避免了算子维度混乱。

3.3 适应度函数与 SymbolicRegressor 配置

适应度用 Rank IC。gplearn 的make_fitness要求函数签名是(y, y_pred, w),返回一个标量,越大越好。IC 可能是负的,取绝对值或直接取负号都行,我一般取负 IC 的绝对值,因为因子方向和收益方向不重要,重要的是区分度。

from gplearn.fitness import make_fitness from scipy.stats import spearmanr def _ic(y, y_pred, w): ic, _ = spearmanr(y, y_pred) return abs(ic) if not np.isnan(ic) else 0.0 ic_fitness = make_fitness(function=_ic, greater_is_better=True)

然后是SymbolicRegressor的配置。种群 1000 到 2000,代数 20 到 50,function_set放内置算子和自定义算子,parsimony_coefficient控制公式复杂度,太小会过拟合,太大公式退化成常数。max_samples小于 1 可以做子采样,加速并防过拟合。

from gplearn.genetic import SymbolicRegressor est = SymbolicRegressor( population_size=1500, generations=30, function_set=['add','sub','mul','div','rank', corr_func], metric=ic_fitness, parsimony_coefficient=0.001, p_crossover=0.7, p_subtree_mutation=0.1, p_hoist_mutation=0.05, p_point_mutation=0.1, max_samples=0.8, random_state=42, n_jobs=-1, verbose=1 )

参数说明:parsimony_coefficient是复杂度惩罚,研报里建议 0.001 到 0.01 之间试;p_crossover占大头保证收敛;max_samples=0.8每次用 80% 样本,既加速又增加随机性。n_jobs=-1开满核,但注意 gplearn 的多进程在 Windows 上容易卡死,Linux 或 WSL 下更稳。

3.4 按日截面训练与因子输出

真正跑的时候,按交易日循环,每个交易日取当日截面数据fit,然后把最优公式predict出来作为当日因子值。这样 Rank 算子天然可用,且每天独立进化,避免跨日信息泄露。

dates = panel['close'].index factor = pd.DataFrame(index=dates, columns=panel['close'].columns, dtype=float) for dt in dates[:-1]: X = pd.DataFrame({ 'close': panel['close'].loc[dt], 'volume': panel['volume'].loc[dt], 'amount': panel['amount'].loc[dt], 'delay5': panel['close'].shift(5).loc[dt], 'corr10': panel['close'].rolling(10).corr(panel['volume']).loc[dt] }).dropna() y = panel['next_ret'].loc[dt].reindex(X.index) if len(X) < 100: continue est.fit(X.values, y.values) factor.loc[dt, X.index] = est.predict(X.values)

逻辑说明:每个交易日构造特征矩阵 X,包含原始行情和预计算的时序因子,y 是下期收益。dropna保证样本完整。len(X) < 100跳过样本太少的交易日。训练完直接predict得到当日因子暴露。这段代码跑全市场十年数据大概几小时,取决于种群大小和核数。输出factor就是挖掘出的因子面板,可以拿去做分层回测。

4. 因子有效性验证与常见翻车点排查

4.1 IC 衰减、分层回测与换手率检查

挖出因子不等于能用,必须验证。三个指标:IC 均值、ICIR、分层单调性。IC 均值大于 0.03 算及格,ICIR 大于 0.3 算稳定,分层回测多空收益要单调。换手率也要看,GP 挖出的公式如果含高频 Rank,换手可能高得离谱,扣掉交易成本就没了。

def ic_series(factor, ret): ic = factor.corrwith(ret, axis=1, method='spearman') return ic def layer_backtest(factor, ret, n=5): layers = factor.apply(lambda s: pd.qcut(s, n, labels=False), axis=1) group_ret = {} for i in range(n): group_ret[i] = ret[layers == i].mean(axis=1) return pd.DataFrame(group_ret)

ic_series逐日算 Rank IC,layer_backtest按因子值分 5 层,看每层平均收益是否单调。如果第 1 层和第 5 层收益差不多,说明因子没有区分度,回去调适应度或换算子。

4.2 遗传规划选股因子挖掘的 5 个避坑记录

现象一:训练集 IC 很高,样本外直接归零。原因:公式过拟合,GP 找到了数据里的噪声。解决:提高parsimony_coefficient,减少generations,加max_samples子采样,或者用滚动窗口训练,每半年重新挖一次。

现象二:程序跑一半报ValueError: The function set is empty。原因:自定义算子注册失败,或者function_set里写了不存在的名字。解决:检查make_function的arity是否和函数参数个数一致,function_set里字符串必须是 gplearn 内置名或已注册的算子对象。

现象三:因子值全是 NaN 或 inf。原因:自定义算子没处理除零和负数开方。解决:所有自定义函数里加np.where保护,比如np.where(x==0, 1e-8, x),开方前取绝对值。

现象四:多进程跑满 CPU 但速度没提升。原因:gplearn 的n_jobs在 Windows 上基于multiprocessing,和 pandas 的线程池冲突。解决:换 Linux 或 WSL,或者把n_jobs设为 1,用外层joblib按日期并行。

现象五:因子分层回测单调但多空收益为负。原因:因子方向和收益方向反了,或者next_ret对齐错位。解决:检查shift方向,next_ret必须是close.pct_change().shift(-1),不能有未来函数。方向反了就把因子取负号。

5. 让 GP 因子更稳的三个进阶技巧

第一个技巧是算子池分层。不要把 Rank、Delay、Correlation 全塞进一个function_set,而是分两阶段:第一阶段只用基础算术和 Rank,挖出粗因子;第二阶段把粗因子作为新特征,加入时序算子再挖一轮。这样搜索空间可控,公式可解释性也强。研报里提到的「因子池迭代」就是这个思路。

第二个技巧是适应度加约束。纯 IC 适应度会奖励高换手因子,可以在适应度里减去换手惩罚项,比如abs(ic) - 0.1 * turnover。turnover 用因子值的一阶自相关近似,自相关越低换手越高。这个改动能让挖出的因子更贴近实盘。

第三个技巧是种群多样性监控。gplearn 的run_details_里有每代的适应度和长度分布,如果发现所有个体长度趋同、适应度早熟,就提高p_subtree_mutation或增大tournament_size。我一般每 5 代打印一次length_的均值和方差,方差掉到 1 以下就说明种群退化了,得重启或调参。

最后说个验证习惯:任何 GP 因子在上实盘前,必须做三件事——样本外 IC 衰减曲线、分年度分层回测、和已有因子池的相关性检查。相关性高于 0.7 的因子没有增量价值,直接丢掉。这套流程我跑了两年,最大的教训是别迷信 GP 的自动化,它只是个公式生成器,真正的 alpha 还是来自你对算子和数据的理解。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询