ARTICLE DETAIL

资讯详情

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

遗传规划自动挖掘阿尔法因子:多因子策略代码包实战

遗传规划自动挖掘阿尔法因子:多因子策略代码包实战 简介这份资源围绕遗传规划算法在多因子投资策略中生成阿尔法因子展开面向具备一定Python基础、希望自主挖掘有效因子的量化投资者与研究者。它借助符号回归技术先构建一组简单随机公式来刻画自变量与证券收益之间的关系从而预测新数据帮助使用者摆脱传统因子逐渐失效的困境。压缩包共8个文件以7个py脚本和1个md说明文档为主整体约28KB代码按遗传算法主流程、适应度评估、函数集与工具模块拆分结构清晰便于按模块阅读与二次开发。该包特别支持时间序列数据要求可变调整价格以二维数据框组织第一维为股票代码、第二维为时间这与原始遗传规划算法有明显差异demo.py则提供了测试运行入口。目前已有699人学习下载适合想快速上手遗传阿尔法、理解因子生成与回测思路的读者参考。1. 遗传规划生成阿尔法因子一份能跑通的多因子策略代码包做多因子策略的人大多经历过这个阶段手工构造的动量、估值、波动率因子回测出来 IC 一年比一年低因为大家都在用同一批因子拥挤度上来了自然失效。想找新因子靠人工试错效率太低。这份 Genetic-Alpha 代码包解决的正是这个问题——它用遗传规划Genetic Programming做符号回归自动搜索自变量与证券收益之间的非线性公式把「找因子」这件事从手工活变成可搜索的优化问题。包体量不大核心是genetic.py、_program.py、functions.py、fitness.py、utils.py几个模块外加demo.py作为入口。它和经典遗传规划最大的区别是支持时间序列数据输入是一个二维 DataFrame第一维是股票代码第二维是时间。这意味着你可以直接喂面板数据进去让算法在时序维度上评估公式的稳定性而不是只做截面拟合。适合已经写过因子回测、想引入自动化因子挖掘的量化从业者也适合想拿一个完整遗传规划实现来改的 Python 开发者。2. 符号回归怎么变成因子挖掘从树结构到适应度函数2.1 遗传规划的个体表示与算子设计遗传规划的核心是把一个数学公式编码成一棵树。叶子节点是变量比如收盘价、成交量、换手率或常数内部节点是算子加减乘除、对数、排名、滞后等。这份代码里_program.py负责定义这棵树的结构functions.py提供可用的算子集合。为什么用树而不是固定形式的线性组合因为线性组合只能表达「因子 A 乘以权重 w」这种关系而真实市场里因子与收益的关系往往是非线性的比如「换手率的倒数乘以波动率的排名」这种复合结构。树结构天然支持任意嵌套搜索空间大得多。常见做法是给算子加约束避免生成无意义的公式。比如除法算子要防止分母为零对数算子要保证输入为正。这份代码在functions.py里对每个算子做了保护返回 NaN 或裁剪到安全区间避免整棵树因为一个节点出错而报废。# functions.py 中算子定义的典型结构示意 import numpy as np def protected_div(x1, x2): # 分母加一个极小值避免除零同时限制结果幅度 with np.errstate(divideignore, invalidignore): result np.where(np.abs(x2) 1e-6, x1 / x2, 1.0) return np.clip(result, -1e6, 1e6) def ts_rank(x, window20): # 时间序列排名过去 window 期内的分位数 from scipy.stats import rankdata return x.rolling(window).apply(lambda s: rankdata(s)[-1] / len(s), rawTrue)上面两个算子代表了因子挖掘里最常用的两类操作逐元素的算术运算和时序窗口运算。protected_div里的1e-6阈值和np.clip的上下限是可以调的——如果你处理的因子量纲很大比如成交额上限要相应放大否则大量个体被裁剪到边界搜索会失去梯度信息。ts_rank的window参数默认 20对应一个月的交易日改成 60 就是季度排名这个参数直接决定因子的换手率和衰减速度。2.2 适应度函数IC、RankIC 还是多空收益fitness.py是整个算法里最需要你动手改的地方。遗传规划靠适应度函数来评价每个公式的好坏适应度设计错了搜出来的因子就是一堆过拟合的垃圾。这份代码默认的适应度大概率是基于 IC信息系数或 RankIC 的。IC 是因子值与下期收益的截面相关系数RankIC 是秩相关系数。两者的区别在于IC 对极端值敏感RankIC 更稳健。如果你处理的股票池里有大量小市值标的收益分布厚尾严重建议用 RankIC。# fitness.py 中适应度计算的典型逻辑示意 def calc_fitness(factor_values, forward_returns): # factor_values: 二维数组行是股票列是时间 # forward_returns: 同形状下期收益 ic_list [] for t in range(factor_values.shape[1] - 1): f factor_values[:, t] r forward_returns[:, t] mask ~(np.isnan(f) | np.isnan(r)) if mask.sum() 30: # 截面样本太少跳过 continue ic np.corrcoef(f[mask], r[mask])[0, 1] ic_list.append(ic) # 用 IC 的均值和标准差构造适应度均值高且稳定才好 ic_mean np.nanmean(ic_list) ic_std np.nanstd(ic_list) return ic_mean / (ic_std 1e-8) # 类似 ICIR这段代码的关键参数是mask.sum() 30这个阈值。截面股票数少于 30 时算出来的相关系数噪声极大不如直接跳过。另一个重点是最后返回的ic_mean / ic_std这是在模仿 ICIRIC 信息比率的思路——不光要因子有效还要效果稳定。如果你只想最大化单期 IC把返回值改成ic_mean就行但那样搜出来的因子往往在样本外崩得很快。2.3 时间序列数据的对齐与滚动窗口处理这份包和原始遗传规划最大的不同就是支持时间序列。utils.py里应该包含了数据对齐和滚动窗口的工具函数。面板数据做因子挖掘最容易翻车的地方就是时间对齐因子计算用到的数据必须是 t 时刻及之前能拿到的收益必须是 t1 时刻的中间不能有未来函数。# utils.py 中数据准备的典型流程示意 import pandas as pd def prepare_panel_data(price_df, volume_df, forward_period1): # price_df: index 是日期columns 是股票代码 # 构造因子候选变量 ret price_df.pct_change() vol_rank volume_df.rolling(20).apply(lambda x: x.rank().iloc[-1]) # 构造下期收益作为标签 forward_ret price_df.pct_change(forward_period).shift(-forward_period) # 对齐只保留因子和收益都存在的样本 factor_stack ret.stack() forward_stack forward_ret.stack() aligned pd.concat([factor_stack, forward_stack], axis1).dropna() return alignedforward_period1表示预测下一期收益改成 5 就是预测未来一周。这个参数要和你的调仓频率匹配——如果你每周调仓用 1 日收益做标签就是错的。rolling(20)的窗口同理要和因子的预期持有期一致。shift(-forward_period)是把未来收益挪到当前行这是构造标签的标准操作但一定要确认挪完之后没有引入未来数据。3. 从零跑通 demo环境、数据格式与第一次搜索3.1 环境准备与依赖安装这份代码是纯 Python 实现没有重型依赖。我一般会建一个干净的虚拟环境避免和已有的量化库版本冲突。# 创建虚拟环境Linux/macOS python -m venv venv_alpha source venv_alpha/bin/activate # Windows 下激活 # venv_alpha\Scripts\activate # 安装核心依赖 pip install numpy pandas scipy如果你用的是国内网络pip 安装慢的话可以临时指定镜像源这个不多说常规操作。装完之后进到代码目录先别急着跑demo.py先确认 Python 版本。遗传规划里用了不少递归和嵌套结构Python 3.7 以上比较稳3.9 或 3.10 更推荐。3.2 数据格式二维 DataFrame 的构造与检查demo.py里应该自带了一份示例数据或者数据生成逻辑。但你要用自己的数据跑就得先搞清楚输入格式。按照项目说明数据是二维 DataFrame第一维是股票代码第二维是时间。注意这里的「第一维」指的是行索引还是列索引不同实现习惯不一样跑之前先打印一下 shape 确认。import pandas as pd import numpy as np # 假设你有一份价格数据index 是日期columns 是股票代码 # 先转成算法要求的格式index 是股票代码columns 是日期 price_panel price_df.T # 转置 # 检查数据完整性 print(股票数:, price_panel.shape[0]) print(时间长度:, price_panel.shape[1]) print(缺失值比例:, price_panel.isna().mean().mean()) # 如果缺失值太多先做填充或截断 # 常见做法是前向填充但要注意停牌期间的数据不能简单填充 price_panel price_panel.fillna(methodffill, limit5)limit5表示最多前向填充 5 期超过就保留 NaN。这是为了防止停牌很久的股票被错误地填充出连续价格。缺失值比例超过 30% 的股票建议直接剔除否则遗传规划在评估适应度时会被大量 NaN 干扰搜出来的公式可能只对少数样本有效。3.3 运行 demo.py 并解读输出数据准备好之后跑demo.py。第一次跑建议把种群规模和迭代代数调小先确认流程能通。# demo.py 中关键参数示意具体名称以实际代码为准 from genetic import GeneticAlpha ga GeneticAlpha( population_size100, # 种群规模 generations10, # 迭代代数 tournament_size5, # 锦标赛选择规模 crossover_rate0.7, # 交叉概率 mutation_rate0.2, # 变异概率 max_depth4, # 树最大深度 random_state42 ) best_program ga.fit(factor_data, forward_returns) print(best_program.expression) # 打印最优公式 print(best_program.fitness) # 打印适应度值population_size100和generations10是调试用的最小配置跑通之后可以加到 500 和 50。max_depth4限制树的深度防止公式过于复杂导致过拟合。random_state42固定随机种子保证结果可复现——这在因子挖掘里很重要不然你没法判断改进是来自参数调整还是随机波动。跑完之后看best_program.expression如果打印出来是一串你能看懂的公式比如div(ts_rank(close, 20), ts_std(volume, 10))说明流程正常。如果打印出来是一堆乱码或者全是常数检查数据输入格式和适应度函数。4. 避坑与排查遗传规划做因子挖掘的五个血泪教训4.1 现象搜出来的因子 IC 很高但实盘亏钱原因过拟合。遗传规划在样本内搜索能力极强给它足够多的算子和足够深的树它能把噪声也拟合进去。常见表现是样本内 IC 0.15样本外直接变负。解决把数据切成训练集和验证集适应度函数里同时看两段的 IC取最小值或者加权平均。另外限制max_depth不超过 5算子集合里不要放太多高次幂和复杂三角函数。4.2 现象程序跑了几代之后种群多样性骤降所有个体长得一样原因选择压力太大。锦标赛选择的tournament_size设得太大或者精英保留比例太高导致少数个体迅速占领整个种群。解决把tournament_size降到 3 到 5 之间精英保留不超过种群的 5%。另外可以加一个随机移民机制每代随机生成几个新个体替换掉最差的。4.3 现象适应度函数返回 NaN整个搜索卡住原因某个算子对输入数据产生了非法值除零、对数负数、开方负数NaN 沿着树往上传播导致整棵树的适应度无法计算。解决在functions.py里给每个算子加保护返回 NaN 时用一个极差的适应度值代替比如 -999而不是让 NaN 参与比较。同时检查输入数据里有没有无穷大或极端异常值提前做 winsorize。4.4 现象时间序列数据对齐后样本量骤减原因因子计算需要滚动窗口收益需要向前平移两者叠加之后首尾都会损失样本。如果股票池里停牌股多损失更大。解决先确认滚动窗口和预测周期然后从数据两端各截掉相应长度。如果样本量还是不够考虑缩短窗口或者放宽缺失值容忍度。但不要为了凑样本量而用未来数据填充。4.5 现象同样的代码和数据两次运行结果完全不同原因随机种子没固定或者用了多线程/多进程导致执行顺序不确定。解决在GeneticAlpha初始化时传入固定的random_state并且确保所有随机操作选择、交叉、变异都从这个种子派生。如果用了并行评估把并行关掉再验证一次确认是并行导致的不确定性还是算法本身的问题。5. 进阶用法把搜出来的公式变成可回测的因子搜到公式只是第一步真正要用起来得把它转成能接入回测框架的因子值。best_program.expression是一个字符串或者树结构你需要写一个求值函数把原始数据代进去算出每只股票每天的因子值。def evaluate_program(program, data_dict): # program 是遗传规划搜出来的树 # data_dict 是变量名到数据的映射比如 {close: close_df, volume: vol_df} if program.is_leaf(): if program.value in data_dict: return data_dict[program.value] else: return program.value # 常数 else: left_val evaluate_program(program.left, data_dict) right_val evaluate_program(program.right, data_dict) return program.operator(left_val, right_val) # 用法 factor_values evaluate_program(best_program, {close: close_panel, volume: volume_panel}) # factor_values 现在是一个和输入同形状的 DataFrame可以直接送进回测这个递归求值函数的关键是data_dict的键要和公式里的变量名一致。遗传规划在搜索时用的变量名可能是x0、x1这种你需要做一个映射表把x0对应到收盘价、x1对应到成交量。映射错了因子含义就完全变了。另一个进阶技巧是给公式做简化。遗传规划搜出来的树往往有冗余分支比如div(mul(x0, 1), 1)这种。可以写一个简单的符号化简函数把常数乘除和重复子树合并掉让公式更简洁也更容易理解经济含义。验证因子有效性我一般会看三个指标IC 均值、ICIR、多空分组单调性。IC 均值大于 0.03、ICIR 大于 0.5、多空收益单调才算初步可用。然后还要做行业中性化和市值中性化确认因子不是靠暴露在某个风格上赚钱。从那以后我每次搜完因子都会强制走一遍样本外验证和中性化回测不看到这两步的结果绝不敢把公式写进策略里。希望这份代码包和上面的流程能帮你少走点弯路把精力花在真正有价值的因子逻辑上。本文还有配套的精品资源点击获取
返回列表