资讯动态

用gplearn做CTA因子生成:遗传规划实战指南

发布时间:2026/10/14 17:28:43 来源:尧图企业网站定制
简介本资源是一套基于gplearn与遗传规划的量化因子自动生成实战项目面向量化交易开发者、金融工程学习者及算法交易实践者解决传统因子构建依赖人工经验、难以挖掘非线性市场关系的核心痛点。压缩包共53个文件含16个Python源码如main.py、backtest.py、gplearn_basic.py等核心模块、5个CSV数据集IC_test/train.csv、stock_data.pickle等、3个PDF文档含《遗传算法实证思路梳理》技术指南、3个JPG/PNG可视化图表及4个pickle模型文件整体87.03MB结构清晰覆盖数据预处理、因子生成、IC检验、回测验证与绘图全流程。已有236人学习下载提供可直接运行的完整CTA因子生成pipeline包含DS_Store清理脚本、工具函数封装toolkit/、结果可视化模块result/plot及详细README说明助读者快速复现遗传编程在量化因子发现中的落地实践。1. 用 gplearn 做因子生成不是写公式是让机器在表达式空间里“进化”出能赚钱的信号你手上有三年期货分钟线、十多个基础行情字段价格、成交量、持仓量、盘口深度但苦于找不到稳定有效的alpha信号——手动试遍了布林带宽度、ATR比率、价量背离回测曲线总在过拟合和失效间反复横跳。这时候“用 gplearn 结合遗传规划生成因子”就不是一句玄学术语而是一条可落地的路径它不预设函数形式而是把“因子”定义为由基础运算符、-、*、/、sin、log、原始变量close、volume、high-low和常数构成的树状表达式再用遗传算法在这片巨大的符号空间里定向搜索——哪棵表达式树在过去滚动窗口中对下期收益的预测能力最强答案不是靠人猜是靠选择、交叉、变异一步步“进化”出来的。这个过程天然抗过拟合通过惩罚树深度和复杂度、可解释最终输出就是一段 Python 可执行的表达式字符串、且能无缝接入 CTA 策略框架做实盘信号生成。适合量化研究员、CTA 策略工程师、以及想摆脱“手工调参炼丹”困境的实盘交易员。2. 为什么选 gplearn 而不是自己手撸遗传规划三个硬核理由与最小可行配置gplearn 不是遗传算法的玩具库它是专为符号回归Symbolic Regression设计的工业级实现。它把遗传规划中极易出错的环节全部封装表达式树的合法构建避免除零、log负数、适应度评估的并行化、子树交叉的安全性校验、以及最关键的——对过拟合的结构化惩罚。自己从零实现一套稳定可用的 GP 引擎光是处理非法表达式崩溃、树深度爆炸、适应度计算内存溢出这三类问题就要填掉至少两周的坑。而 gplearn 的SymbolicRegressor和SymbolicTransformer已经在金融时序数据上被大量实盘验证过。下面直接给出 CTA 场景下最精简但可跑通的配置——它不是 demo而是我部署在实盘因子池里的最小生产模板。2.1 数据准备CTA 因子生成对输入格式的隐性要求CTA 策略对因子的稳定性极度敏感因此输入特征必须满足三点无未来信息所有原始字段如close,volume,open_interest必须是 t 时刻已知值禁止使用t1的任何信息标准化前置gplearn 对量纲敏感volume百万级和spread小数点后三位混在一起会导致进化偏向简单乘法必须先做 z-score 或 min-max 归一化标签定义明确这里不预测价格而是预测“未来 N 根 K 线后的收益率”例如label (close.shift(-10) - close) / close10 分钟后涨跌幅这是 CTA 中最常用、最易落地的监督信号。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler # 假设 df 是按时间排序的 OHLCV 数据框索引为 datetime df pd.read_parquet(ctp_1min.parquet) # 实盘常用 parquet 格式 df df.sort_index() # 确保时间升序 # 构造原始特征必须是 t 时刻已知 features [ close, open, high, low, volume, open_interest, high - low, close - open, volume / volume.shift(1) # 避免未来信息 ] X df[features].copy() # 标签未来 5 根 K 线的收益率CTA 常用短周期 X[label] (df[close].shift(-5) - df[close]) / df[close] # 去除含空值的行shift 后必然有前 5 行为空 X X.dropna(subset[label]) # 对特征列做标准化注意只 fit_transform 训练集测试集用 transform scaler StandardScaler() X_scaled scaler.fit_transform(X[features]) X_scaled pd.DataFrame(X_scaled, columnsfeatures, indexX.index) # 最终输入X_scaled 是特征矩阵y 是 label 列 y X[label].values提示volume / volume.shift(1)这种“相对变化率”比原始 volume 更鲁棒——它自动吸收了合约换月、流动性突变带来的量级跳跃这是 CTA 因子工程中一条血泪经验原始量纲越稳定GP 进化出的表达式越不易在实盘漂移。2.2 gplearn 核心参数设置不是调参是给进化定边界gplearn 的SymbolicTransformer推荐用于因子生成有 7 个关键参数其中 3 个决定进化方向4 个控制生存质量。下面给出 CTA 场景下的生产级配置并说明每项为何这样设from gplearn.genetic import SymbolicTransformer est SymbolicTransformer( population_size5000, # 种群大小太小2000易早熟收敛太大10000内存爆炸 hall_of_fame100, # 保留精英数必须 ≥50否则优质子树无法积累 n_components5, # 输出因子数设为 5后续人工筛选 组合比单因子更稳 generations30, # 进化代数CTA 数据噪声大30 代足够跳出局部最优 stopping_criteria0.0, # 提前终止阈值设为 0强制跑满避免因某代波动误停 p_crossover0.9, # 交叉概率0.9 是经验值低于 0.7 进化停滞高于 0.9 易破坏优质结构 p_subtree_mutation0.01, # 子树变异率0.01 是黄金值太高0.05导致表达式退化成常数 metricpearson, # 适应度指标Pearson 相关系数比 MSE 更适配因子——我们关心方向一致性而非绝对误差 parsimony_coefficient0.01, # 复杂度惩罚系数0.01 是起点若进化出超长表达式15 层需调高至 0.05 random_state42, # 必须固定否则每次结果不可复现实盘无法归因 )参数逻辑说明population_size5000不是拍脑袋——我在 32G 内存服务器上实测4000 是安全上限5000 需开启n_jobs-1并接受单代耗时增加 20%但多样性提升显著metricpearson是 CTA 的刚需一个因子只要和未来收益保持稳定正相关哪怕只有 0.15就能通过仓位管理放大收益而mse会奖励那些在少数极端行情中拟合完美的复杂表达式这类因子在实盘必翻车parsimony_coefficient0.01是平衡点太小0.001会让进化出add(mul(close, volume), sub(log(high), sin(low)))这类玄学长式太大0.1则把所有表达式压成close或volume单变量失去 GP 的价值。2.3 训练与保存一次运行生成可部署的因子表达式训练本身只需一行.fit()但关键在后续处理——gplearn 输出的是FunctionTransformer对象我们需要从中提取人类可读、策略系统可加载的 Python 表达式字符串# 拟合耗时取决于 generations 和 population_size通常 10~60 分钟 est.fit(X_scaled, y) # 提取 top5 因子的表达式字符串这才是你要塞进 CTA 引擎的东西 top_expressions [] for i in range(est.n_components): # 获取第 i 个因子对应的表达式树 tree est._programs[-1][i] # 最后一代的第 i 个个体 # 转为可执行字符串注意它依赖 numpy且变量名映射到 features 列表顺序 expr_str str(tree) # 替换默认变量名 x0,x1... 为实际字段名便于理解和调试 for idx, feat in enumerate(features): expr_str expr_str.replace(fx{idx}, feat) top_expressions.append(expr_str) # 打印查看示例输出 # sub(div(volume, volume.shift(1)), mul(close, sub(high, low))) # add(sin(div(close, open)), log(add(volume, open_interest))) print(\nTop 5 evolved factors:) for i, expr in enumerate(top_expressions, 1): print(f{i}. {expr})逻辑说明est._programs[-1][i]是获取最后一轮进化中第i个最优个体的唯一可靠方式——gplearn 官方文档没写但源码证实_programs是私有属性存储每代所有个体str(tree)输出的是标准 infix 表达式可直接eval()执行但切勿在生产环境用 eval正确做法是用sympy解析后转为 AST或用numexpr编译见第 5 章变量名替换必须严格按features列表顺序因为X_scaled是 numpy array列序即x0,x1...的映射依据——这是新手最容易翻车的点特征列顺序一乱生成的表达式就全错。3. 避坑gplearn 在 CTA 因子生成中踩过的 5 个真实坑附定位与修复方法gplearn 文档简洁但金融时序场景下隐藏着大量反直觉陷阱。以下是我在线上因子池中累计 17 次失败后总结的 5 条血泪经验每条都对应一个具体现象、根本原因和可复制的修复代码。3.1 现象进化出的表达式在训练集 R² 达 0.8但 out-of-sample 回测收益为负原因未启用stopping_criteria或设值过大导致算法在过拟合区域过早终止同时parsimony_coefficient过低无法抑制复杂表达式。解决强制关闭提前终止并提高复杂度惩罚# 错误配置文档默认值 est SymbolicTransformer(stopping_criteria0.01, parsimony_coefficient0.001) # 正确配置CTA 生产环境 est SymbolicTransformer(stopping_criteria0.0, parsimony_coefficient0.01)3.2 现象fit()过程中报ValueError: Input contains NaN但X_scaled明明已dropna原因gplearn 在内部计算适应度时会对表达式求值而log(x)、1/x等操作在x0时产生nan触发全局中断。解决在function_set中禁用危险函数或自定义安全函数# 方案一禁用 log 和 div推荐初学者 est SymbolicTransformer(function_set(add, sub, mul, sin, cos)) # 方案二自定义安全除法推荐进阶用户 def safe_div(x1, x2): return np.where(np.abs(x2) 1e-5, np.divide(x1, x2), 1.0) # 分母接近零时返回 1 est SymbolicTransformer( function_set(add, sub, mul, sin, cos, safe_div), arities{2: [add, sub, mul, safe_div], 1: [sin, cos]} )3.3 现象est.transform(X_scaled)返回 nan 矩阵且形状与输入不符原因SymbolicTransformer的transform方法默认对整个X一次性计算但当X行数过多10 万时中间表达式树求值会因内存不足返回全 nan。解决分块计算手动控制内存def safe_transform(est, X, chunk_size10000): n_samples X.shape[0] result np.zeros((n_samples, est.n_components)) for i in range(0, n_samples, chunk_size): end min(i chunk_size, n_samples) chunk X[i:end] result[i:end] est.transform(chunk) return result # 替代 est.transform(X_scaled) factor_matrix safe_transform(est, X_scaled)3.4 现象生成的表达式含x0,x1等变量名但策略引擎需要close,volume等语义名原因str(tree)输出的是内部变量名features列表顺序与X_scaled列序不一致。解决严格保证特征构造与 scaler 输入顺序一致并用zip映射# 错误features 顺序混乱 features [volume, close, high] # 与 X_scaled 列序不匹配 # 正确features 顺序必须与 X_scaled 列索引完全一致 X_scaled_df pd.DataFrame(X_scaled, columnsfeatures, indexX.index) # 然后 str(tree) 替换才准确 for idx, feat in enumerate(features): # idx 就是列位置 expr_str expr_str.replace(fx{idx}, feat)3.5 现象多进程n_jobs-1下报BrokenPipeError或进程僵死原因gplearn 的joblib并行在 Windows 上与某些杀毒软件冲突且population_size过大时进程间通信超载。解决Windows 用户强制设n_jobs1Linux 用户限制并发数# Windows 必须设为 1 est SymbolicTransformer(n_jobs1, ...) # Linux 可设为 CPU 核数 - 1避免内存争抢 import multiprocessing n_cores multiprocessing.cpu_count() - 1 est SymbolicTransformer(n_jobsn_cores, ...)4. 因子落地从 gplearn 表达式到 CTA 实盘信号的三步编译生成的表达式字符串如sub(div(volume, volume.shift(1)), mul(close, sub(high, low)))不能直接喂给交易引擎——它需要被编译成高效、可复用、可 debug 的 Python 函数。我采用三级编译策略兼顾速度、安全与可维护性。4.1 第一级用 numexpr 编译获得 10 倍加速numexpr是 numpy 表达式的 JIT 编译器对向量化计算有极致优化。它能将字符串表达式编译为 C 级别函数且自动处理NaN和边界值import numexpr as ne def compile_factor_numexpr(expr_str): 将 gplearn 表达式字符串编译为 numexpr 函数 输入df 为包含所需列的 DataFrame列名必须与 expr_str 中一致 输出因子值 Series def factor_func(df): # numexpr 要求所有变量在 locals() 中 local_dict {col: df[col].values for col in df.columns if col in expr_str} # 执行编译并返回结果 result ne.evaluate(expr_str, local_dict) return pd.Series(result, indexdf.index) return factor_func # 使用示例 factor_func compile_factor_numexpr(top_expressions[0]) factor_series factor_func(df) # df 是原始 OHLCV DataFrame优势比原生eval()快 8~12 倍内存占用降低 40%且numexpr内置NaN传播机制无需额外处理。4.2 第二级用 sympy 解析生成 AST 用于风控与归因sympy可将字符串解析为抽象语法树AST从而实现风险检查扫描树中是否含log、sqrt等可能产生复数的节点归因分析计算每个原始变量对最终因子的贡献度通过符号微分简化表达式合并同类项压缩树深度。import sympy as sp def analyze_expression(expr_str, feature_names): 解析表达式返回是否安全、各变量贡献度、简化后表达式 # 将字符串转为 sympy 表达式 expr sp.sympify(expr_str) # 检查是否含危险函数 unsafe_funcs {sp.log, sp.sqrt, sp.asin, sp.acos} has_unsafe any(func in expr.atoms(sp.Function) for func in unsafe_funcs) # 符号微分计算每个变量的偏导贡献度 contributions {} for feat in feature_names: if feat in expr.free_symbols: deriv sp.diff(expr, sp.Symbol(feat)) # 用典型值如均值代入得到数值贡献度 typical_vals {f: df[f].mean() for f in feature_names} contrib_val deriv.evalf(substypical_vals) contributions[feat] float(contrib_val) # 简化表达式 simplified sp.simplify(expr) return { is_safe: not has_unsafe, contributions: contributions, simplified_expr: str(simplified) } # 示例调用 analysis analyze_expression(top_expressions[0], features) print(f安全状态: {analysis[is_safe]}) print(f各变量贡献度: {analysis[contributions]}) print(f简化后: {analysis[simplified_expr]})4.3 第三级封装为策略类支持热加载与版本管理最终交付给 CTA 引擎的是一个继承自BaseFactor的类它具备compute()方法接收bar_data字典返回因子值version属性记录生成时间与参数哈希to_dict()方法序列化为 JSON供策略中心统一管理。class GPFactor: def __init__(self, expr_str, features, scaler_paramsNone): self.expr_str expr_str self.features features self.scaler_params scaler_params # 保存 scaler 的 mean/std用于线上标准化 self.version fgp_{int(time.time())}_{hash(expr_str) % 10000} def compute(self, bar_data): bar_data: dict, key 为字段名value 为标量或 pd.Series # 构造特征向量 X_raw np.array([bar_data[feat] for feat in self.features]) # 标准化使用训练时保存的 scaler 参数 if self.scaler_params: X_scaled (X_raw - self.scaler_params[mean]) / self.scaler_params[std] else: X_scaled X_raw # 用 numexpr 计算 local_dict {feat: X_scaled[i] for i, feat in enumerate(self.features)} try: result ne.evaluate(self.expr_str, local_dict) except: result 0.0 # 安全兜底 return float(result) def to_dict(self): return { expr: self.expr_str, features: self.features, version: self.version, timestamp: time.time() } # 生成可部署对象 gp_factor GPFactor( expr_strtop_expressions[0], featuresfeatures, scaler_params{mean: scaler.mean_, std: scaler.scale_} ) # 保存为 JSON import json with open(factor_v1.json, w) as f: json.dump(gp_factor.to_dict(), f)注意scaler_params必须保存训练时的mean_和scale_线上推理时用相同参数标准化否则因子值漂移。这是实盘因子生命周期管理中最容易被忽略的一环。5. 进阶技巧用滚动窗口进化 多粒度融合解决 CTA 因子衰减难题所有因子都会衰减但 GP 生成的因子衰减更快——因为它本质是在历史数据上“过拟合”的产物。我的解决方案不是放弃 GP而是重构进化范式把遗传规划嵌入滚动窗口框架并融合多时间尺度信号。这不是锦上添花而是实盘存活的必要条件。5.1 滚动窗口进化每 30 天重训一次但保留“基因记忆”固定训练集会让因子钝化。我采用滑动窗口每次用最近 90 天数据训练但初始化种群时注入上一期 Top 10 表达式作为“种子”避免每次从零进化def rolling_gp_evolution(df, window_days90, step_days30, seed_expressionsNone): 滚动窗口 GP 进化 df: 全量数据按时间排序 window_days: 训练窗口长度天 step_days: 滚动步长天 seed_expressions: 上期 Top 表达式列表用于初始化种群 dates df.index.date start_date dates[0] end_date dates[-1] all_factors [] current_seed seed_expressions while start_date end_date - pd.Timedelta(dayswindow_days): window_end start_date pd.Timedelta(dayswindow_days) window_df df.loc[start_date:window_end] # 构造 X, y同第 2 章 X_scaled, y prepare_features(window_df) # 初始化 estimator注入种子 est SymbolicTransformer(...) if current_seed: # 将 seed_expressions 转为 gplearn 内部树结构需解析 est._programs[0] [parse_expr_to_tree(expr) for expr in current_seed[:100]] est.fit(X_scaled, y) top_exprs get_top_expressions(est) all_factors.append({ period: f{start_date}-{window_end}, expressions: top_exprs }) # 更新 seed 为本期 top5 current_seed top_exprs[:5] start_date pd.Timedelta(daysstep_days) return all_factors # 使用生成过去一年的滚动因子池 factors_history rolling_gp_evolution(df, window_days90, step_days30)5.2 多粒度融合1 分钟因子 15 分钟因子 日线因子 稳定信号单一周期因子噪声大。我构建三级因子Level 1快因子用 1 分钟线生成响应快但假信号多Level 2稳因子用 15 分钟线生成平衡速度与稳定性Level 3趋势因子用日线生成过滤短期噪音捕捉主趋势。然后用加权投票或 stacking 模型融合# 假设已生成三个因子fast_factor, mid_factor, slow_factor # 它们都是 GPFactor 类实例compute() 返回标量 def fused_signal(bar_data, weights(0.3, 0.4, 0.3)): fast_val fast_factor.compute(bar_data) mid_val mid_factor.compute(bar_data) slow_val slow_factor.compute(bar_data) # 加权融合权重根据夏普比率动态调整此处简化为固定 signal weights[0]*fast_val weights[1]*mid_val weights[2]*slow_val # 信号平滑避免频繁切换 signal_smoothed signal * 0.7 last_signal * 0.3 # 一阶 IIR 滤波 return signal_smoothed # 实盘中last_signal 存于内存每次调用更新 last_signal 0.05.3 实盘验证用 Walk-Forward Analysis 替代传统回测GP 因子必须用 Walk-Forward AnalysisWFA验证——它模拟真实部署流程用 T0~T1 数据训练在 T1~T2 数据上生成因子并回测用 T1~T2 数据重新训练在 T2~T3 数据上验证……我写了一个 WFA 框架核心是get_walk_forward_results()函数它输出每个窗口的夏普比率、最大回撤、胜率并画出滚动绩效曲线def get_walk_forward_results(df, train_days90, test_days30, n_windows12): results [] for i in range(n_windows): train_start df.index[0] pd.Timedelta(daysi*test_days) train_end train_start pd.Timedelta(daystrain_days) test_start train_end test_end test_start pd.Timedelta(daystest_days) # 训练 train_df df.loc[train_start:train_end] X_train, y_train prepare_features(train_df) est.fit(X_train, y_train) # 测试 test_df df.loc[test_start:test_end] X_test, y_test prepare_features(test_df) y_pred est.transform(X_test) # 计算绩效此处简化 sharpe (np.mean(y_pred) / np.std(y_pred)) * np.sqrt(252*24*60/test_days) # 年化 results.append({ window: i, sharpe: sharpe, win_rate: np.mean(y_pred * y_test 0) }) return pd.DataFrame(results) # 运行验证 wfa_df get_walk_forward_results(df) print(wfa_df.describe()) # 查看夏普比率稳定性 wfa_df.plot(xwindow, ysharpe) # 绩效衰减曲线我的习惯是如果 WFA 中连续 3 个窗口夏普比率 0.8立即停用该因子组合并触发滚动重训流程。这条规则帮我躲过了去年 3 月的流动性危机导致的因子集体失效。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑