尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

gplearn实战:用遗传规划自动挖掘量化因子

gplearn实战:用遗传规划自动挖掘量化因子 简介基于gplearn模型的量化交易因子自动生成完整项目利用遗传规划中的选择、交叉与变异操作自动挖掘能预测价格变动的数学表达式面向量化分析师、金融工程人员及Python开发者弥补传统手工因子提取的局限。压缩包共包含53个文件整体大小约87MB其中Python源码脚本16个、pyc编译文件13个、CSV数据文件5个、pickle持久化数据4个并附有3个PDF文档、可视化图片及Markdown说明目录按数据、算法与工具模块分层组织便于系统性学习与二次开发。目前已有236人下载学习适合希望系统掌握遗传规划因子生成流程的读者。项目以simple-ba_use-gplearn-to-generate-CTA-factor为基础完整覆盖数据预处理、IC测试、回测与可视化等环节同时提供遗传编程核心模块封装及实证思路梳理文档帮助读者从数据到策略快速构建自己的CTA因子挖掘框架。1. 用gplearn做因子生成先分清遗传算法与遗传规划用gplearn做因子生成很多人第一反应是遗传算法但gplearn真正落地用的是遗传规划Genetic Programming。遗传算法把解编码成定长向量遗传规划把解编码成表达式树直接进化出公式。量化挖因子时我一般只用gplearn的SymbolicTransformer基础量价特征进去带未来收益标签的因子出来表达式可读、能复盘。它训练时资源消耗不小好处是Python生态成熟几千行股票截面数据几分钟能跑完。这篇文章写给打算用遗传规划自动合成因子、又不想自己从零写进化框架的人也写给那些在用Python写遗传算法代码但始终生成不出可解释因子的同路人。先别急着上强化学习遗传规划生成的公式因子在A股和期货里仍然是最容易解释、最容易跟踪失效原因的一类。2. 遗传规划在gplearn里的实现树结构、函数集与训练对象2.1 从GA遗传算法到GP遗传规划为什么树结构更适合造因子要理解gplearn先要区分两个词。常规的GA遗传算法Genetic Algorithm里个体是一条定长编码二进制串或浮点数向量交叉是切割拼接编码串变异是随机改写某个基因位。这种结构适合参数优化比如给一个已有的多因子模型找权重但它表达不出“A除以B再取对数”这种公式嵌套。遗传规划Genetic Programming, GP把个体换成树。叶子节点是变量X0、X1或常数内部节点是运算符add、sub、mul、div整棵树就是一个表达式。交叉换的是某个子树变异换的是某个节点。这样搜索空间从“参数值”变成了“函数结构”正好对因子挖掘的口味你给gplearn一堆基础特征它负责把这些特征拼成新的复合因子。我常拿一个例子给团队讲假设X0是5日动量X1是20日波动率传统遗传算法要调的是这两个变量的权重遗传规划则能进化出div(X0, sqrt(X1))这类动量除以波动的结构本质上是一个风险调整动量因子。这个公式用嵌套结构天然表达了“高动量低波动”的逻辑而权重调参做不到这种结构创新。很多讲遗传算法python代码详解的文章里交叉变异写起来很顺手但一旦要造因子定长编码方案就傻了。老老实实用定长编码去搜权重搜到顶也就是线性模型非线性关系全靠你预先设计特征。gplearn把进化算子封装在Cython加速的Program对象里用户只需要调fit树的生成、评估、选择、遗传操作都由库完成。2.2 SymbolicTransformer、Regressor、Classifier到底选谁gplearn主要提供三个类。SymbolicRegressor适合直接学一个回归模型输入X输出y的预测值它最后得到的是一个最终预测公式SymbolicClassifier类似做分类。但在因子挖掘场景我要的不是那个预测值而是“一组新特征”所以我几乎固定用SymbolicTransformer。SymbolicTransformer的fit(X, y)也是监督训练但transform(X)输出的是n_components个新特征。它的内部逻辑是每一代进化出若干棵树每棵树代表一个因子表达式用这些表达式对X做变换再计算变换结果与y的相关性相关性高的树被保留。也就是说y的作用是给因子打分而不是让你最终用predict去预测。这样很契合因子挖掘思路先用现有特征合成候选因子再用y筛选。有人问我为什么不用SymbolicRegressor直接预测收益再反转成因子。我试过问题在于Regressor对整体MSE负责出来的公式偏向拟合噪声而且它只输出一个表达式因子的单调性基本不可控。Transformer则每个输出都明确对着响应变量优化后续做IC分析、分层回测都更方便。Classifier我基本不用。因子挖掘的响应变量是连续的未来收益把它离散化成涨跌标签会丢掉极值信息而且分类目标对树结构的影响不如秩相关那么直接。除非你在做事件驱动比如预测停牌或者涨跌停否则别把连续收益切掉。2.3 数据形态与第一个能跑通的最小脚本gplearn的fit接口和sklearn一致X是二维数组y是一维数组。但要注意量化里的“一行”不是一个时间点一只股票的普通行而是一个(股票, 日期)样本且X里不能混入未来信息。常见做法是把截面展平每行填过去5日的量价统计量y填该股票未来N日收益。下面这个脚本用随机数据演示API确认训练流程能跑通import numpy as np import pandas as pd from gplearn.genetic import SymbolicTransformer # 模拟两天的股票截面数据每行是一个(股票, 日期)样本 # 真实场景里X必须只用t-1及之前的数据构造y用t1到t5的收益 rng np.random.default_rng(42) n_samples 2000 X pd.DataFrame({ momentum_5d: rng.normal(0, 1, n_samples), volatility_5d: rng.lognormal(0, 0.5, n_samples), turnover_5d: rng.uniform(0, 0.1, n_samples), dist_ma10: rng.normal(0, 0.05, n_samples), }) y rng.normal(0, 0.02, n_samples) gp SymbolicTransformer( population_size500, generations2, function_set[add, sub, mul, div, abs, log, sqrt, max, min], parsimony_coefficient0.01, metricspearman, random_state0, n_jobs1, verbose1, ) gp.fit(X, y) new_factor gp.transform(X) print(new_factor.shape) # 得到n_samples行, n_components列这里有几个初次上手就会看花眼的参数。population_size是每一代保留多少棵候选树generations是进化多少代两者共同决定搜索规模。function_set是允许使用的运算符我故意把sin、cos去掉它们在价格类序列上很容易过拟合。parsimony_coefficient是复杂度惩罚系数越大越偏向短公式后面会专门讲。metricspearman表示因子与y用秩相关打分这比mse更符合IC口径。random_state固定住复现结果才可靠。n_jobs先设1并发问题放到第5章说。这段代码跑完new_factor就是gplearn替你生成的因子矩阵。注意随机数据上它没有意义重要的是API调用方式。把它换成真实量价数据后输出就是候选因子。有人第一次跑完看到遗传规划python代码比想象中短就开始往shuffle后的数据上套这是最容易翻车的地方第3章讲数据切分时继续说。2.4 真实量价特征怎么进模型shift是唯一底线接上面我实际构造X时通常不直接用原始价格而是把原始行情先做滞后再算衍生统计量。原因很简单t日收盘价本身要到t日收盘后才知道而你的因子如果要能在t日收盘后计算并用于t1日交易就必须保证X里的所有数据都只用到t日及以前的数据。但gplearn的每一代树可以对X做任意组合比如div(X0, X1)里X0是当日动量X1是当日成交量它们本身都不含未来可如果树里出现未来N日成交量的特征泄漏就会直接混进IC。我的底线是所有原始行情先按股票做shift(1)再用shift后的数据计算滚动统计量。比如5日动量用的是close.shift(1)再除以close.shift(6)而不是直接用当天的close。这样做出来的特征天然落后一个交易日y用未来5日收益两者不会撞车。panel panel.sort_values([date, stock]).groupby(stock).apply( lambda g: g.assign( momentum_5dg[close].shift(1) / g[close].shift(6) - 1.0, volatility_5dg[close].shift(1).pct_change().rolling(5).std().shift(1) ) ).reset_index(dropTrue)这段代码两个shift要看清第一个shift让当日数据变成昨日及以前第二个shift保证滚动窗口的最后一天在t-1日不包含t日。很多用gplearn挖因子的人翻车不是进化算法写错而是X里的未来函数没处理干净。gplearn本身不会帮你做这个事它只会把“能预测y”的树选出来而那些“能预测y”的树里有不少正是用了不该出现的特征。3. gplearn生成因子的最小流程从数据对齐到参数设置3.1 样本切分时间序列必须按时间切不能按行随机切gplearn进化时会在训练集上拼命找“能解释y的X组合”如果你用sklearn默认的train_test_split随机切分同一时刻的截面样本会被切进训练和验证两侧因子中只要有一点点用到未来统计量验证IC都会假性偏高。我一般按日期排序取前70%时间做训练后30%做验证y的窗口也要保证没有重叠泄漏。切分之后fit只在训练集上做transform在验证集上做。有人图省事把全样本交给gplearn然后再看IC那等于把考试答案提前给了学生。gplearn的进化过程会在选出的因子上继续迭代最终选出的树几乎必然和全样本响应变量相关。所以最小可用流程至少是三段训练集进化、验证集做IC、测试集做最终确认。还有一个常见问题是训练集里包含极端行情。2020年疫情那波大跌2024年年初的alpha对冲危机这些时段如果混进训练集gplearn会专门进化出针对极端跳空的公式。这些公式在正常市况下表现平庸。我会在做gplearn之前先把涨跌停和一字板样本剔除或者给极端收益样本降权。遗传规划不擅长区分“规律”和“偶发事件”你要替它把偶发事件先抹掉。3.2 一个完整的“训练并评估RankIC”示例下面给一套可以直接改的代码。假设你已经构造好了面板数据panel包含date列、stock列、特征列和future_ret列。# 按时间排序后切分 panel panel.sort_values([date, stock]).reset_index(dropTrue) dates panel[date].unique() train_dates dates[: int(len(dates) * 0.7)] val_dates dates[int(len(dates) * 0.7):] feature_cols [c for c in panel.columns if c.startswith(f_)] X_train panel.loc[panel[date].isin(train_dates), feature_cols].to_numpy() y_train panel.loc[panel[date].isin(train_dates), future_ret].to_numpy() X_val panel.loc[panel[date].isin(val_dates), feature_cols].to_numpy() y_val panel.loc[panel[date].isin(val_dates), future_ret].to_numpy() gp SymbolicTransformer( n_components5, population_size2000, generations5, function_set[add, sub, mul, div, abs, log, sqrt, max, min], parsimony_coefficient0.01, tournament_size20, metricspearman, max_samples10000, random_state1, n_jobs2, verbose1, ) gp.fit(X_train, y_train) factor_train gp.transform(X_train) factor_val gp.transform(X_val)这里fit完成之后不要急着看factor_val的IC先看verbose输出。gplearn的verbose会显示每一代的最好适应度和平均适应度。如果最好适应度一直在涨但第3代以后平均适应度开始下降说明种群在收敛可能出现一个公式统治全局的情况。这时候生成的因子多样性很差后面第5章会细说。接着用按日期截面的方法计算RankICimport numpy as np from scipy.stats import spearmanr def rank_ic_stats(panel_, factor_df, y_col, date_col): ic_list {i: [] for i in range(factor_df.shape[1])} for dt in np.unique(panel_[date_col]): mask panel_[date_col] dt y panel_.loc[mask, y_col].to_numpy().ravel() for col in range(factor_df.shape[1]): f factor_df.loc[mask, col].to_numpy().ravel() ic, _ spearmanr(f, y) ic_list[col].append(ic) return {ffactor_{i}: {IC: np.mean(v), ICIR: np.mean(v) / (np.std(v) 1e-8)} for i, v in ic_list.items()} rank_ic_stats(panel, factor_val, future_ret, date)逻辑说明n_components5表示要生成5个候选因子fit之后transform返回的列数与之一致。这里特意把切分放在进化之前验证集的IC才是真实水平。rank_ic_stats对每个交易日做一次截面秩相关再平均得到常见的IC和ICIR两个指标。参数说明tournament_size控制锦标赛选择的参赛个体数量太大选择压力过强种群容易过早收敛20是起步值。max_samples10000控制每次评估随机抽取的样本数应对超大面板时能大幅降内存代价是评估有一定噪声。随机抽样的种子由random_state决定复用同一个random_state才会让max_samples的抽样结果稳定。3.3 核心参数推荐区间与调参顺序把常用参数整理成一张表后面改参数时就照着这张表来。参数起始值推荐范围什么时候动它population_size30001000~8000因子太杂或IC不稳时加大训练太慢时减小generations53~20验证IC还在涨就增大训练集IC已经远高于验证集就减小function_setadd/sub/mul/div/abs/log/sqrt/max/min不加sin/cos公式里全是除和log时删掉除避免nanparsimony_coefficient0.010.001~0.1公式超过10层且验证IC下滑时调大tournament_size2010~50早熟收敛时调大多样性太差时调小max_samples100005000~全量内存爆时减小样本量不大就设1.0n_components51~10要多组备选因子时加大但相关性检查成本也会加metricspearmanspearman/mse想直接用回归误差评估时改mse我的调参顺序是固定的先跑一组小规模population_size500、generations2确认数据没有泄漏、程序不崩然后用上述参数跑主搜最后只动parsimony_coefficient和generations这两项对过拟合影响最直接。function_set原则上一次定好中途改等于重跑。有一点值得专门提醒gplearn的进化随机性很强。同一份X和y只改random_state得到的因子完全可能不同。所以我不会只跑一次就拍板而是跑5到10个random_state把每个状态下的验证集IC稳定在0.03以上才进入下一步。这个习惯帮我挡住了不少纯靠运气冒出来的“假因子”。3.4 从遗传算法python代码迁移到gplearn的三个陷阱如果你以前写过遗传算法python代码第一次用gplearn会顺手但有三处习惯要改。第一遗传算法里种群初始化通常是随机生成而gplearn的初始种群也会从function_set里随机拼树这导致每次结果完全不同必须用random_state固定第二遗传算法里适应度函数常常是越小越好gplearn的个体适应度是metric值spearman是越大越好mse是越小越好同一个符号在不同metric下含义不同第三遗传算法里你可以随时打印当代最优解gplearn的verbose只能看适应度想看公式要等训练完再遍历_programs中期监控能力有限。这三点不是bug是库的设计取舍。记住gplearn不是给你做在线学习的它适合离线批量挖掘然后把稳定公式固化下来。别拿它去做日频滚动训练一天跑一次几十代进化算力成本不划算也容易在盘中资源被拉爆。4. 把gplearn公式落地成可用因子清洗、去极值与中性化四步4.1 导出表达式而不是直接pickle模型gplearn训练完的SymbolicTransformer可以被pickle保存但随着种群增大模型文件里会带上每一代所有个体文件可能上百MB加载也慢。更稳妥的做法是把最终选出的表达式文本导出在独立的py模块里重写一遍。如果是SymbolicTransformer最终表达式存在gp._programs[-1]里每个元素是一棵树。可以通过遍历打印出来for i, prog in enumerate(gp._programs[-1]): print(i, str(prog))我一般会把这些字符串逐条过目看到超过15层的复杂公式直接放弃。表达式文本没有太多可读性但至少让你知道它是怎么由X0、X1拼出来的。还有一个筛选技巧看变量使用次数。如果某个输出树里X2从头到尾只用了一次而X0反复出现这个因子基本就是一个藏在X2外衣下的X0变形进入因子池后会和已有因子高度共线意义不大。表达式要入库时我会把它改写成SQL或极简Python函数。注意不要在线上环境每行都跑一遍解析器而是把表达式转成固定几个函数用numpy向量化计算。否则一个gplearn因子可以在回测里跑上实盘却因为逐行循环慢得没法用。4.2 用numpy重写公式处理除零和log负数gplearn里有些函数自带保护但自写函数时还是会踩坑。假设你得到公式div(X0, X1)当X1恰好等于0结果就是inf或nanlog(X0)遇到X00也一样。这类异常值进入IC统计会直接污染整个截面。我重写公式时固定加一层保护def safe_div(a, b, fill0.0): with np.errstate(divideignore, invalidignore): res np.where(np.abs(b) 1e-8, a / np.where(b 0, np.nan, b), np.nan) return np.where(np.isnan(res), fill, res) def safe_log(a, fill0.0): with np.errstate(divideignore, invalidignore): res np.where(a 0, np.log(np.where(a 0, np.nan, a)), np.nan) return np.where(np.isnan(res), fill, res)逻辑说明safe_div先去掉分母为0的位置再把无意义值统一填充成0safe_log只对正数取对数。这样写出来的因子在任何极端行情下都不会出现inf。参数fill可以根据因子分布定一般填0或者该列中位数。这一步很多人偷懒跳过结果就是因子在个别股票上出现几千倍的离群值后续去极值也救不回来。记住先清理inf/nan再做截面统计。4.3 覆盖缺失值并做数据对齐转换后的因子在停牌、新股、极端行情下可能整行缺失。这些缺失行不能直接删否则不同因子的样本量不一致IC比较就没有可比性。常见做法是先按date和stock做外连接对齐然后用截面中位数填充缺失。此外交易数据天然有位置敏感问题一个股票在t日的因子值必须对应t1到t5的收益不能因为groupby排序搞错offset。我在构造y时就固定用shift(-5)后的收益并在最终合并时保留date字段做二次校验。校验办法很简单随机抽十个日期检查因子与future_ret是否错位一周。错位是最隐蔽的坑复盘时比过拟合还难发现。缺失值处理还有一个量化里特有的点涨跌停日股票流动性变差因子值往往极端。如果你把涨跌停样本的缺失值用全截面中位数填充相当于淡化极端事件反而会让因子在回测里显得更平滑、更“干净”。我的做法是保留涨跌停标记作为单独特征而不是偷偷填充掉。gplearn能自己对X组合搜索给它一个is_limit的0/1变量它会自己决定要不要把涨跌停条件用进公式。4.4 去极值、标准化与中性化让因子可比较新因子在进入候选池之前我会统一做三步处理。第一步是MAD去极值比用百分比截尾更抗极端值第二步是截面zscore标准化第三步是按市值和行业做中性化去掉风格暴露。def mad_winsorize(s, n5): med s.median() mad (s - med).abs().median() scale 1.4826 * mad return s.clip(med - n * scale, med n * scale) def zscore_by_date(series): return (series - series.mean()) / (series.std() 1e-8) # 用最小二乘取残差完成市值与行业中性化 def neutralize(series, exposure_df): X np.column_stack([np.ones(len(series)), exposure_df]) beta, _, _, _ np.linalg.lstsq(X, series, rcondNone) return series - X betamad_winsorize的n是极值倍数5倍MAD是常见起点对干净一点的量价因子可以降到3。zscore_by_date要求同一日期内标准化避免不同市场环境下的波动差异压过因子本身。neutralize的exposure_df里行业列要one-hot展开市值列取对数。取残差后因子与行业、市值正交后续IC才更纯粹。中性化这步容易被忽视尤其做指数增强的人。如果你辛辛苦苦进化出一个因子结果它只是“小市值高波动”的加壳表达式放进模型后会加大风格暴露。gplearn根本不知道“风格”是什么它只认y和X的相关性所以你必须用中性化把风格效应先剥离掉再决定是否保留残差因子。做完这四步gplearn输出的一堆杂项才算变成一个能被检验的标准因子。直接把原始transform结果扔进回测框架是最常见的“看起来跑通了、最后不敢用”的根源。4.5 四步完成后的自检清单最后再补一个自检习惯四步做完不要急着看IC先跑一个统计摘要new_factor pd.DataFrame(new_factor, columns[fgp_{i} for i in range(new_factor.shape[1])]) for col in new_factor.columns: print(col, nan占比:, np.isnan(new_factor[col]).mean(), inf占比:, np.isinf(new_factor[col]).mean(), 去极值后std:, mad_winsorize(new_factor[col]).std())这里关注两件事。一是nan和inf占比任何一个超过1%就说明第4.2步的保护函数没写全二是去极值后std如果接近0说明这个因子在大多数样本上是常数比如div(X0, X1)里X0远小于X1结果几乎全为0。这类因子即使IC高也是极少数极端样本贡献的进不了实盘。5. gplearn因子挖掘常见坑排查从过拟合到公式爆炸5.1 训练集IC高、验证集IC转负现象进化过程里训练IC一路涨到0.08模型还在verbose里显示找到了更优个体换到验证集IC直接变成-0.02分组收益也倒挂。原因gplearn没有正则化样本外验证。它的适应度函数就是训练集上的Spearman相关训练集里只要存在几根异常K线或涨跌停板造成的极端样本树就往那些样本上拟合。代数越高过拟合越严重。我见过最夸张的一次训练IC到了0.12验证IC是-0.08原因就是训练集里包含一个连续三天的板块性涨停树学会了“追涨停后一天还有溢价”的假规律。解决先把generations降回3~5把parsimony_coefficient从0.01上调到0.05强制公式变短再检查训练样本里是否混入了全天一字板的极端行情剔除涨跌停样本重跑。最关键的还是按时间切分把验证集完全隔离出进化过程只用作事后评估。还有一个小技巧把训练集末尾5%单独留出来做“夹层验证”如果进化过程在夹层上适应度大幅下降说明种群已经开始记住训练集的个体噪声这时候应该立即停止增加代数。5.2 生成的公式里全是inf和nan现象transform之后有一列超过30%的样本值是nan或inf打印表达式看到div(X0, X1)、log(X0)的组合。原因function_set里放了log、div但没有对这些运算符做保护。gplearn的进化只管公式的“结构新颖度”不管运行时的数值合法性。当X0接近0或X1等于0整棵树的结果就被污染。另外浮点溢出也会出现比如mul(mul(mul(X0,X0),X0),X0)在X0量级是1e4时直接变成1e16。解决在function_set里避免直接使用裸log和div改用safe_div、safe_log包一层再传参或者在重写公式时统一用第4.2节的两个函数。跑完fit后第一时间执行np.isinf(new_factor).sum()和np.isnan(new_factor).sum()只要任一列超过1%这组因子直接作废。如果你想保留原始gplearn表达式用于观察可以只改transform之后的后处理不修改function_set但这样每跑一遍都要做一轮清洗工作量翻倍不值得。5.3 多进程并行把内存打爆现象population_size设到5000generations设到10n_jobs4训练到一半进程卡死或者被OOM杀掉换成n_jobs1又慢得离谱。原因gplearn的并行是把种群拆成多份交给joblib每个worker都要持有当前代所有个体的表达式和评估矩阵。代数多了Program对象的引用也被保留在_programs里内存是随pop_size乘generations增长不是线性增长。我之前在一个64G内存的服务器上跑pop_size8000、generations20跑到第11代直接OOM连jupyter内核都崩了。解决先设n_jobs1跑一小时代数确认单worker内存占用再把n_jobs提高到2或4同时把max_samples设成5000或10000限制每次评估的样本量。_programs里历史代的个体如果不复现可以训练完立即只保留_programs[-1]其余置空再存模型。进阶做法是分阶段搜先跑一个pop_size3000扛过5代把表现最好的几百棵树作为下一阶段初始种群再代际更迭。这样可以避免一次性开大种群内存压力小得多。5.4 生成的多个因子之间相关性过高现象n_components设成5得到的5列因子两两相关系数普遍在0.9以上加起来不如只取一列放到多因子模型里有效暴露高度重合。原因SymbolicTransformer的进化目标只针对适应度函数不包含“让输出之间互不相关”的约束。于是种群很快收敛到少数几棵相似树尤其是当某个结构在Spearman指标上特别突出时。比如X0是动量X1是换手率树里最常出现的结构可能是div(abs(X0), X1)一旦这个结构适应度领先其他输出树也纷纷接近这个形态。解决跑完先算候选因子相关矩阵保留每个相关性聚簇里IC最高的一个或者把n_components降到1~2跑多个random_state再合并筛选。如果一定要一次生成多个低相关因子需要自己在外部做正交化gplearn本身控不了。我现在的做法是分两轮第一轮生成20个单因子第二轮用层次聚类合并相关性低于0.5的因子然后只保留每簇的IC最高者。这样得到的因子池多样性比单次n_components20好很多。5.5 因子与未来收益错位最隐蔽的泄漏现象训练集和验证集IC都稳定在0.03以上可到了实盘前模拟盘上换手高得离谱交易成本把收益全部吃掉查看因子明细发现因子在第t日使用的特征其实是t日收盘包含未来信息。原因构造X时rolling窗口没有shift比如用当日收盘价除以5日均线如果均线窗口含当日和之前4天没问题但如果你用未来5日收益率当特征或者用包含次日停牌信息的复权因子泄漏就藏进去了。gplearn的适应度函数不会察觉这是泄漏它只会照着这种“提前知道答案”的特征造出一个高IC假象。解决所有基础特征统一用groupby(stock).shift(1)后再去做滚动计算y用future_ret两个时间戳严格错开。构造完成后随机抽几行人工核对数据再用当天因子值预测昨天收益做反向测试如果IC还高肯定有泄漏。反向测试的做法是把y替换成过去5日收益如果gplearn在这样荒谬的y上还能训练出高IC说明X里含有“未来信息通过某种变换漏进来”的路径需要逐列检查特征。5.6 训练时间过长一晚上都跑不完现象population_size2000generations10数据集是3000只股票×1000天跑了两个小时还在第3代。原因gplearn的时间瓶颈主要在评估所有树的适应度而不是遗传算子。每次评估都要对X做一次完整的前向计算如果一个样本有50个特征population有2000棵树一次迭代就是2000次50维运算。max_samples增加也会放大时间成本。解决优先砍max_samples到5000把n_jobs提到与物理核心数相同关掉verbose可以少一点IO开销如果还是慢换小一点的function_set。function_set里少一个log或sqrt树的组合空间会明显缩小搜索也快得多。最后别在每次调参时都用全量历史先拿最近200天数据探路确定参数后再拉长样本只跑一次正式版。6. 上线前的最后一步滚动重训与单因子验证gplearn挖出的因子衰减得比手工因子快因为它的搜索过程容易被市场风格带着走。我现在的习惯是每两周做一次滚动重训取最近800天行情训练gplearn把历史已上线因子的表达式作为基准新因子先与已有因子做正交化再评估增量IC。经过至少两次滚动窗口都能稳定贡献增量才考虑纳入实盘模型。一个单因子能否上线我只看三个数字IC均值、ICIR、分层单调性。IC均值低于0.02的因子直接丢弃ICIR低于0.3说明稳定性不足分层回测时因子按大小分成五组收益必须单调中间两组可以平但不能出现第一组和第五组收益同向。如果手头没有现成的回测框架单因子验证可以用这段简化的滚动IC代码跑通def check_factor(panel, factor_col, ret_col, date_col): ic_series [] for dt, idx in panel.groupby(date_col).groups.items(): sub panel.loc[idx] ic, _ spearmanr(sub[factor_col], sub[ret_col]) ic_series.append(ic) ic_series pd.Series(ic_series) return ic_series.mean(), ic_series.std() / (ic_series.mean() 1e-8)输出IC和IR再配合月度分组收益就能快速淘汰大多数gplearn因子。算完这几个数我还会把因子表达式打印出来看一眼是否只是把现有因子做了简单非线性变换。如果只是X0 / X1这类结构而X0、X1本身已经在因子库里那它不配占用一个因子额度。做gplearn因子挖掘这一年多我最大的教训是进化算法产出的不是策略只是一批需要被严格检验的候选对象。把验证流程做扎实比调高population_size更值钱。希望帮到你。本文还有配套的精品资源点击获取
返回列表