简介:本资源是一套基于gplearn遗传规划算法的量化投资因子生成框架,面向量化研究者、策略开发者及金融工程方向的学生,解决人工设计因子主观性强、难以覆盖非线性有效因子的问题。包内共54个文件,以py脚本、pyc缓存、csv数据、pickle序列化文件及jpg、pdf、gv等图表文档为主,压缩包约87.02MB,涵盖因子生成、回测评估与可视化分析全流程。项目通过遗传规划自动演化时序因子与选股因子,可捕捉人眼难以识别的复杂模式,并支持股票与期货两个市场。回测模块允许生成因子后立即进行历史模拟交易,检验策略盈利与风险;可视化部分则展示因子表现、风险收益及因子间相关性,帮助理解模型内在逻辑。目前已有46人学习,适合希望快速搭建自动化因子挖掘与验证流程的读者参考。
1. 从手工挖因子到遗传规划自动生成:这套 gplearn 量化框架到底能省多少事
做量化的人大多经历过这个阶段:打开行情数据,手工构造几十个因子,跑 IC、算 IR,然后发现大部分因子在样本外直接失效。问题不在于你不努力,而在于人工枚举因子的速度远远跟不上市场结构变化的速度。这套基于 gplearn 遗传规划算法的量化投资框架,核心思路就是用进化算法自动搜索因子表达式空间,把“人想公式”变成“机器试公式”。它覆盖了从因子生成、回测评估到可视化分析的完整链路,支持股票和期货两个市场。适合两类人:一是已经有因子库但想扩充非线性组合因子的从业者,二是想系统学习遗传规划在量化中怎么落地的新手。下面我从实际拆包和跑通的角度,把这份资源讲透。
2. 遗传规划生成因子的原理与 gplearn 选型:为什么不是随机搜索
2.1 遗传规划在因子挖掘中的核心机制
遗传规划本质上是一种符号回归方法。它把因子表达式表示成一棵语法树,叶子节点是原始特征(开盘价、成交量、换手率等),内部节点是运算符(加减乘除、rank、delay、correlation 等)。每一棵树就是一个候选因子。算法通过选择、交叉、变异三代操作,让适应度高的表达式存活下来,逐代逼近更优解。
和随机搜索相比,遗传规划的优势在于它利用了历史搜索信息。随机搜索每次都是独立采样,而遗传规划会把上一代表现好的子树保留下来,通过交叉组合出新的表达式。这意味着在同样的计算预算下,遗传规划更有可能找到高适应度的因子。但代价是它容易早熟收敛,种群多样性下降后,后续几代基本在重复相似的表达式。所以参数设置里种群大小和变异率非常关键。
常见做法是种群大小设在 1000 到 5000 之间,代数设在 10 到 30 代。代数太多不仅耗时,而且过拟合风险急剧上升。我一般会先用小种群跑 5 代看看因子表达式是否合理,再决定要不要放大规模。
2.2 为什么选 gplearn 而不是自己写遗传规划
gplearn 是 scikit-learn 风格的遗传规划库,API 设计和 sklearn 高度一致,fit、predict、transform 一套走下来很顺。它内置了符号回归和特征生成两种模式,后者正好对应因子挖掘场景。相比自己从零实现遗传规划,gplearn 帮你处理了语法树编码、交叉变异算子、适应度评估并行化这些脏活。
但 gplearn 原生不支持时序算子。量化因子里的 delay、ts_rank、correlation 这些操作,gplearn 默认的函数集里没有。这套框架的一个关键工作就是扩展了 gplearn 的函数集,把时序算子注册进去。如果你直接拿原版 gplearn 跑价量数据,生成的因子基本没有时序信息,IC 会低得让你怀疑人生。
# 扩展 gplearn 函数集,注册时序算子 from gplearn.functions import make_function import numpy as np def _ts_rank(x, window=20): """时序排名:当前值在过去 window 期中的百分位""" result = np.full_like(x, np.nan) for i in range(window, len(x)): result[i] = np.sum(x[i-window:i] <= x[i]) / window return result def _delay(x, period=1): """延迟算子:取 period 期前的值""" result = np.full_like(x, np.nan) result[period:] = x[:-period] return result # 注册为 gplearn 可用的函数 ts_rank_func = make_function(function=_ts_rank, name='ts_rank', arity=1) delay_func = make_function(function=_delay, name='delay', arity=1) # 在 SymbolicTransformer 中通过 function_set 传入 from gplearn.genetic import SymbolicTransformer est = SymbolicTransformer( function_set=('add', 'sub', 'mul', 'div', ts_rank_func, delay_func), generations=15, population_size=2000, hall_of_fame=100, n_components=20, parsimony_coefficient=0.001, random_state=42, n_jobs=-1 )上面这段代码做了两件事:定义时序算子函数,然后通过 make_function 包装成 gplearn 能识别的格式。注意 arity=1 表示这个函数接收一个输入参数。如果你要定义双输入的时序算子比如 correlation,arity 要设成 2。parsimony_coefficient 是复杂度惩罚项,值越大越倾向于生成短表达式,防止公式膨胀到几百个字符没法看。n_components=20 表示最终输出 20 个因子,hall_of_fame=100 表示从表现最好的 100 个表达式里选多样性最高的 20 个。
2.3 因子生成前的数据准备与参数配置
数据准备这一步容易被忽视,但坑最多。输入 gplearn 的特征矩阵必须是数值型、无缺失、已对齐的。常见做法是把 OHLCV 数据做标准化或截面排序后再喂进去。如果不同标的的价格量纲差异大,不做标准化会导致遗传规划偏向选择量纲大的特征。
import pandas as pd from sklearn.preprocessing import StandardScaler # 假设 df 是 MultiIndex (date, asset) 的行情数据 # 特征列:open, high, low, close, volume, turnover features = ['open', 'high', 'low', 'close', 'volume', 'turnover'] # 截面标准化:每个时间截面上做 z-score def cross_section_zscore(df, cols): grouped = df.groupby(level='date')[cols] return grouped.transform(lambda x: (x - x.mean()) / (x.std() + 1e-8)) df_norm = df.copy() df_norm[features] = cross_section_zscore(df, features) # 去除缺失值 df_norm = df_norm.dropna(subset=features) # 按时间切分训练集和测试集,避免未来函数 train_mask = df_norm.index.get_level_values('date') < '2022-01-01' X_train = df_norm.loc[train_mask, features].values X_test = df_norm.loc[~train_mask, features].values截面标准化用 groupby(level='date') 保证每个时间截面上独立计算均值和标准差,避免引入未来信息。dropna 之后要检查剩余样本量,如果某只股票因为停牌导致大量缺失被删掉,可能影响后续回测的标的覆盖度。训练集和测试集按时间切分,绝对不能随机切分,否则遗传规划会学到未来模式,样本外表现直接崩掉。
3. 回测评估与可视化:从因子表达式到净值曲线
3.1 因子评估指标体系与回测流程
生成因子只是第一步,关键是怎么判断哪些因子值得留。这套框架的评估模块通常包含 IC 分析、分层回测和换手率统计。IC 是因子值和未来收益的截面相关系数,IC 均值大于 0.03 且 ICIR 大于 0.3 的因子才值得进一步看。分层回测是把股票按因子值分成 5 到 10 组,看多空组合的净值曲线是否单调。
import numpy as np from scipy.stats import spearmanr def compute_ic(factor_df, forward_return_df, method='spearman'): """ factor_df: DataFrame, index=(date, asset), columns=factor_names forward_return_df: Series, index=(date, asset), 未来一期收益 返回每个因子的 IC 序列和 ICIR """ ic_results = {} for col in factor_df.columns: ic_list = [] dates = factor_df.index.get_level_values('date').unique() for dt in dates: try: f = factor_df.loc[dt, col] r = forward_return_df.loc[dt] common = f.index.intersection(r.index) if len(common) < 30: continue if method == 'spearman': ic, _ = spearmanr(f[common], r[common]) else: ic = np.corrcoef(f[common], r[common])[0, 1] ic_list.append(ic) except KeyError: continue ic_series = pd.Series(ic_list) ic_results[col] = { 'IC_mean': ic_series.mean(), 'IC_std': ic_series.std(), 'ICIR': ic_series.mean() / (ic_series.std() + 1e-8), 'IC_positive_ratio': (ic_series > 0).mean() } return pd.DataFrame(ic_results).T这段代码逐日计算截面 IC,用 spearman 秩相关而不是 pearson 相关,因为因子值和收益的关系往往是非线性的,秩相关更稳健。ICIR 是 IC 均值除以 IC 标准差,衡量因子稳定性的核心指标。IC_positive_ratio 表示 IC 为正的交易日占比,低于 55% 说明因子方向不稳定。注意每个截面至少需要 30 个标的才有统计意义,少于这个数就跳过当天。
3.2 分层回测与多空组合构建
分层回测比 IC 更直观。把每个截面的标的按因子值从高到低分成 N 组,分别计算每组的等权或市值加权收益,然后累乘得到净值曲线。如果 Top 组和 Bottom 组的净值曲线明显分离且单调,说明因子有效。
def layered_backtest(factor_df, return_df, n_groups=5): """ 分层回测:返回每组净值曲线和多空净值 """ group_returns = {i: [] for i in range(n_groups)} dates = factor_df.index.get_level_values('date').unique().sort_values() for dt in dates: try: f = factor_df.loc[dt] r = return_df.loc[dt] common = f.index.intersection(r.index) if len(common) < n_groups * 5: continue f_common = f[common] r_common = r[common] # 按因子值分组 labels = pd.qcut(f_common, n_groups, labels=False, duplicates='drop') for g in range(n_groups): mask = labels == g if mask.sum() > 0: group_returns[g].append(r_common[mask].mean()) else: group_returns[g].append(0.0) except (KeyError, ValueError): continue # 构建净值曲线 nav_df = pd.DataFrame(index=dates[:len(group_returns[0])]) for g in range(n_groups): ret = pd.Series(group_returns[g], index=nav_df.index) nav_df[f'Group_{g}'] = (1 + ret).cumprod() # 多空净值:Top组 - Bottom组 long_ret = pd.Series(group_returns[n_groups-1], index=nav_df.index) short_ret = pd.Series(group_returns[0], index=nav_df.index) nav_df['Long_Short'] = (1 + long_ret - short_ret).cumprod() return nav_dfpd.qcut 按分位数分组,duplicates='drop' 处理因子值大量重复的情况。多空组合用 Top 组收益减 Bottom 组收益,再累乘净值。注意这里用的是等权平均,实盘要考虑市值加权和交易成本。如果多空净值曲线回撤超过 20%,这个因子基本可以放弃。
3.3 可视化分析与因子筛选
可视化模块通常包含 IC 衰减图、因子相关性热力图、分层净值曲线。IC 衰减图看因子预测能力随持有期的变化,如果 IC 在 5 天后迅速衰减到 0,说明这是个短周期因子,换手率会很高。因子相关性热力图用来剔除高度相关的因子,避免组合里全是同一种逻辑。
import matplotlib.pyplot as plt import seaborn as sns def plot_factor_analysis(ic_df, nav_df, factor_corr): fig, axes = plt.subplots(1, 3, figsize=(18, 5)) # IC 柱状图 ic_df['IC_mean'].sort_values().plot(kind='barh', ax=axes[0]) axes[0].set_title('Factor IC Mean') axes[0].axvline(x=0.03, color='r', linestyle='--') # 分层净值曲线 for col in nav_df.columns: axes[1].plot(nav_df.index, nav_df[col], label=col) axes[1].legend() axes[1].set_title('Layered Backtest NAV') # 因子相关性热力图 sns.heatmap(factor_corr, annot=False, cmap='coolwarm', ax=axes[2]) axes[2].set_title('Factor Correlation') plt.tight_layout() plt.savefig('factor_analysis.png', dpi=150) plt.show()IC 柱状图里红色虚线是 0.03 阈值,低于这条线的因子直接淘汰。分层净值曲线看单调性,如果 Group_0 到 Group_4 的净值曲线交叉混乱,说明因子没有区分度。相关性热力图里如果两个因子相关系数超过 0.7,保留 IC 更高的那个就行。这套可视化流程跑一遍,基本能筛掉 80% 的无效因子。
4. 避坑与常见问题排查:血泪经验换来的五条铁律
4.1 未来函数导致样本外崩盘
现象:训练集 IC 高达 0.08,样本外 IC 直接变成负的。原因:因子表达式里用到了未来信息,比如用当日收盘价计算当日收益,或者标准化时用了全样本均值。解决:所有时序算子的窗口必须严格只往回看,标准化必须在每个截面上独立做。检查方法是把因子值整体滞后一期再算 IC,如果 IC 大幅下降,说明有未来函数。
4.2 种群早熟收敛生成同质化因子
现象:20 个输出因子之间的相关系数普遍超过 0.8,本质上是同一个因子的变体。原因:种群大小太小或者代数太多,多样性丢失。解决:把 population_size 调到 3000 以上,parsimony_coefficient 调到 0.002 到 0.005 之间,增加变异率。另外 hall_of_fame 设大一点,让最终选择有更多候选。
4.3 量纲未统一导致算子失效
现象:生成的因子里全是 add 和 sub,几乎没有 div 和 mul。原因:不同特征量纲差异太大,除法算出来的值极小,适应度评估时被淹没。解决:所有输入特征做截面 z-score 标准化,让均值为 0、标准差为 1。这样加减乘除都在同一量级上,遗传规划能公平地评估每个算子。
4.4 回测未扣交易成本导致虚高收益
现象:多空组合年化收益 40%,实盘跑下来只有 10%。原因:高换手因子每天调仓,交易成本吃掉大部分收益。解决:回测时加入双边千分之三的成本,或者把调仓频率从日频降到周频。换手率超过 50% 的因子要特别警惕,除非 IC 非常高,否则不值得做。
4.5 gplearn 并行报错与内存溢出
现象:n_jobs=-1 时程序崩溃或者报内存错误。原因:种群太大加上并行进程数太多,内存扛不住。解决:把 n_jobs 设成 CPU 核心数的一半,population_size 先从小规模跑通再放大。如果还是溢出,把数据按时间分块,逐块生成因子再合并。
5. 进阶技巧:用遗传规划做因子组合与参数调优
跑通基础流程之后,真正拉开差距的是怎么把遗传规划生成的因子组合起来。我一般不会直接把 20 个因子等权合成,而是用遗传规划再做一层组合优化。具体做法是把上一步生成的因子值作为新特征,用 SymbolicTransformer 再跑一轮,让算法自己找因子之间的非线性组合方式。
# 第二阶段:因子组合优化 from gplearn.genetic import SymbolicTransformer # 假设 factor_matrix 是第一阶段生成的因子值矩阵 # forward_return 是未来收益 combo_est = SymbolicTransformer( function_set=('add', 'sub', 'mul', 'div'), generations=10, population_size=1000, hall_of_fame=50, n_components=5, parsimony_coefficient=0.003, random_state=42, n_jobs=4 ) # 用因子值和收益做拟合,注意这里 y 是收益 combo_est.fit(factor_matrix, forward_return) combo_factors = combo_est.transform(factor_matrix)第二阶段的关键区别是 function_set 只保留四则运算,不再加时序算子,因为时序信息已经在第一阶段提取过了。n_components=5 表示最终合成 5 个组合因子,再对这 5 个因子做 IC 和分层回测,选最好的 1 到 2 个用于实盘。parsimony_coefficient 设大一点,防止组合公式过于复杂导致过拟合。
参数调优方面,我习惯用网格搜索加时间序列交叉验证。具体是把训练集按时间分成 3 折,每折上跑不同参数组合,看验证集 IC 的均值和标准差。优先选 IC 均值高且标准差小的参数。常见做法是 population_size 在 [1000, 2000, 3000] 里选,generations 在 [10, 15, 20] 里选,parsimony_coefficient 在 [0.001, 0.003, 0.005] 里选。27 个组合跑下来大概半天时间,但能避免拍脑袋定参数。
还有一个容易被忽视的点是因子正交化。遗传规划生成的因子之间往往有相关性,用对称正交或者施密特正交把因子之间的线性依赖去掉,再合成多因子打分。正交化之后 IC 可能略微下降,但多空净值的稳定性会明显提升。
从那以后我每次跑遗传规划之前,都强制先做三件事:检查输入数据有没有未来函数、确认所有特征已截面标准化、把训练集和测试集按时间切干净。这三步走完再跑,翻车概率至少降一半。希望帮到你。
本文还有配套的精品资源,点击获取