news 2026/10/18 8:26:56

遗传规划自动挖因子:从公式进化到可解释Alpha策略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
遗传规划自动挖因子:从公式进化到可解释Alpha策略

简介:基于遗传编程的阿尔法因子生成算法Python代码包,面向量化投资研究人员、因子策略开发者和金融工程学习者。该工具可有效缓解传统阿尔法因子被广泛使用后逐渐失效的问题,适用于多因子选股、行业轮动及个股收益预测等场景。其核心是通过符号回归自动搜索与证券收益显著相关的因子表达式,以证券收益为因变量、用户自定义特征为自变量,并采用股票代码与时间序列构成的二维数据框输入,贴近实际投资逻辑。压缩包共8个文件,7个Python脚本和1个Markdown说明文档,涵盖遗传算法核心、函数构建、适应度评估、工具函数及演示模块,整体仅28KB,模块化清晰,便于定位逻辑和二次扩展。配合演示脚本可跑通因子进化全流程,也支持替换自有数据生成个性化阿尔法因子;已有700人浏览学习,适合从手工因子转向自动化挖掘的实践者参考和复现。

1. 遗传规划挖因子:把找因子从拍脑袋变成自动化搜索

做过多因子策略的人都有体会:估值、动量、反转这些显式因子早被研究透了,上线没几个月 alpha 就衰减得不成样子。这个项目解决的正是这个问题——用遗传规划(Genetic Programming)自动搜索新的阿尔法因子。它不是让你手动挑因子,而是构造一组随机公式当作初始种群,用适应度函数告诉算法哪些公式和证券收益的关联更强,再通过交叉与变异一代代进化,最后输出一个可读的数学表达式。和传统遗传规划不同,它直接消费时间序列数据,自变量可以是你能想到的任何和收益相关的变量,因变量就是证券收益。适合两类人:想做因子补充的量化研究者,以及想找一个能落地到 DataFrame 上的完整 Python 实现的开发者。解压之后按 demo 跑一遍,很快能看到第一批自动生成的因子长什么样。

2. 架构拆解:从 genetic.py 到 fitness.py 的因子进化链路

2.1 目录结构:八个文件的职责分配

解压后是一个标准的 Python 包结构,文件不多,但每个模块的边界划得很清楚。我第一次打开的时候大概花了十分钟就把整个流程读通了,因为它的依赖方向是单向的:genetic.py 调 fitness.py 和 _program.py,demo.py 调 genetic.py,utils.py 只负责数据。

文件职责
genetic.py进化算法主入口,种群初始化、选择、交叉、变异的主循环
_program.py公式树的数据结构,定义个体(一棵树)怎么创建、怎么转成表达式字符串
functions.py算子函数集,包含加减乘除、对数、排名等基础运算,以及保护性实现
fitness.py适应度评估,计算 IC、ICIR 等指标,决定一个公式算不算好因子
utils.py数据加载与预处理工具,把原始面板数据整理成算法能吃的格式
demo.py最小可运行示例,把数据准备和进化流程串起来
README.md使用说明
init.py包初始化,导出公开接口

依赖关系很简洁,没有外部数据库,没有网络调用,整个项目是一个纯内存运算的离线脚本集合。这也意味着你拿到手之后,跑起来几乎不需要额外适配,只要把数据喂对就行。

2.2 主流程:一次完整的因子进化

整个算法的主循环是标准的遗传规划流程:初始化种群、评估适应度、选择父代、交叉变异生成子代、精英保留,然后重复。以我对这类实现的经验,核心骨架基本长这样:

# 简化后的主循环骨架(细节以解压后的源码为准) for gen in range(max_generations): scores = fitness.evaluate(population, panel_df, target) # 给每个个体打分 elites = keep_best(population, scores, n_elite=2) # 精英保留 parents = tournament_select(population, scores, k=3) # 锦标赛选择 children = [] for p1, p2 in zip(parents[::2], parents[1::2]): c1 = crossover(p1, p2, max_depth=max_depth) # 子树交换 c1 = mutate(c1, func_set, max_depth=max_depth) # 随机变异 children.append(c1) population = elites + children[: pop_size - len(elites)] best_program = population[scores.argmax()]

这里每一步都值得展开说。锦标赛选择是每轮随机抽 k 个个体、留下适应度最高的那个当父代,k 越大选择压力越大,但也越容易早熟;crossover 交换两颗树的子树,让新个体继承旧个体的有效结构而不是完全从零开始;mutate 会随机替换树中的某个节点或者整棵子树,保证种群的探索性。精英保留的意义在于防止当前最好的公式在交叉变异中被破坏,我一般把 n_elite 控制在 2 到 4 个,太少保不住最优解,太多则种群多样性下降。

参数层面的默认配置,常见的取值范围大致是种群 200 到 500 个个体,进化 20 到 50 代,树深度限制在 4 到 6。这几个参数直接决定运行时间:种群翻倍,单代评估时间近似翻倍;深度加深,单棵树的运算量指数级增长。做因子挖掘并不是树越深越好,金融数据里深度超过 7 的表达式基本就开始过拟合了,出来的公式在样本外往往跑不动。

2.3 数据契约:为什么输入必须是二维面板

这个项目最值得注意的设计是数据组织方式。它推翻了机器学习里习惯的 X, y 二维表,要求第一维是股票代码、第二维是时间。换句话说,自变量是一个面板数据,行为股票代码,列为交易日期:

import pandas as pd import numpy as np # 行:股票代码;列:交易日期 panel = pd.DataFrame( np.random.randn(500, 1200), index=[f"stock_{i}" for i in range(500)], columns=pd.date_range("2020-01-01", periods=1200) ) # 下一期收益作为因变量,注意 shift 必须沿列方向移动 future_return = panel.shift(-1, axis=1)

为什么这样设计?因为遗传规划生成的表达式会同时包含横截面操作(比如 rank 排名)和时间序列操作(比如过去 20 日的动量均值)。如果数据只是一列,算法只能在单一股票上做时间维度的运算,根本用不上市场的横截面信息;而因子投资里最核心的正是横截面上的相对强弱——同一时刻,哪些股票在目标特征上排名靠前。面板结构让公式树每个节点都能拿到全部股票的完整时间序列,既能在时间上滚动,又能在横截面上比较。

这里和传统遗传规划的区别也体现在数据契约上:传统符号回归的输入输出都是数值向量,这里每个自变量本身就是一张面板,因变量是下一期的收益面板。适应度函数在计算时会逐日做横截面排名,再对时间序列求平均,这个细节直接决定了后面 fitness.py 的写法。

3. 核心实现走读:公式树、遗传算子与时间序列适应度

3.1 genetic.py:种群初始化与进化主循环

genetic.py 是算法的心脏。种群里的每个个体都是一棵公式树,树的内节点是算子,叶子节点是特征名或常量。初始化种群时常见的做法是 half-and-half:一半用 full 方法生成(所有叶子深度相同),一半用 grow 方法生成(各分支深度随机)。这样初始种群既有结构规整的个体,也有形态多样的个体,避免一开始就陷入单一形状的搜索空间。

# _program.py 中公式树节点的简化实现 class Node: def __init__(self, value, arity=0, children=None): self.value = value # 算子符号、特征名或常量 self.arity = arity # 子节点数量,叶子节点为 0 self.children = children or [] def evaluate(self, panel): # 叶子节点直接取面板里对应特征列 if self.arity == 0: return panel[self.value] # 算子节点先递归算出子树结果,再套用算子 args = [c.evaluate(panel) for c in self.children] return FUNC_TABLE[self.value](*args)

这段代码的巧妙之处在于 evaluate 的输入输出都是面板:每个子节点返回一张 (股票 × 时间) 的 DataFrame,算子节点对这张表做向量化运算,所以表达式无论嵌套多深,最后得到的因子矩阵维度始终和收益面板对齐。这也是为什么整个项目只需要 numpy 和 pandas 就能跑的底层原因。

交叉操作一般发生在树的中间节点上:随机选出父代 A 的某个子树,替换成父代 B 的某个子树。替换深度会被 max_depth 卡住,超出就重新选交叉点。变异则有三种常见类型:点变异(把某个算子替换成另一个算子)、子树变异(随机生成一棵新子树替换旧子树)、hoist 变异(把子树里某个分支提升到更高的位置)。一个合格实现的遗传算子不会只做一种,因为金融数据里公式结构微小改变可能带来完全不同的因子表现。

3.2 functions.py:保护性算子和函数集设计

金融数据的一个常见麻烦是除零和取对数,价格、收益率、成交量这些字段经常出现零值或负值。原生除法和 log 会把整棵树的评估结果污染成 NaN,适应度计算直接崩掉。所以 functions.py 里一定会有保护性算子,这是我在读这种项目时会特别留意的细节。

# functions.py 中常见的保护性实现 import numpy as np def safe_div(a, b): # 当分母绝对值过小时,用 1 替代,避免结果变成无穷或 NaN return np.where(np.abs(b) < 1e-6, 1.0, a / b) def safe_log(x): # 对非正数做截断,保持输出数值稳定 return np.where(x > 1e-10, np.log(x), 0.0) def rank(x): # 横截面排名:按列操作,每列是一个交易日的截面数据 return x.rank(axis=0)

safe_div 的阈值 1e-6 不是随便定的:太小了仍然会有数值溢出的风险,太大会把正常的小分母全部扭曲掉。rank 算子在这里是把整张面板的每个时间截面分别排名,输出的是排名分位数而不是原始数值,这样能让公式对异常值和量纲不敏感——两个股票,一个价格是 5 元一个是 500 元,直接相减没有意义,排名之后才有横截面可比性。函数集里一般还会放 ts_mean、ts_delta 这类带窗口的时间序列算子,窗口长度通常由常量子代随机生成,这也是项目能产出动量和反转类因子的原因。

3.3 fitness.py:用截面秩相关评估公式好坏

适应度是因子挖掘的灵魂。源码里对单个公式的评估,核心指标大概率是 IC(Information Coefficient)和 ICIR。IC 衡量的是因子值和未来收益在横截面上的相关强度,ICIR 则是 IC 的均值除以标准差,代表因子有效性的稳定性。一个因子如果 IC 高但波动大,实际使用体验往往不如 IC 平稳的因子。

# fitness.py 中 rank IC 的常见计算方式 def rank_ic(factor_panel, target_panel): # 每个交易日对因子值和未来收益分别做横截面排名 factor_rank = factor_panel.rank(axis=0) target_rank = target_panel.rank(axis=0) # 逐日计算因子排名与收益排名的相关性 ic_series = factor_rank.corrwith(target_rank, axis=0) # 返回平均 IC 与 ICIR return ic_series.mean(), ic_series.mean() / ic_series.std()

注意两个细节:第一,为什么用秩相关而不是皮尔逊相关。收益分布厚尾,极端涨跌会拉高线性相关的假信号,Spearman 秩相关只关心排序关系,对极端值天然免疫。第二,axis=0 在这里是列方向操作,对应的是每一列一个交易日的横截面排名;整个 DataFrame 行是股票、列是日期,所以按列排名就是在对每个时点上的全市场股票排序。这样算出来的 IC 序列长度为交易日数,再对其求均值和标准差,就得到一个既能衡量强度又能衡量稳定性的标量。

这种适应度设计的取舍在于:它只看横截面排序,忽略了因子值本身的分布形态。一个公式即使整体偏移很大,只要排序对,IC 照样高。这既是优点也是风险,后面避坑章节会提到这个问题的实际后果。

4. 跑通 demo:数据准备、运行命令与输出解读

4.1 环境准备:只需要 numpy 和 pandas

跑这个项目的门槛很低。我建议先建一个干净的虚拟环境,避免污染系统 Python:

python -m venv .venv # Windows 下 .venv\Scripts\activate # macOS / Linux 下 source .venv/bin/activate pip install numpy pandas

依赖只有这两个,说明源码里的所有算子都是基于 DataFrame 向量化实现的,没有用到 sklearn、torch 之类的重型库。装完之后直接运行python demo.py,等它跑完就能看到输出。如果当前环境的 pandas 版本比较新,rolling 相关算子接口有过调整,遇到报错时先去看 pandas 的变更日志,而不是急着改源码。

4.2 demo.py 的调用方式与参数说明

demo.py 做的事情是把数据准备和进化流程串起来。以解压后的实际代码为准,调用形态大致是这样:

# demo.py 的典型调用方式 from genetic import GeneticAlpha from utils import load_panel # 读取面板数据,要求行是股票代码、列是日期 panel = load_panel("adj_close.csv") # 用收益率序列作为目标:当日价格 / 前一日价格 - 1 target = panel.pct_change().shift(-1, axis=1) model = GeneticAlpha( pop_size=300, max_generations=30, max_depth=5, func_set=["add", "sub", "mul", "safe_div", "rank", "ts_mean"], ) model.fit(panel, target) print(model.best_program())

参数含义直接看名字就能猜个大概,但实际调参有几个经验点。pop_size 决定每代能探索多少公式,太小容易早熟,太大跑得慢;max_generations 决定总进化轮数,一般看适应度曲线是否在最后几代还在上升;max_depth 是公式树的深度上限,控制表达式复杂度。func_set 里算子越多,搜索空间越大,但也会引入更多无意义的组合;我一般会从基础四则运算加 rank、ts_mean 起步,跑通之后再逐步加算子。

另一个重要参数是目标收益的计算。代码里用pct_change()算的是当日相对前一日的收益率,shift(-1, axis=1)让它变成下一期收益。这里有个关键约束:公式中用到的所有特征必须只依赖 T 时刻及之前的数据,而目标必须是 T 时刻之后的收益,否则就是未来函数。demo 数据里特征列是复权后的价格,天然满足这个约束,但你自己接数据时一定要确认。

4.3 输出解读:最优公式、适应度曲线与因子矩阵

运行结束后,输出一般包含三块:最优公式的表达式、每代的适应度记录、以及最优因子在样本数据上的因子矩阵。最优公式长什么样?以我的经验,输出类似:

best program: rank(ts_mean(adj_close, 20)) / rank(adj_close) train IC: 0.038, ICIR: 0.46

第一行是遗传规划抠出来的表达式,语义上可以解读成"过去 20 日均价与当前价格的比值在全市场的排名",这本质上是一个带排名的动量因子。第二行是它在训练数据上的表现:IC 在 0.03 到 0.05 之间是量化因子里的常见水平,ICIR 大于 0.3 说明有效性还算稳定。如果 IC 超过 0.08,先别高兴,大概率是数据泄漏或者过拟合,后面避坑章节会展开。

适应度曲线看的是收敛情况:如果前几代 IC 快速上升,后面趋于平稳,说明搜索是有效的;如果曲线一直在低位抖动,考虑加大种群或者增加算子。因子矩阵是最终产物,shape 应该是 (股票数, 日期数),可以直接存成 parquet 或 csv 进因子库,后续做分层回测或者相关性分析。

5. 避坑指南:因子搜索里的翻车现场和排查手段

5.1 数据维度搞反,后面全是白算

现象:demo 换成自己的数据后,报错提示行列对不齐,或者训练出来的因子矩阵转置之后才和收益对齐。原因:把面板数据传成了行是日期、列是股票代码。这个项目的数据契约是行=股票代码、列=日期,和很多量化平台导出的数据方向正好相反。解决:加载数据后先打印data.shape和data.index[:2]、data.columns[:2],确认第一维是股票代码再往下走。另一个更隐蔽的情况是收益率计算用的shift方向写反了,导致目标里混进了过去收益,IC 看起来正常但逻辑完全错误。

5.2 进化过程失控:深度爆炸和早熟收敛

现象:跑着跑着内存占用越来越高,或者进化到第 10 代以后种群里的公式全部长得一样。原因:深度爆炸是因为交叉变异时没有严格执行 max_depth 限制,子树替换后某些分支嵌套越来越深,表达式树膨胀成"深不见底的葡萄藤";早熟收敛则是选择压力太大、变异率太低导致的,锦标赛规模 k 设得过大,几代之后劣质个体全被淘汰,剩下个体高度相似。解决:在交叉点选择时检查子树深度,超出限制就重新选点;同时把 tournament_size 从 5 调到 2 到 3,把 mutation_prob 从 0.1 上调到 0.2 左右。深度爆炸还有一个通用处理手段,就是对个体长度加惩罚项——适应度减去一个随树节点数增长的惩罚值,逼着算法偏好简洁公式。

5.3 样本内 IC 很高,样本外直接失效

现象:训练集上跑出来的公式 IC 0.07、ICIR 0.8,看起来很漂亮,换到最近一年的数据上回测,多空组合净值和躺平没什么区别。原因:遗传规划强大的搜索能力会找到那些"恰好"拟合历史噪声的公式,尤其是人口规模大、进化代数多的时候,过拟合几乎是必然事件。另一个常见来源是公式里混入了时间相关的常数——比如进化过程中生成了一个只依赖某个固定日期的特征,在训练段内表现稳定,出了训练段就失效。解决:最有效的方法是把数据按时间切段,前 80% 做进化搜索,后 20% 做样本外验证,只有样本外 IC 仍然显著的公式才进入候选池。其次是多看 ICIR 而不是只看 IC:IC 可以靠运气高,ICIR 高说明的是多头和空头分组在市场各个时期都稳定分化,这才是可交易的因子——这也是 fitness.py 里为什么同时返回两个指标的原因。

6. 二次开发:把 Genetic-Alpha 接进你自己的因子流水线

6.1 值得动手改的三个切入点

第一个切入点是 functions.py 的算子扩展。默认函数集能挖出价格和收益类表达式,但换手率、成交额、波动率这些衍生特征需要自己定义算子。我一般会加ts_delta(x, n)表示过去 n 日变化量,wma(x, n)表示加权移动平均,加完算子之后放进 func_set 参数就能参与进化。

第二个切入点是 fitness.py 的评估函数。如果你做的是多空组合而不是纯横截面选股,可以把适应度从 rank IC 改成"多空组合夏普比率"——按因子值每日分组,做多 top 10%、做空 bottom 10%,计算组合净值序列的夏普。这样进化出来的公式会直接优化可交易收益,而不是优化相关关系,减少"IC 高但交易成本吃掉收益"的落差。

第三个切入点是并行评估。纯 Python 的树评估在大种群下会很慢,一个低成本的改造是用 joblib 把每代种群评估并行化,按 CPU 核数分块。改造时注意适应度函数要能被 pickle,所以算子定义要放在模块顶层,不能写在 fit 方法里。我踩过这个坑,自定义算子在局部函数里的时候并行直接报错。

6.2 改造后怎么验证

每次改动都要走一遍验证路径:先跑 demo 确认没改坏基线,再跑你的历史数据,最后把输出因子矩阵存下来做分层回测。分层回测的做法是把每天按因子值从小到大分成五组,画五组累计收益曲线,如果 top 组和 bottom 组没有拉开差距,说明因子有效性不足,这时候回看适应度函数和数据预处理,问题多半出在那里。

记得我第一次拿它批量扫因子时,直接上了全市场 3000 只股票,跑了两个多小时,出来的最优公式竟然是个常数。排查半天才发现目标收益的列名传错了,适应度函数在算一个全部相等的因子和收益的相关,IC 的方差为零所以返回了一个异常值。从那以后我每次跑新数据都会先打印因子矩阵的describe()看方差,确认它不是常数再进入进化主循环。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 8:26:40

生成式UI与LLM应用工程霸榜:开源项目如何解决AI落地最后一公里?

今天&#xff08;2026年2月12日&#xff09;刷一遍开源社区热榜&#xff0c;有个挺明显的感受&#xff1a;榜单上围绕“模型微调”“推理加速”的纯底层项目少了&#xff0c;取而代之的是生成式UI和LLM应用工程两个方向几乎平分秋色。这个信号对正在做AI应用产品化的人&#xf…

作者头像 李华
网站建设 2026/10/18 8:25:35

虚幻引擎项目资源拆解:截图驱动的调试与协作实践

简介&#xff1a;这是一份基于虚幻引擎的开源项目合集&#xff0c;包含多个可直接打开浏览的工程实例&#xff0c;适合游戏开发者、技术美术及三维可视化学习者参考。包内523个文件以uasset资源资产为主&#xff0c;辅以umap地图文件、ini配置和uproject工程文件&#xff0c;覆…

作者头像 李华
网站建设 2026/10/18 8:25:04

ReWOO实战:解耦推理与观察,让LLM Agent告别token焦虑

做 LLM Agent 的朋友&#xff0c;大概都遇到过这种尴尬&#xff1a;任务明明不复杂&#xff0c;但跑着跑着上下文就爆了。尤其是用 ReAct 那套“边想边做”的循环&#xff0c;每调一次工具&#xff0c;整个对话历史都要重新喂给模型一遍。当时我算了一笔账&#xff0c;一个带搜…

作者头像 李华
网站建设 2026/10/18 8:22:40

轻量级PHP积分商城系统:高并发兑换码生成与核销闭环

简介&#xff1a;这是一套面向PHP开发者与中小型电商项目技术负责人的开源积分商城系统源码&#xff0c;专为构建积分兑换类平台&#xff08;如会员积分商城、企业福利兑换站、营销活动赠品发放系统&#xff09;而设计&#xff0c;兼顾功能完整性与二次开发友好性。资源包共973…

作者头像 李华
网站建设 2026/10/18 8:22:33

麻雀搜索算法优化Transformer-GRU的电力负荷预测实战

简介&#xff1a;面向负荷数据回归预测的Matlab实现方案&#xff0c;采用麻雀搜索算法&#xff08;SSA&#xff09;优化Transformer-GRU组合模型&#xff0c;覆盖数据预处理、模型训练、预测评估等核心环节&#xff0c;适合计算机、电子信息工程、数学等专业学生完成课程设计、…

作者头像 李华
网站建设 2026/10/18 8:20:24

广州租赁合同没到期房东要求提前收房怎么办 租客的救济途径

广州租客遇到房东在租期内要求提前收房&#xff0c;可以拒绝并要求继续履行合同&#xff0c;也可以同意搬离但主张违约金和实际损失赔付。房屋在这期间被出售的&#xff0c;不影响租赁合同效力&#xff0c;租客可以住到租期届满。应对的关键是留好合同、付款和沟通证据&#xf…

作者头像 李华