
简介这份华泰证券金工深度研究报告聚焦遗传规划在选股因子挖掘中的应用面向量化投资研究者、因子开发人员及金融工程方向的学习者帮助解决传统人工构建因子难以突破思维局限的问题。资源包内含1个PDF文件大小约3.32MB完整呈现25页研究报告内容。报告系统梳理了遗传规划的核心知识包括公式的树形结构表示、适应度计算、选择、交叉、变异与终止条件等总体流程并深入讲解gplearn程序包的定制改进思路如扩充函数集、引入单因子测试中性化、采用并行运算加速因子矩阵计算等。测试部分以个股20个交易日后的收益率为预测目标挖掘出6个具有增量信息的选股因子在剔除行业、市值、换手率等风格影响后仍保持较稳定的RankIC。报告还客观讨论了遗传规划因子可解释性降低的局限并提示读者可依据自身数据源、股票池与调仓周期灵活调整框架。目前已有930人学习适合希望拓展因子研究方法论的读者参考。1. 遗传规划选股因子挖掘从一份 25 页研报到一个能跑通的 gplearn 流程很多人第一次听到「遗传规划选股因子挖掘」脑子里浮现的是机器学习那套调参玄学觉得离自己很远。其实它要解决的问题非常朴素手工构造因子太慢一个分析师一年能试几百个公式就算高产而遗传规划Genetic ProgrammingGP能在同样的数据上自动演化出成千上万个候选公式再按适应度筛出真正有超额收益的那批。华泰这份 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 gplearn0.4.2 pip install numpy pandas scikit-learn0.24.2 joblib数据格式必须统一。我一般准备三张表行情表date, stock, open, high, low, close, volume, amount、财务表date, stock, roe, eps, bps 等、收益表date, stock, next_ret。全部转成宽表index 是日期columns 是股票代码值是对应字段。这样每个交易日切片出来就是一个 Series可以直接喂给 gplearn。缺失值用前向填充加截面中位数兜底不要用 00 在比率型因子里会制造假信号。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(indexdate, columnsstock, valuescol) # 前向填充再截面中位数兜底 for k in panel: panel[k] panel[k].ffill().apply(lambda s: s.fillna(s.median()), axis1) 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(pctTrue).values def _delay(x, n5): # 时序滞后这里用固定窗口实际按日切片时 x 是单日截面需另做处理 return np.roll(x, n) def _corr(x, y, n10): # 滚动相关输入两个等长序列 return pd.Series(x).rolling(n).corr(pd.Series(y)).values rank_func make_function(function_rank, namerank, arity1) delay_func make_function(function_delay, namedelay, arity1) corr_func make_function(function_corr, namecorr, arity2)这里有个大坑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_betterTrue)然后是SymbolicRegressor的配置。种群 1000 到 2000代数 20 到 50function_set放内置算子和自定义算子parsimony_coefficient控制公式复杂度太小会过拟合太大公式退化成常数。max_samples小于 1 可以做子采样加速并防过拟合。from gplearn.genetic import SymbolicRegressor est SymbolicRegressor( population_size1500, generations30, function_set[add,sub,mul,div,rank, corr_func], metricic_fitness, parsimony_coefficient0.001, p_crossover0.7, p_subtree_mutation0.1, p_hoist_mutation0.05, p_point_mutation0.1, max_samples0.8, random_state42, n_jobs-1, verbose1 )参数说明parsimony_coefficient是复杂度惩罚研报里建议 0.001 到 0.01 之间试p_crossover占大头保证收敛max_samples0.8每次用 80% 样本既加速又增加随机性。n_jobs-1开满核但注意 gplearn 的多进程在 Windows 上容易卡死Linux 或 WSL 下更稳。3.4 按日截面训练与因子输出真正跑的时候按交易日循环每个交易日取当日截面数据fit然后把最优公式predict出来作为当日因子值。这样 Rank 算子天然可用且每天独立进化避免跨日信息泄露。dates panel[close].index factor pd.DataFrame(indexdates, columnspanel[close].columns, dtypefloat) 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, axis1, methodspearman) return ic def layer_backtest(factor, ret, n5): layers factor.apply(lambda s: pd.qcut(s, n, labelsFalse), axis1) group_ret {} for i in range(n): group_ret[i] ret[layers i].mean(axis1) return pd.DataFrame(group_ret)ic_series逐日算 Rank IClayer_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(x0, 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 还是来自你对算子和数据的理解。希望帮到你。本文还有配套的精品资源点击获取