基于gplearn的量化因子自动生成系统实践:收益回撤比与5日IC双指标评估
简介:量化投资中,因子挖掘是策略收益的重要来源,但传统人工挖因子效率低、同质化严重。遗传规划算法作为一种进化计算技术,通过表达式树自动搜索因子空间,为自动化因子生成提供了新思路。在实践中,基于gplearn构建的因子生成系统,将收益回撤比与5日IC作为核心评估指标,可有效筛选出具备预测能力的因子表达式。该系统覆盖时序因子与选股因子两大场景,结合数据清洗、复杂度惩罚与分层回测机制,能够显著提升因子开发效率。本文从遗传规划原理出发,探讨因子自动生成的技术价值,并介绍其在期货日线级别与股票横截面选股等中短周期量化研究中的应用路径。通过合理配置函数集与评估流程,gplearn能在海量表达式中发现稳定有效的因子,为多因子模型提供高质量素材。 最近我把一套基于gplearn的量化投资因子自动生成系统完整跑通了一遍,收益回撤比和5日IC指标都做了嵌入评估,整体效果比人工挖因子要省力得多。今天把整个项目的设计思路、实现细节和踩过的坑整理出来,给同样在做因子挖掘的朋友做个参考。这套系统核心就两件事:一是用遗传规划算法gplearn自动生成候选因子,二是把收益回撤比和5日IC指标作为筛选依据,从海量表达式里挑出真正有预测能力的因子。拆分下来包含时序因子生成和选股因子生成两个大模块,外加一套因子回测框架和可视化工具,适合做中短周期的量化研究,尤其是期货日线级别和股票横截面选股两类场景。
1. 项目整体设计思路:为什么用遗传规划来挖因子
1.1 传统因子挖掘的痛点
手工挖因子这件事,做过的人都懂那种痛苦。先要盯着一堆量价数据想逻辑,然后写代码验证,再用历史数据回测,最后十有八九是无效的。即便挖到一个能用的因子,市面上同质化也严重,被人复制之后很快失效。这种模式下,一个研究员一个月能产出三五个能上线的因子就算高产了。
搜索空间的问题更麻烦。假设用5个基础特征做素材,允许做加减乘除和若干函数变换,两层以内能组合出的表达式数量就已经是天文数字。靠人脑枚举是根本枚举不完的。于是我用遗传规划算法来解决这个问题:让程序在表达式空间中自动搜索,找出预测能力最强的因子。
1.2 遗传规划的核心机制
遗传规划(Genetic Programming)是进化算法的一种,跟遗传算法最大的区别在于:它进化的个体不是参数向量,而是一棵表达式树。每一棵表达式树就是一个因子公式,比如(close - open) / (high - low + eps)这棵树里,叶子节点是原始特征,内部节点是运算符。
gplearn的实现遵循了标准GP流程:随机初始化一批表达式树构成种群,每一代通过锦标赛选择挑出表现好的父本,然后做子树交叉和点变异生成下一代,如此迭代几十代后收敛出一批高分表达式。关键参数包括:
- population_size:种群规模,即每代保留多少棵表达式树
- generations:进化代数
- tournament_size:锦标赛选择的候选个体数量
- function_set:允许使用的函数集合
- parsimony_coefficient:复杂度惩罚系数,防止表达式无限膨胀
gplearn的独特优势在于,它直接用sklearn风格的API,.fit()完成进化,.transform()把新样本映射成语义特征,能无缝嵌入现有机器学习流程。
1.3 两大模块划分的深层原因
时序因子和选股因子看起来都是“用历史数据算出一个值”,但底层数据结构完全不同。
时序因子针对的是单一标的(或同品种集合)的时间序列,每一行是一个时间点,目标是预测未来K期的收益。例如对螺纹钢期货,用过去20天的量价数据生成一个特征,判断未来5天的涨跌方向。这种场景下,因子的时间衰减速度是核心指标,5日IC就是专门衡量中短周期预测能力的。
选股因子针对的是横截面数据,每一行是某一天某个股票,目标是预测股票之间的相对强弱。这种场景下,因子的IC要按每天截面计算,然后汇总成IC序列,看重的是因子在横截面上区分股票的能力。
这两种数据结构差异太大,打包在一个程序里必然出现代码混乱、参数互相干扰。所以系统拆成两个模块,共用评估框架,但生成流程完全分离。
2. 环境准备与数据工程
2.1 依赖安装与版本选型
整个系统基于Python 3.8+开发,核心依赖如下:
pip install gplearn==0.4.2 pandas numpy scipy matplotlib statsmodels这里重点强调一下gplearn的版本问题。0.4.2是当前最稳定的版本,0.4.1的SymbolicTransformer在某些情况下会丢特征列,0.4.0之前不支持自定义metric。实测下来0.4.2没有明显bug,推荐直接锁版本。
pandas和numpy就没什么好说的了,量化研究标配。scipy用来算Spearman相关系数,这也是IC计算的核心工具。statsmodels用于后续的回归中性化处理,选股模块里会用到。
2.2 数据结构设计:长表和宽表的选择
gplearn的输入输出本质上是二维矩阵,所以数据组织方式直接决定因子能否正确生成。
时序模块的数据我用的是长表展开成宽表的结构:
import pandas as pd import numpy as np def prepare_ts_features(df, feature_cols, window=20): """ 将原始时序数据转换为gplearn可训练的特征矩阵 df: 包含 datetime, symbol, close, open, high, low, volume, returns 等列 feature_cols: 需要展开窗口的基础特征列 window: 窗口长度 """ frames = [] for col in feature_cols: for lag in range(1, window + 1): frames.append(df.groupby('symbol')[col].shift(lag).rename(f'{col}_lag_{lag}')) X = pd.concat(frames, axis=1) return X.dropna()这里的关键思路是:gplearn的函数都是element-wise操作,它本身不理解“时间顺序”。要让遗传规划感知时序信息,就必须把历史窗口展开成独立特征列。比如close_lag_1表示前一天的收盘价,close_lag_5表示5天前的收盘价,这样表达式树里就可以组合出类似close_lag_1 / close_lag_5这样的动量因子。
选股模块的数据组织方式不同。每行是一个(date, stock)对,特征列是该股票在当天的截面特征(估值、动量、换手率等),再加一个industry和market_cap列用于中性化。
2.3 数据清洗与训练测试切分
量化因子的数据清洗比特征工程更关键,几个红线必须守住。
第一,去极值。原始量价数据中偶尔会有异常跳动,直接用会让表达式学习到噪声。我用了MAD(中位数绝对偏差)去极值:
def winsorize_series(s, n=5): median = s.median() mad = (s - median).abs().median() upper = median + n * mad lower = median - n * mad return s.clip(lower, upper)第二,切分必须按时间顺序,不能随机打乱。因子模型最怕的就是未来函数和泄漏。我的切分方式是:前70%作为训练期,中间15%作为验证期,最后15%作为测试期。gplearn只在训练期上进化,验证期用来筛选因子,测试期最后做一次终评。
第三,时序数据要防止同一标的的前后样本被同时选用。gplearn的max_samples参数如果小于1.0就会随机抽样训练行,对于面板数据,这可能导致同一天的多个股票被抽中而时间上重叠。所以我在训练前先按日期排序固定索引,并给每个日期一个权重列,抽样时尽量均匀覆盖时间区间。
3. 时序因子生成模块实现
3.1 时序因子的数学表达与函数集配置
时序因子要捕捉的典型市场规律有这么几类:
- 动量反转:
close / close_lag_N - 1 - 波动异常:
std(returns, N) / mean(returns, N) - 量价背离:
returns与volume的偏离度 - 日内形态:
(close - open) / (high - low)
要让gplearn进化出这些形态,函数集配置是关键。默认函数集只有加减乘除,我扩展了以下几类:
from gplearn.functions import make_function import numpy as np def _log(x): with np.errstate(divide='ignore', invalid='ignore'): return np.where(x > 0, np.log(x), 0.0) def _sqrt(x): return np.sqrt(np.abs(x)) def _inv(x): with np.errstate(divide='ignore', invalid='ignore'): return np.where(x != 0, 1.0 / x, 0.0) log_func = make_function(function=_log, name='log', arity=1) sqrt_func = make_function(function=_sqrt, name='sqrt', arity=1) inv_func = make_function(function=_inv, name='inv', arity=1)这里有几个容易踩的坑:一是除法直接在函数集里用原生的/会产生大量除零错误,我在自定义除法时加了eps保护;二是log函数必须处理负数输入,否则会生成一堆NaN因子;三是sqrt同样要取绝对值再开方。这些保护措施直接影响进化过程的稳定性,很多gplearn生成的因子在实盘中一算就报错,大多是这里没处理好。
3.2 基于SymbolicTransformer的时序特征演化
SymbolicTransformer适合生成多列新特征,它跟SymbolicRegressor的核心区别是:transformer不追求拟合具体标签,而是学习一组能保留数据信息的新表示。我的做法是给transformer设定较高的输出特征数量,让它一次生成多个候选因子,然后外部用IC去筛。
下面的代码展示了核心训练流程:
from gplearn.genetic import SymbolicTransformer function_set = ['add', 'sub', 'mul', 'div', log_func, sqrt_func, inv_func] gp = SymbolicTransformer( generations=30, population_size=3000, hall_of_fame=50, n_components=20, function_set=function_set, parsimony_coefficient=0.005, max_samples=0.8, metric='spearman', random_state=42, n_jobs=4 ) gp.fit(X_train, y_train) # 提取生成的特征 X_train_gp = gp.transform(X_train) X_test_gp = gp.transform(X_test)这里要重点解释metric='spearman'。gplearn默认的metric是mse,也就是最小化均方误差,但在因子挖掘场景,我们更关心的是因子值和未来收益之间的秩相关性,而不是精确预测收益。所以我自定义了一个基于Spearman相关的metric。这里有一个技巧:因为gplearn内部是调用sklearn的r2_score、mean_squared_error这类函数,自定义metric时需要保证函数签名是(y_true, y_pred):
from scipy.stats import spearmanr from gplearn.metrics import make_fitness def _spearman(y_true, y_pred): if np.std(y_true) == 0 or np.std(y_pred) == 0: return 0.0 corr, _ = spearmanr(y_true, y_pred) return 1.0 - corr # 注意:gplearn内部是最小化fitness spearman_fitness = make_fitness(function=_spearman, greater_is_better=False)设置为greater_is_better=False是gplearn的设定,metric越小代表越好。这样进化过程就是在最大化因子与标签的秩相关。
3.3 时序因子的有效性初筛
进化完成后,transform得到的20列新特征里,真正有效的通常只有两三列。我做初筛时看三个指标:
- IC均值:因子值与未来5日收益的Spearman相关,按时间平均
- ICIR:IC均值除以IC标准差,衡量稳定性
- IC衰减:看因子在未来1/3/5/10天的IC变化,如果衰减太快,说明只适合短周期
下面是一段典型的因子评估代码:
def evaluate_ts_factor(factor_series, forward_returns, periods=[1, 3, 5, 10]): """评估时序因子的IC表现""" df = pd.DataFrame({'factor': factor_series, 'fwd_ret': forward_returns}) results = {} for p in periods: # 假设forward_returns已经按未来p天收益计算 ic = df[['factor', f'fwd_ret_{p}']].dropna().corr(method='spearman').iloc[0, 1] results[f'IC_{p}d'] = ic return results实际筛选中我会设两个硬性门槛:5日IC绝对值大于0.03,且ICIR大于0.3。低于这个水平,就算回测曲线再漂亮,也不能进入下一步回测。
4. 选股因子生成模块实现
4.1 截面数据的组织与清洗
选股因子的原始数据是每个交易日所有股票的特征快照。我在生成因子前先做了预处理流程:
- 剔除ST股、上市不足60天的新股,以及停牌股
- 对所有原始特征做横截面标准化(Z-score)
- 用行业哑变量和市值对数做主成分回归,取残差作为中性化后的特征
中性化这一步很多人会忽略,但它太重要了。如果一个因子本质上只是在选大盘股,那么这个因子赚的是市值beta的钱,而不是alpha。中性化后的特征才能让gplearn学到真正的横截面选股逻辑。
4.2 让gplearn生成选股因子
选股因子本质上是一个横截面打分公式:给定某天所有股票的截面特征,计算每只股票的因子值,然后按因子值排序选股。由于数据结构是面板,我把训练样本按(date, stock)展开成一行,让遗传规划在这个大矩阵上进化。
代码上与时序模块高度相似,只是特征列不同,但有个关键差异:标签y的构造方式。时序模块的标签是单一标的的未来收益,选股模块的标签应该是横截面处理后的未来收益——也就是未来5日收益减去当日全市场等权收益,消除市场整体涨跌的影响。
# 横截面收益中性化:减去当日全市场平均收益 df['fwd_ret_5d_neutral'] = df.groupby('date')['fwd_ret_5d'].transform( lambda x: x - x.mean() )这样gplearn学到的就是纯选股alpha,而不是对市场涨跌的预测。
另外,我对选股因子模块的进化做了更强的复杂度惩罚。因为选股数据行数多、特征维度高,容易进化出复杂到离谱的公式。实测中把parsimony_coefficient从0.005提到0.01,能显著降低过拟合风险。
4.3 选股因子的多因子合成
生成的单个选股因子IC再高,实际使用中也不可能只靠一个因子。我的做法是生成20~50个候选因子,按ICIR从高到低排序,再从高到低依次挑选,去掉与已选因子相关系数大于0.7的候选,最终保留5~8个低相关因子。
合成时用IC加权:
def ic_weight_combine(factor_df, ic_values): """按IC权重合成多因子""" ic_arr = np.array(ic_values) weights = ic_arr / ic_arr.sum() combined = np.zeros(len(factor_df)) for i, col in enumerate(factor_df.columns): combined += weights[i] * factor_df[col].rank(pct=True) # 先做截面排名标准化 return combined注意这里合成前必须对每个因子做截面排名标准化,否则量纲差异会毁掉加权逻辑。这个细节我一开始没注意,合成后的因子效果反而比单个因子还差,排查了半天才发现是量纲问题。
5. 核心评估体系:收益回撤比与5日IC
5.1 收益回撤比的定义与计算
收益回撤比这里用的是Calmar Ratio的变体,即年化收益率与最大回撤的比值。对于因子本身,我通常看的是多空组合的净值曲线,即做多因子值最高的N只、做空因子值最低的N只(期货场景)或仅做多(股票场景)。
计算代码如下:
def calc_calmar_ratio(net_value_series, periods_per_year=252): """ net_value_series: 因子多空(或多头)组合的净值序列 periods_per_year: 年化周期数,日线数据传252 """ returns = net_value_series.pct_change().dropna() annual_return = (net_value_series.iloc[-1] / net_value_series.iloc[0]) ** (periods_per_year / len(net_value_series)) - 1 max_drawdown = (net_value_series / net_value_series.cummax() - 1).min() if max_drawdown == 0: return 0.0 return annual_return / abs(max_drawdown)这个指标的优点是综合了收益和风险,不容易被单边行情所欺骗。我在项目里设定的标准是:回测期不少于3年,CalmarRatio大于1.0的因子才进入下一步考察。低于这个值,策略的净值波动通常令人难以忍受。
5.2 5日IC指标的计算原理
IC(Information Coefficient)是因子预测能力的核心度量,等于因子值与未来N期收益的Spearman秩相关系数。5日IC就是看因子对5天后收益的预测能力。
时序因子模块里,IC按同一标的的时间序列计算;选股因子模块里,IC按每个交易日的横截面计算Spearman相关,然后把所有交易日的相关系数取均值得到IC均值,再除以标准差得到ICIR。
def calc_daily_ic(factor_series, forward_return_series): """ 选股因子使用:按日分组计算IC,返回每日IC序列 factor_series: 因子值,index为(date, stock) forward_return_series: 未来收益,与factor_series对齐 """ df = pd.DataFrame({'factor': factor_series, 'fwd_ret': forward_return_series}) df['date'] = df.index.get_level_values(0) ic_series = df.groupby('date').apply( lambda x: x['factor'].corr(x['fwd_ret'], method='spearman') if len(x) > 5 else np.nan ) return ic_series.dropna()为什么选择5日而不是1日或20日?这是我做过大量回测对比后确定的。1日IC的噪声太大,容易被微观结构因素干扰;20日IC又过于滞后,等因子发出信号时行情已经走完大半。5日刚好处于一个平衡点,既有足够的预测力,又能在中短周期策略中快速调仓。当然这只是针对我测试的期货和股票数据得到的经验值,不同市场周期可以自行调整。
5.3 把IC和收益回撤比嵌入筛选优化流程
这是整个系统最核心的设计决策。gplearn的进化过程本身很难直接嵌入IC和回撤评估,因为每次评估完整回撤太耗时。所以我采用了分阶段筛选策略:
- 第一阶段,gplearn在训练集上进化,用spearman metric作为适应度,快速淘汰明显无效的表达式
- 第二阶段,把hall_of_fame里的候选因子全部提取出来,在验证集上计算5日IC、ICIR和收益回撤比
- 第三阶段,按照ICIR为主要排序指标、CalmarRatio为次要指标,综合打分选出Top因子
- 最后,用选出的因子在独立的测试集上复算一遍,确认没有显著的样本内外差异
这样做的好处很明显:gplearn专注于快速搜索,深度的金融指标评估放到外部做,两者解耦。如果硬要让IC回撤完全嵌入gplearn内部,每代都要做完整的组合回测,那训练时间会变成完全不可接受。
5.4 可视化工具
可视化部分我封装了几个核心图表,主要用来快速诊断因子质量:
- IC时间序列图:每日IC的柱状图加滚动均值曲线,能直观看到因子的稳定性
- 累积IC图:如果累积IC曲线持续向上,说明因子的预测能力是稳定的,如果出现平台期或回落,说明因子在某些市场环境下失效
- 分组收益图:按因子值分成5组或10组,看每组未来5日的平均收益,单调性越好,因子质量越高
- 净值曲线与回撤图:展示多头、空头、多空组合的净值曲线和最大回撤区间
这些图我用matplotlib做了模板函数,丢给它因子序列和收益序列就能出图。代码不到一百行,但诊断效率比看一堆数字高得多。
6. 完整因子回测框架
6.1 分层回测框架设计
分层测试是检验因子有效性的行业标准,核心思路是把股票按因子值从小到大分成N组,统计每组的未来收益表现。如果第1组到第N组收益呈现单调递增或递减,说明因子的区分度好。
def layer_backtest(factor_df, forward_ret_df, layers=5): """ factor_df: 因子矩阵,index为date,columns为stock forward_ret_df: 未来收益矩阵,与factor_df对齐 """ def daily_layer_ret(factor_row, ret_row): valid = factor_row.dropna().index.intersection(ret_row.dropna().index) if len(valid) < layers * 10: return pd.Series(np.nan, index=range(layers)) ranks = factor_row[valid].rank(pct=True) layer_ret = {} for i in range(layers): lower, upper = i / layers, (i + 1) / layers members = ranks[(ranks >= lower) & (ranks < upper)].index layer_ret[i] = ret_row[members].mean() return pd.Series(layer_ret) layer_ret_df = pd.concat( [daily_layer_ret(factor_df.loc[dt], forward_ret_df.loc[dt]) for dt in factor_df.index], axis=1 ).T return layer_ret_df这段代码的逻辑不难,但需要注意性能问题。如果直接用for循环遍历几百个交易日,每个交易日又对几百只股票做排名,数据量上来后会非常慢。我实际优化时把rank和分组操作向量化了,跑2000个交易日的回测控制在几秒内。
6.2 多空组合回测与交易成本模拟
分层测试之后,我会进一步构建多空组合。做法是:每个调仓日做多因子值最高的前10%股票,做空因子值最低的后10%股票,持有5个交易日后调仓。
关键是要把交易成本加进去。很多因子挖出来看似赚钱,一算手续费和冲击成本就全吐回去了。我用的成本模型是双边万三的佣金加万五的冲击成本,即单次换仓成本约万八:
def simulate_long_short(factor_df, ret_df, top_ratio=0.1, cost_rate=0.0008): """ 简化的多空组合模拟,考虑换手成本 """ rank_df = factor_df.rank(axis=1, pct=True) long_mask = rank_df > (1 - top_ratio) short_mask = rank_df < top_ratio long_ret = (ret_df[long_mask].sum(axis=1) / long_mask.sum(axis=1)).fillna(0) short_ret = -(ret_df[short_mask].sum(axis=1) / short_mask.sum(axis=1)).fillna(0) gross_ret = long_ret + short_ret # 简化计算换手率:假设每期全部调仓 turnover = 1.0 net_ret = gross_ret - turnover * cost_rate net_value = (1 + net_ret).cumprod() return net_value这里的资金分配做了简化,实际运行中还要考虑资金占用和期货保证金,但作为因子初筛已经足够。
6.3 过拟合检测方法
遗传规划最大的风险就是进化出只拟合历史噪声的怪异公式,样本外直接崩盘。我的过拟合检测分三层:
第一层是样本内外IC对比。训练集的IC可能高达0.08,如果测试集IC掉到0.02以下,基本可以判定过拟合。我要求测试集IC不低于训练集的50%,否则直接丢弃。
第二层是因子相关性分析。gplearn生成的表达式树往往会在语义上高度相似,比如close / open和(close * 2) / (open * 2)形式不同但本质没区别。我对hall_of_fame里的所有因子做相关性矩阵,聚成几个簇后每簇只保留IC最高的代表,避免因子池里全是同一类逻辑。
第三层是随机数据扰动测试。把验证集的目标收益随机打乱,重新评估一遍所有候选因子。如果打乱后的随机数据上仍然出现IC绝对值大于0.03的因子,说明评估流程本身有泄漏或统计偏差,需要排查。
6.4 三大模块的接口设计
整个项目包拆成三个主要模块:
factor_pipeline/ ├── data_loader.py # 数据加载与清洗 ├── ts_factor.py # 时序因子生成模块 ├── cross_factor.py # 选股因子生成模块 ├── evaluate.py # IC、ICIR、CalmarRatio计算 ├── backtest.py # 分层回测与多空回测 └── visual.py # 可视化模块之间用标准DataFrame接口连接,时序模块输出的是时间序列因子值,选股模块输出的是面板因子矩阵,评估模块统一接收。这个设计让后续替换生成算法变得非常方便,比如我后来试验了用LightGBM做因子生成,只需要替换因子生成模块的内部实现,评估和回测完全不用动。
7. 常见问题与排查技巧实录
7.1 gplearn训练时间爆炸
这个几乎所有人第一次跑都会遇到。进化30代、种群3000,如果有20个原始特征,函数集7个算子,单机跑十几个小时是家常便饭。
我的解决办法是:
- 先减特征:用Pearson相关做预筛选,把相关性过高的特征合并,只保留5~8个核心特征进GP,生成完之后再逐步加回特征验证
- 调小
max_samples:0.3~0.5的采样比例,能极大加快每代评估速度,代价是进化过程可能更不稳定 - 用
hall_of_fame保存全局最优:即使进化后期种群退化,历史最优个体仍然被保留 - 并行加速:
n_jobs=-1设置全核并行,但注意线程数过高等同于内存溢出,建议8核以内
实测上述优化后,30代3000种群从十几小时降到约两小时,因子质量基本没有下降。
7.2 生成的因子大量无效或全为常数
这个问题也很常见。表达式树出现x / x这类自抵消结构时,输出恒为1,IC直接是NaN或0。gplearn不会自动剔除这类退化个体。
我的对策是:
- 训练前在自定义函数里做保护,比如除法加
eps,让x / x不再等于1而是约等于1附近的一个小范围数值 - 训练后清洗:对每个输出因子检查标准差,标准差小于某一阈值的直接删除
- 加大复杂度惩罚系数,鼓励更短的表达式,减少自抵消结构的产生概率
7.3 随机性与复现问题
gplearn的进化过程依赖随机数种子。如果不固定random_state,同一份数据两次运行会得到完全不同的因子,给调试带来极大困难。
我的习惯是:每次实验固定一个种子,然后跑3个不同的种子各一遍,取三个结果中IC表现最好且稳定的那个。这样既保证了单次实验可复现,又能通过多种子跑批排除运气成分。调参时用同一个种子对比,找到最优参数后再用多个种子做稳定性验证。
7.4 报错与解决方案速查表
| 现象 | 原因 | 解决方案 |
|---|---|---|
fit时报ValueError: array must not contain infs or NaNs | 特征或标签里有缺失值 | 训练前统一dropna(),或者对标签做填充 |
| transform输出特征数与n_components不符 | 部分特征因为常数被过滤 | 检查n_components设置,适当增大;检查自定义函数是否返回了常数 |
| metric传自定义函数报错 | 函数签名不符合gplearn要求 | 确认函数签名是(y_true, y_pred),且返回标量 |
| 训练速度极慢 | 特征维度太高或种群过大 | 减少特征列、调低population_size、使用max_samples |
| 生成的因子在样本外失效严重 | 过拟合 | 增大parsimony_coefficient,减少代数,增加样本外验证频率 |
自定义函数内部用到了pd.Series报错 | gplearn函数只接受numpy数组 | 自定义函数内部全部使用numpy操作 |
7.5 因子同质化问题
这是因子挖掘项目后期一定会遇到的瓶颈。第一轮进化出20个优质因子,第二轮再跑,新因子跟旧因子的相关系数动辄0.8以上,整个因子库的长尾效应严重。
解决办法是我在因子入库时强制做相关性去重:新因子与已有因子相关系数超过0.7就拒绝入库,除非新因子的ICIR显著高于旧因子。这种“宁缺毋滥”的策略虽然让因子库增长速度变慢,但保证了每个入库因子都有独一无二的逻辑贡献。真正上线之后,这种差异化的因子组合在风险控制上优势明显。
我个人在实际操作中最深的体会是:gplearn这类自动化因子挖掘工具,真正的价值不在于替你思考,而在于把“从亿万个表达式中找规律”这种人类不擅长的事情变成例行公事。系统跑出来的因子,有相当一部分像极了手工挖出来的经典因子,这说明进化算法确实在朝有效的方向搜索;但偶尔也会出现几棵结构非常反直觉但样本内外表现都稳定的表达式,这种往往才是真正的超额收益来源。如果你准备在这条路上深入,建议先在小规模数据上把评估管线打磨扎实,再放种群和代数,否则很容易被gplearn跑出来的海量垃圾因子淹没。
本文还有配套的精品资源,点击获取
