十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

遗传规划选股因子挖掘:从表达式树到RankIC验证

遗传规划选股因子挖掘:从表达式树到RankIC验证 简介一份华泰证券人工智能系列深度研究报告聚焦遗传规划在选股因子挖掘中的应用面向量化研究者、金融工程从业者及对多因子模型感兴趣的投资者。报告系统讲解遗传规划的总体流程、公式表示、适应度计算与进化方法并基于gplearn程序包进行深度定制扩充函数集、引入单因子测试与并行计算从有限量价数据中挖掘出6个具有增量信息的选股因子。资源为单个PDF文件共1个文件压缩包大小3.32MB便于阅读与存档。已有929人学习下载。通过本报告可了解“先有公式、后有逻辑”的因子研究新思路掌握遗传规划框架的关键参数调节与改进方向为自行构建因子挖掘体系提供可复用的方法论参考。1. 基于遗传规划的选股因子挖掘从搜索公式到发现规律选股因子挖掘的常见误区是把注意力放在搜索空间的大小上。传统暴力搜索遍历几百万个公式组合最终得到的往往是一堆在训练集上 IC 很高、换到验证集就失效的“拟合产物”。基于遗传规划的选股因子挖掘本质上是用进化算法在表达式空间里搜索一个能提升股票截面区分度的公式它不预设因子的具体形式而是让算子自动组合出可解释的算术表达式。华泰证券在 2019 年发布的 AI 系列第二十一篇研报把这一方法系统地搬到了国内 A 股场景今天回看仍是很多团队搭建因子挖掘框架的起点。它解决三个具体问题因子表达式怎么编码、用什么适应度函数引导进化、挖出的因子如何通过回测检验。对量化研究员和 AI 工程化团队来说这套流程的价值在于把“拍脑袋想因子”变成“可重复的自动搜索”同时保留最终结果的可解释性。本文按“理论 — 实现 — 验证 — 鲁棒性”的顺序给出一个可以直接落地的 GP 因子挖掘方案。2. 遗传规划因子挖掘的形式化基础表达式树、函数集与适应度2.1 把因子公式改写到语法树上遗传规划GP与传统遗传算法的核心区别在于个体表示。遗传算法通常用固定长度的向量表示解而 GP 用树形结构表示程序或表达式。比如因子公式(close - open) / (high - low 0.01)可以写成一颗语法树根节点是除法左子树是减法右子树是另一个减法末端是最小不可分的价格变量。这种树形编码天然支持不同长度的表达式交叉和变异也能在子树层面操作不会破坏公式的完整性。在选股场景中因子公式通常作用于横截面数据。也就是说同一棵表达式树会被应用到每一只股票的时间序列数据上输出一个数值作为该股票在当前截面的因子值。公式里的变量都应该被定义为“截面数据上的算子”比如rank(close)表示当前截面所有股票的收盘价排名而不是单纯的价格。这样处理的好处是避免市场整体涨跌对因子值的影响让适应度函数更关注相对强弱。表达式树的节点类型一个完整的 GP 节点系统包含两类节点函数节点和终端节点。函数节点接收子节点计算结果并做运算终端节点负责取数。常见的因子表达式终端如下表终端类型示例说明行情字段open,close,high,low,volume取个股日线数据衍生数据return_5,vol_20,amount预计算好的动量或波动率特征截面处理rank(x),zscore(x)在当期截面做排序或标准化常量0.01,5,0.5用于避免除零或调整尺度要特别注意终端的类型一致性。如果函数集的add节点定义只接受两个数值输入那么终端和子树输出就必须都是数值型。一旦加入rank这种返回截面排序值的操作交叉和变异时就需要额外的类型检查否则很容易生成语义错误的个体。2.2 函数集与终端集的选择决定了搜索空间边界函数集是 GP 的“语法边界”。如果函数集里只有加减乘除那么无论进化多少代都不可能生成包含log或max的因子。反过来函数集太复杂会急剧扩大搜索空间让种群很难收敛到有效区域。实际项目中我一般把函数集分为基础组和增强组基础组add,sub,mul,div。其中div必须做保护处理通常把除数加上一个极小常量避免零值导致异常。增强组log,sqrt,max,min,rank,zscore。这些函数能表达非线性关系和截面排序特征但会显著增加公式复杂度建议在中期再加入。终端集的设计同样关键。直接把原始high、low塞进去并没有错但会让搜索过程花大量代数去组合出“有效价差”这类本就该预处理的量。一种更高效的做法是把收盘价、开盘价、成交量、振幅、换手率等先做一遍标准化和去极值然后对每个字段提供若干时间窗口的衍生特征。这样终端集的可解释性强搜索空间也会小很多。2.3 适应度函数RankIC 是比收益率更稳的训练信号适应度函数是 GP 进化过程的“指挥棒”。如果直接用因子值和未来收益率的相关系数IC作为适应度那么异常值的影响会被放大而且市场风格剧烈切换时同一批个体会出现得分大幅震荡。相比之下RankIC因子排名与收益排名的 Spearman 相关系数对单调关系更敏感同时天然抵御极端值。另一个常见选择是“ICIR”即 IC 的均值除以标准差。它包含了“预测稳定性”的信息但对于单次评估而言需要回看过去很多期数据计算成本较高。我的做法是在每一代适应度评估时使用最近 12 个月月度截面数据的平均 RankIC并减去一个复杂度惩罚项fitness mean(rankic_t) - lambda * complexity(expr)其中complexity可以定义为树的节点总数或最大深度lambda是惩罚系数。这样进化过程会偏向“简单且稳定有效”的公式而不是一味追求训练期的高 IC。3. 用 DEAP 从零实现 GP 选股因子挖掘的最小可运行流程3.1 函数集、终端集与个体生成DEAP 是 Python 生态里最常用的进化计算框架内置gp模块支持树形个体的交叉、变异和编译。下面这段代码定义了一个面向选股因子的 GP 搜索问题import operator import numpy as np import pandas as pd from deap import base, creator, tools, gp # 定义适应度最大化 RankIC - 复杂度惩罚项 creator.create(FitnessMax, base.Fitness, weights(1.0,)) creator.create(Individual, gp.PrimitiveTree, fitnesscreator.FitnessMax) # 定义函数集 pset gp.PrimitiveSet(MAIN, arity6) # arity6 表示终端节点占 6 个输入位置这里暂用 6 个预计算特征 pset.addPrimitive(operator.add, 2) pset.addPrimitive(operator.sub, 2) pset.addPrimitive(operator.mul, 2) pset.addPrimitive(operator.truediv, 2) pset.addPrimitive(np.log, 1, namelog) pset.addPrimitive(np.sqrt, 1, namesqrt) pset.addPrimitive(gp.if_then_else, 3, nameif_then_else) # 定义终端6 个特征 2 个常量 pset.addEphemeralConstant(rand_const, lambda: round(np.random.uniform(0.01, 2.0), 2)) # 设置输入名称后续编译时按顺序传入数据 pset.renameArguments(ARG0feature_0, ARG1feature_1, ARG2feature_2, ARG3feature_3, ARG4feature_4, ARG5feature_5)这段代码的关键点是if_then_else这类三参数函数。它让进化算法具备“条件分支”能力可以生成类似“如果成交量大则用动量否则用反转”的规则式因子。rand_const是临时常量每生成一个新个体时都会随机产生一次这比固定常量集合更容易找到合适的数值阈值。3.2 因子计算与 IC 评估的无循环写法GP 个体的评估必须高效否则几百个个体、几十代进化会非常耗时。一个常见的优化方法是将全部股票的因子值一次性算出一个向量然后在下一个月度截面上做 Spearman 相关。先准备一个特征矩阵X和未来收益向量ydef evaluate_individual(individual, X, y, complexity_penalty0.001): func gp.compile(individual, pset) with np.errstate(divideignore, invalidignore): factor np.nan_to_num(func(*[X[:, i] for i in range(X.shape[1])])) if np.std(factor) 1e-8: return (0.0,) # 计算 Spearman 相关 from scipy.stats import spearmanr rank_ic, _ spearmanr(factor, y) # 复杂度惩罚节点数越多惩罚越大 complexity len(individual) return (rank_ic - complexity_penalty * complexity,)这里把六个特征数组直接作为位置参数传给funcDEAP 会按ARG0到ARG5的顺序绑定。np.nan_to_num用来兜底因对数、根号产生的 NaN 值。逻辑上如果因子方差接近零说明公式退化成了常量适应度直接给 0避免浪费进化代数。3.3 进化主循环与日志输出有了评估函数就可以搭建种群进化主循环。一般我会设置种群大小为 300进化代数为 30 到 50 代并在每一代结束保存最优个体toolbox base.Toolbox() toolbox.register(expr, gp.genHalfAndHalf, psetpset, min_2, max_5) toolbox.register(individual, tools.initIterate, creator.Individual, toolbox.expr) toolbox.register(population, tools.initRepeat, list, toolbox.individual) toolbox.register(evaluate, evaluate_individual, XX_train, yy_train) toolbox.register(select, tools.selTournament, tournsize3) toolbox.register(mate, gp.cxOnePoint) toolbox.register(mutate, gp.mutUniform, exprtoolbox.expr, psetpset) pop toolbox.population(n300) hof tools.HallOfFame(1) for gen in range(40): offspring algorithms.varAnd(pop, toolbox, cxpb0.6, mutpb0.3) fits map(toolbox.evaluate, offspring) for ind, fit in zip(offspring, fits): ind.fitness.values fit pop toolbox.select(offspring pop, klen(pop)) hof.update(pop) best hof[0] print(fgen {gen}: best fitness {best.fitness.values[0]:.4f}, expr {best})genHalfAndHalf是生成初始种群最稳妥的方法它混合了满树和随机树避免一开始所有个体深度都相同。cxOnePoint是子树交叉能保留父代中的有效子表达式。选择时让父代和子代一起参与锦标赛保证精英不会丢失。3.4 关键参数速查表参数名推荐范围说明种群大小200 - 500过小容易早熟过大单代评估过慢进化代数30 - 80一般 40 代后收益递减交叉概率 cxpb0.5 - 0.8负责探索不宜过低变异概率 mutpb0.1 - 0.4负责局部扰动过高会破坏好结构树最大深度5 - 8深度超过 8 的公式极易过拟合锦标赛大小3小值保持多样性大值加快收敛实际操作中我建议先用群体大小 100、代数 20 跑通流程确认无报错后再加大参数。因为 GP 搜索本身有随机性同一个项目跑两次结果不会完全一样关键不是某一次的最优值而是多次实验中最优个体的分布是否集中在某几类表达结构上。4. 挖掘后的因子验证分层回测、IC 衰减与正交化4.1 月度截面 RankIC 计算GP 输出的因子公式只是“半成品”必须经过独立数据上的验证才能进入因子库。验证第一步是时序 IC 检验。把样本数据按月份分组在每个月的截面计算因子值和下月收益的 Spearman 相关得到一条 IC 序列factor_values evaluate_all_stocks(best_expr, feature_data) ic_series [] dates sorted(set(monthly_dates)) for dt in dates: mask monthly_dates dt if mask.sum() 30: continue f factor_values[mask] r forward_return[mask] ic, _ spearmanr(f, r) ic_series.append((dt, ic)) ic_series pd.DataFrame(ic_series, columns[date, ic])这里要特别注意数据对齐。GP 挖掘时使用的特征必须是 T 日收盘后能拿到的基础数据而forward_return必须是 T1 日到 T1 个月后的收益。如果在训练集和验证集里错配了一天IC 会整体虚高实盘却无法复现。常见的做法是在训练和验证区间之间留至少一个月的空白缓冲期防止重叠。IC 序列出来后看两个指标IC 均值是否显著大于 0.02ICIR 是否大于 0.5。如果一个因子在某个年份 IC 很高但其他年份接近零说明它捕捉的是阶段性的风格收益不适合作为长期 alpha 来源。4.2 分层回测构造IC 只能证明“预测方向和强弱”无法说明因子在可交易组合里的表现。分层回测的做法是每个调仓日按因子值从大到小分成 10 组计算每组等权组合的下一期收益然后观察多空组合的累计净值曲线。def layer_backtest(factor, forward_return, dates, n_layers10): df pd.DataFrame({ date: dates, factor: factor, fwd_ret: forward_return }) results {} for dt in sorted(set(dates)): cross df[df[date] dt].dropna() if len(cross) 2 * n_layers: continue cross[layer] pd.qcut(cross[factor], n_layers, labelsFalse, duplicatesdrop) for layer in range(n_layers): group cross[cross[layer] layer] if layer not in results: results[layer] [] results[layer].append(group[fwd_ret].mean()) return pd.DataFrame(results).mean()合理的分层结果应该是 Layer 9最高因子值组的收益高于 Layer 0最低因子值组并且中间层基本单调。如果出现最高组和最低组都很高、中间组偏低的 U 型曲线说明因子更多是在做“极端值识别”而不是单调排序这时要做去极值处理。4.3 因子正交化与增量检验即使新因子自身 IC 很高也可能只是已有因子市值、反转、波动率的组合重排。增量检验的常见做法是先把旧因子对新因子做线性回归用残差替代原因子再测 ICimport statsmodels.api as sm def orthogonalize(new_factor, old_factor): X sm.add_constant(old_factor) model sm.OLS(new_factor, X).fit() return model.resid残差 IC 如果依然显著说明新因子提供了增量信息。如果残差 IC 迅速降到零附近就不用浪费因子库容量了。需要留意的是正交化会破坏公式的原始数值尺度后续使用残差因子时要重新做截面标准化和去极值否则在模型里与其他因子拼接时会有量纲问题。4.4 常见陷阱幸存者偏差与清洗顺序最容易被忽略的问题是股票池的幸存者偏差。挖掘和验证时如果直接用当前 A 股列表那么已经退市的股票在整个训练区间内都缺失IC 会被系统性抬高。应该用各调仓日的可交易股票池并回补退市股票在退市前的行情数据。另外所有预计算特征必须在进入 GP 前完成去极值和标准化但不要在挖掘流程内部重复做这些操作否则 GP 可能学习到对“标准化参数”的依赖换一个样本期就会失效。5. 过拟合控制与鲁棒性验证的四个实用技巧5.1 在适应度里加入复杂度惩罚项上一章已经提到复杂度惩罚这里是关键技巧惩罚系数不要选固定值而是按“采样代数衰减”的方式设置。早期种群个体普遍简单惩罚系数可以设小一些鼓励探索到了 20 代以后种群普遍变复杂再把系数提升强制收敛到简洁结构。实现上只有一行改动lambda_current 0.0005 if gen 20 else 0.002这种动态惩罚比固定值更符合 GP 进化的实际曲线能显著减少最终公式中出现重复冗余子树的情况。5.2 用滚动窗口重放代替单次训练一次 30 代的进化只能得到“某个时间断面下的最优因子”。我建议把训练过程复制到 3 到 5 个滚动窗口上每个窗口使用不同起止日期最后只保留在多数窗口中都重复出现的高频子树。具体做法是把所有最优个体转成前缀表达式对公共子树做模式计数保留出现次数最多的 3 个结构再做一次因子拼接。5.3 对输入特征做聚类去冗余如果终端集里放了大量相似特征比如不同窗口的动量GP 很容易陷入局部最优。提前用相关性聚类把特征压到 6 个以内每个类别只保留一个代表特征能有效降低搜索难度。这个步骤虽然牺牲了终端集的丰富性但换来的是进化过程的稳定和结果的更有可解释性。5.4 探索“基础因子 vs 复合因子”的 alpha 稳定性最后一个技巧是把 GP 挖出的因子再和简单的规则因子做对比统计它们在 5 个不同市场环境下的表现。如果复合因子只在单边上涨行情中有效而基础因子在不同环境下表现更平稳说明搜索过程更多是在拟合历史行情。此时可以调整适应度函数加入风格中性化的约束比如让因子和市值收益的相关性趋近于零帮助进化过程避开过于依赖风格暴露的公式。本文还有配套的精品资源点击获取
返回列表