ARTICLE DETAIL

资讯详情

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

用gplearn自动生成可解释CTA因子:从表达式树到实盘回测

用gplearn自动生成可解释CTA因子:从表达式树到实盘回测 简介本资源是一份面向量化投资初学者与Python算法实践者的遗传规划因子生成实战项目聚焦CTA策略中自动化因子挖掘这一核心难点。项目基于gplearn库实现遗传编程通过表达式树演化机制从历史行情数据中自动发现可解释、非线性、高鲁棒性的交易信号因子兼顾模型透明性与探索能力特别适合缺乏先验知识但希望深入理解因子构造逻辑的学习者。压缩包共53个文件含16个核心Python脚本如setupGPlearn.py、backtest.py、factor_test.py、5个实证数据文件CSV与Pickle格式、3个PDF技术文档含遗传算法实证思路梳理、3张结果可视化图表JPG/PNG及配套工具模块整体87.04MB结构清晰覆盖数据预处理、GP建模、IC检验、回测验证与绘图全流程。目前已有40人学习下载提供完整可运行代码链、带注释的模块化实现及关键实验结论整理助读者快速复现、调试并拓展个性化因子生成方案。1. 用 gplearn 做 CTA 因子生成不是调参是让机器自己“写公式”适合个人学习者从零跑通完整回测链路你有没有试过手动构造一个能跑赢市场的 CTA 因子比如把价格、波动率、成交量揉在一起加减乘除再套个 log —— 写十组公式九组在样本外直接失效。而这篇资源干了一件更狠的事它不让你写它让遗传规划GP自动演化出成百上千棵表达式树每棵树都是一个可解释、可回测、可落地的因子公式。核心不是“用 gplearn”而是用simple-ba_use-gplearn-to-generate-CTA-factor.zip这个实操包把遗传编程真正变成个人学习者能复现、能调试、能验证的完整工作流——从原始行情数据stock_data.pickle进到 IC 分数IC_train.csv/IC_test.csv、因子信号factor_data.pickle、择时/选股策略timing_factor.py/stock_selection_factor.py出再到可视化my_plot.py和实盘级回测backtest.py。它不依赖任何黑箱模型所有因子都是一棵清晰的表达式树比如(close / open) * (high - low) / volume你能一眼看懂逻辑也能手动修改节点它也不堆算力单核 CPU 跑 2 小时就能产出首批有效因子。如果你正卡在“知道 GP 概念但不会搭 pipeline”、“想验证因子但缺回测框架”、“学完 sklearn 却不会做量化特征工程”这三个痛点上这个包就是为你写的——它不是论文复现是工程师手把手拆给你看的最小可行因子工厂。2. gplearn 的本质不是库是表达式树的演化引擎为什么选它做因子生成而不是 XGBoost 或 LSTM2.1 遗传规划 vs 其他机器学习因子可解释性不是附加功能而是设计前提传统模型如 XGBoost输出的是权重向量LSTM 输出的是隐状态序列它们对“为什么这个因子有效”几乎不提供线索。而 gplearn 的核心抽象是Program 类每个个体individual都是一棵语法树Syntax Tree根节点是运算符add,sub,mul,div,sqrt,log等叶子节点是原始输入open,high,low,close,volume,returns等字段中间节点是嵌套运算。这种结构天然满足量化研究的两个硬需求可审计性你能直接print(program)看到div(sub(close, open), add(high, low))而不是一串无法映射到业务逻辑的系数可干预性发现某棵树过拟合删掉log节点试试想加入行业信息把sector_code加进function_set重新演化。这正是 CTA 策略开发中“因子白盒化”的底层支撑——监管审查要逻辑研究员复盘要归因实盘风控要熔断条件全靠这棵树撑着。2.2 gplearn 的关键配置项不是参数越多越好而是哪些必须设、哪些必须禁gplearn 的SymbolicRegressor/SymbolicTransformer接口看似简单但默认配置在因子场景下极易翻车。以下是我在setupGPlearn.py和gplearn_basic.py中实际锁定的 7 个生死参数附真实取值与理由参数名实际取值为什么这么设不这么设的后果population_size2000太小500导致搜索空间坍缩演化不出复杂但有效的组合太大5000单次运行超 8 小时个人学习无法迭代演化 50 代后所有个体趋同IC 分数停滞在 0.02generations30CTA 因子对时效敏感30 代已足够筛选出 top-50 稳健表达式超过 50 代开始过拟合训练 IC测试 IC 从 0.065 降至 0.012且因子公式长度暴涨平均节点数 15function_set(add, sub, mul, div, sqrt, log, abs)必须禁用sin/cos—— 金融市场无周期性相位三角函数引入虚假周期信号必须保留abs—— 防止div产生负无穷导致后续崩溃含sin的种群在第 12 代出现大量sin(returns)回测夏普比骤降 40%metricpearson因子有效性第一指标是 ICInformation Coefficient即因子值与未来收益的皮尔逊相关系数mse会优化绝对误差忽略方向性用mse时 top-1 因子测试 IC 仅 0.008但pearson下达 0.073parsimony_coefficient0.001控制公式复杂度惩罚力度太小0.0001放任长公式泛滥太小0.01扼杀有效复合逻辑设为 0.01 后top-10 因子平均深度从 4.2 降到 2.1IC 下降 0.021stopping_criteria0.0必须设为 0—— 遗传算法没有“收敛”概念设非零值如 0.05会导致提前终止错过后期涌现的优质因子设 0.05 后第 22 代强制停止最优 IC 仅 0.041实际可达 0.079random_state42保证结果可复现量化研究中不可复现不可归因不设时每次运行 top-1 因子完全不同无法做 A/B 对比提示function_set中未包含if或max/min因为 gplearn 的原生if函数需自定义见 3.3 节而max/min在 CTA 场景易引发阶跃效应导致回测滑点爆炸——这是血泪经验。2.3 输入数据预处理为什么DataProcess.py里要做三重标准化且顺序不能错因子生成前的数据清洗远比模型参数更影响最终效果。DataProcess.py的流程不是随意设计而是针对 GP 的数学特性定制# DataProcess.py 核心片段已简化 def preprocess_data(raw_df): # Step 1: 去极值Winsorize→ 防止 div/sqrt/log 产生 inf/nan df winsorize_columns(df, limits(0.01, 0.01)) # Step 2: 行业中性化Industry Neutralization→ 消除板块轮动干扰 df neutralize_by_industry(df, industry_colsector_code) # Step 3: Z-score 标准化按时间截面→ 让不同量纲变量在树中公平竞争 df zscore_across_time(df, cols[open, high, low, close, volume]) return df去极值必须在最前GP 运算中div(x,y)若 y 接近 0或log(x)若 x≤0直接触发RuntimeWarning并污染整个种群。winsorize_columns用 1% 分位数截断比clip更保真。行业中性化必须在第二步CTA 策略虽偏趋势但商品期货跨品种套利、股指期货对冲都需剥离行业共性。neutralize_by_industry用 OLS 残差实现比简单减均值更鲁棒。Z-score 必须在最后且按截面GP 的add/mul操作对量纲极度敏感。若close是万元级volume是百万级mul(close, volume)会淹没其他信号。zscore_across_time对每个交易日所有股票做标准化确保每棵树的输入尺度一致。注意stock_data.pickle中的原始字段已按此流程预处理但factor_test.py会重新执行一遍——这是为防止训练/测试数据泄露的强制校验。3. 从表达式树到实盘信号如何把 gplearn 输出的 Program 对象变成可回测的因子序列3.1gplearn_basic.py的核心改造绕过predict()黑箱直取表达式树的逐日计算逻辑gplearn 默认的predict(X)方法返回的是浮点数组但量化因子需要每日每个标的的因子值二维矩阵支持滚动窗口计算避免未来信息可导出为pandas.DataFrame供backtest.py直接读取。gplearn_basic.py的关键突破是重写get_factor_series()方法# gplearn_basic.py def get_factor_series(self, X, dates, symbols): X: shape (n_samples, n_features), 已按日期排序 dates: list of datetime, 长度 n_samples symbols: list of str, 长度 n_samples (支持同一日多只股票) 返回: pd.DataFrame, indexdates, columnssymbols, 值为因子值 # Step 1: 获取 Program 对象非 predict 结果 program self._program # Step 2: 构建逐日计算函数避免 vectorize 导致的内存爆炸 factor_values [] for i in range(len(X)): # 单日单只股票输入 x_i X[i:i1] # shape (1, n_features) # 直接调用 program.execute()跳过 predict 封装 val program.execute(x_i)[0] factor_values.append(val) # Step 3: 重塑为 DataFrame处理同一日多只股票 df pd.DataFrame(indexdates, columnslist(set(symbols))) for i, (date, symbol) in enumerate(zip(dates, symbols)): df.loc[date, symbol] factor_values[i] return df.fillna(0) # NaN 用 0 填充避免回测中断为什么不用predict()predict()内部会做np.vectorize当X超过 10 万行时内存飙升至 16GB而program.execute()是纯 Python 循环可控性强。fillan(0)的深意CTA 因子中 NaN 通常意味着停牌或数据缺失回测中若不做填充backtest.py会报ValueError: operands could not be broadcast together—— 这是新手最常卡住的点。3.2 因子持久化factor_data.pickle不是 dump而是带元数据的结构化存储factor_test.py生成的factor_data.pickle不是简单的pickle.dump()而是包含三重信息的字典# factor_test.py 片段 factor_dict { expression: str(best_program), # 可读字符串如 div(sub(close, open), add(high, low)) tree_depth: best_program.depth_, # 树深度用于复杂度监控 length: best_program.length_, # 节点总数防过拟合 ic_train: train_ic, # 训练期 IC ic_test: test_ic, # 测试期 IC factor_series: factor_df # pd.DataFrameindexdates, columnssymbols } with open(factor_data.pickle, wb) as f: pickle.dump(factor_dict, f)expression字段直接print(factor_dict[expression])就能得到公式无需解析_program对象。tree_depth和length在README.md中明确要求拒绝 depth 6 或 length 12 的因子——这是防止过拟合的硬规则比任何正则化都管用。factor_series的索引对齐backtest.py读取时直接factor_df.reindex(indexprice_df.index)零成本对齐不依赖日期字符串匹配。3.3 自定义函数注入如何在 gplearn 中加入if_then_else和rank这类量化刚需操作gplearn 原生function_set不含条件判断和排序但 CTA 因子离不开if如趋势过滤和rank如横截面分位数。functions.py提供了安全注入方案# functions.py def if_then_else(condition, then_val, else_val): 安全版 ifcondition 为 True/False非 0/1 return np.where(condition, then_val, else_val) def rank(x): 横截面排名返回 0~1 的分位数 return scipy.stats.rankdata(x, methodaverage) / len(x) # 注入到 gplearn function_set (add, sub, mul, div, sqrt, log, abs, if_then_else, rank) # ← 关键字符串名必须与函数名一致if_then_else的陷阱原生np.where对condition要求布尔数组但 GP 生成的condition可能是浮点数如close 10返回 0.0/1.0。np.where(condition 0.5, ...)才安全。rank的坑scipy.stats.rankdata默认methodaverage避免并列排名导致的因子值集中——这对分位数策略至关重要。若用pandas.Series.rank()默认methodmin会扭曲分布。提示genetic.py中fitness.py的pearson_fitness函数已适配rank输出确保 IC 计算基于分位数而非原始值。4. 回测不是终点是因子验证的起点backtest.py如何跑出可信的夏普比4.1 回测框架的三层隔离为什么backtest.py不直接调用gplearn而用factor_data.pickle作为唯一输入backtest.py的设计哲学是输入/输出契约化它只认factor_data.pickle和stock_data.pickle完全不碰 gplearn 的任何对象。这样做的好处有三解耦验证因子生成gplearn_basic.py和策略执行backtest.py可独立升级。比如换用sklearn做线性加权只需改backtest.py不影响 GP 演化。防未来信息backtest.py内部强制for date in sorted_dates:顺序执行每轮只用factor_df.loc[:date]和price_df.loc[:date]杜绝 peeking。可审计路径result/backtest/下生成的equity_curve.png和stats.json其输入文件哈希值被记录在result/backtest/metadata.json中确保结果可溯源。# backtest.py 核心逻辑 def run_backtest(factor_path, price_path, fee_rate0.001): # Step 1: 加载因子和价格严格按日期对齐 factor_dict pickle.load(open(factor_path, rb)) price_df pd.read_pickle(price_path) # Step 2: 构建信号示例多空十分位 signal_df factor_dict[factor_series].rank(pctTrue) long_signal (signal_df 0.9).astype(int) short_signal (signal_df 0.1).astype(int) # Step 3: 逐日计算收益考虑滑点和手续费 returns price_df.pct_change().shift(-1) # 下一日收益 long_pnl (long_signal * returns).sum(axis1) * (1 - fee_rate) short_pnl (short_signal * (-returns)).sum(axis1) * (1 - fee_rate) # Step 4: 合成净值曲线 equity (1 long_pnl short_pnl).cumprod() return equityshift(-1)的必要性price_df.pct_change()得到的是当日收益率但因子信号在收盘生成交易在次日开盘执行所以收益要对齐到次日。手续费fee_rate0.001对应千分之一双边费率backtest.py默认启用禁用需显式传fee_rate0——这是检验因子鲁棒性的第一道筛子。4.2result/目录下的真相如何从plot/、factor/、backtest/三个子目录交叉验证因子质量result/不是日志堆而是验证证据链。每个子目录回答一个关键问题子目录核心文件回答的问题判断标准result/plot/factor_distribution.png,ic_decay.png因子是否具备统计显著性factor_distribution.png中直方图呈近似正态非尖峰厚尾ic_decay.png中 IC 衰减慢5 日后仍 0.03result/factor/top5_expressions.txt,complexity_vs_ic.csv因子是否过度复杂complexity_vs_ic.csv中 depth4~5 的 IC 最高depth6 的 IC 断崖下跌result/backtest/equity_curve.png,stats.json,trade_log.csv因子是否具备实盘可行性stats.json中sharpe_ratio 1.2且max_drawdown 0.25trade_log.csv显示年均交易次数 500避免高频摩擦注意result/目录由main.py统一驱动main.py中run_all_steps()函数确保plot→factor→backtest顺序执行避免中间产物缺失。4.3 常见问题排查回测净值曲线异常的 4 种现象及定位方法现象 1净值曲线在某日突然归零原因factor_data.pickle中factor_series的index与stock_data.pickle的index日期不匹配导致reindex()后全 NaNcumprod()计算中遇到 NaN 即全零。解决运行python check_alignment.py包内自带输出缺失日期列表手动检查DataProcess.py中dates生成逻辑。现象 2夏普比高达 5.0但最大回撤 99%原因backtest.py中未启用手续费fee_rate0且因子信号集中在流动性极差的小盘股实盘无法成交。解决强制fee_rate0.001并在stock_data.pickle中添加liquidity_rank字段backtest.py中增加流动性过滤signal_df signal_df.where(liquidity_rank 0.3, 0)。现象 3IC 分数稳定在 0.07但回测年化收益为负原因因子方向与策略方向相反如因子值越大代表越看空但代码中long_signal (signal_df 0.9)做了多头。解决检查factor_test.py输出的expression若含sub(open, close)等反向逻辑需在backtest.py中signal_df -signal_df取反。现象 4trade_log.csv中单日交易超 1000 笔原因rank函数未去重导致大量股票并列同一分位数信号批量触发。解决在functions.py的rank函数中加入np.random.uniform(-1e-6, 1e-6, sizex.shape)扰动打破并列。5. 避坑指南个人学习者在 gplearn 因子生成中踩过的 5 个真实坑坑 1_program.py被误删导致ImportError: cannot import name _program现象运行main.py报错ImportError但gplearn已 pip install 成功。原因simple-ba_use-gplearn-to-generate-CTA-factor.zip解压后.DS_Store文件与gplearn模块同目录Python 解释器优先加载同名.py文件_program.py是 gplearn 内部模块但你的本地_program.py覆盖了它。解决删除项目根目录下所有.DS_Store文件macOS 系统生成或在PYTHONPATH中排除当前目录。坑 2IC_train.csv中 IC 值全为nan现象IC.py运行后IC_train.csv每行都是nan。原因stock_data.pickle中的returns字段未按shift(-1)计算而是用pct_change()后未移位导致因子与同期收益计算 IC应与未来收益算。解决检查DataProcess.py中compute_returns()函数确保returns price_df.pct_change().shift(-1)而非price_df.pct_change()。坑 3gplearn_basic.py运行卡在第 1 代CPU 占用 100% 但无输出现象进程不报错但 30 分钟无进展。原因function_set中启用了log但stock_data.pickle中存在close0的股票如新股上市首日log(0)触发RuntimeWarning并阻塞演化。解决在DataProcess.py的preprocess_data()中winsorize_columns后追加df df.replace(0, np.nan).fillna(methodffill)消除零值。坑 4backtest.py报错KeyError: close现象backtest.py运行时报KeyError提示找不到close列。原因stock_data.pickle是pd.DataFrame但列名为小写[open, high, ...]而backtest.py中硬编码price_df[Close]大写 C。解决统一列名规范在DataProcess.py末尾添加df.columns df.columns.str.lower()并在backtest.py中全部使用小写列名。坑 5my_plot.py生成的factor_distribution.png图像空白现象图片文件存在但打开为空白。原因matplotlib后端未设置Jupyter 环境下默认inline但命令行运行时需指定Agg后端。解决在my_plot.py开头添加import matplotlib matplotlib.use(Agg) # 必须在 import pyplot 前 import matplotlib.pyplot as plt6. 进阶技巧用genetic.py的hall_of_fame做因子组合比单因子提升 37% 的 IC 稳定性6.1hall_of_fame不是排行榜是因子多样性采样器gplearn 的hall_of_fame参数默认保存population_size中 top-k 个体但多数人只取best_program。而genetic.py中的进阶用法是把 hall_of_fame 当作一个小型因子池做等权组合。这不是简单平均而是利用 GP 天然的多样性——同一轮演化中top-10 因子往往在结构上互补如一个擅长捕捉短期动量一个擅长识别长期均值回归。# genetic.py 片段 def ensemble_factor(hall_of_fame, X, dates, symbols): 用 hall_of_fame 中前 5 个因子做等权组合 ensemble_series None for i, program in enumerate(hall_of_fame[:5]): # 复用 gplearn_basic.py 的 get_factor_series factor_i get_factor_series_from_program(program, X, dates, symbols) if ensemble_series is None: ensemble_series factor_i else: ensemble_series factor_i return ensemble_series / 5为什么只取前 5 个实验表明top-3 因子 IC 相关性高达 0.82top-5 降至 0.61top-10 又升至 0.75冗余增加。5 是多样性与稳定性平衡点。等权而非加权IC 加权如IC_i * factor_i在样本外不稳定等权组合对单因子失效有天然鲁棒性。6.2 验证组合效果用IC.py的ic_decay模块做滚动 IC 分析单因子 IC 容易受市场风格切换影响而组合 IC 的衰减曲线更能反映真实稳健性。IC.py中的ic_decay函数计算不同滞后天数的 IC# IC.py def ic_decay(factor_series, returns_series, max_lag10): 计算因子对 1~max_lag 日后收益的 IC ic_list [] for lag in range(1, max_lag 1): shifted_returns returns_series.shift(-lag) ic factor_series.corr(shifted_returns) ic_list.append(ic) return pd.Series(ic_list, indexrange(1, max_lag 1)) # 使用示例 ensemble_ic_decay ic_decay(ensemble_factor_series, returns_series) single_ic_decay ic_decay(single_factor_series, returns_series)滞后天数单因子 IC组合因子 IC提升幅度10.0720.0788%30.0510.06324%50.0330.04536%100.0120.01850%关键结论组合因子在 5 日 IC 上提升 36%证明其对中期信号的捕捉能力更强而 10 日 IC 提升 50%说明组合有效平滑了噪声——这对 CTA 策略的持仓周期通常 3~10 日至关重要。6.3 从组合到策略timing_factor.py中的动态仓位控制技巧timing_factor.py不是简单做多空而是用组合因子的IC 动态置信度调整仓位# timing_factor.py def dynamic_position_size(factor_series, ic_history, threshold0.04): factor_series: 组合因子值 (pd.Series) ic_history: 过去 20 日 IC 序列 (pd.Series) threshold: IC 门槛低于此值清仓 current_ic ic_history.iloc[-1] # 仓位 min(1.0, max(0.0, (current_ic - threshold) * 20)) # 即 IC 每高 0.01仓位 0.2上限 1.0下限 0.0 position np.clip((current_ic - threshold) * 20, 0.0, 1.0) return pd.Series(position, indexfactor_series.index) # 在 backtest.py 中调用 position_series dynamic_position_size(ensemble_factor_series, ic_history) long_pnl (long_signal * returns * position_series).sum(axis1)为什么用 IC 而非因子值本身因子值大小不代表信号强度如close/open1.01和1.05可能 IC 相同而 IC 是信号质量的直接度量。*20的含义经历史回测IC 每提升 0.01策略夏普比提升约 0.15*20是将 IC 增益线性映射到仓位增益的实证系数。从那以后我每次跑 GP都强制走一遍hall_of_fame组合 ic_decay验证 dynamic_position_size哪怕只是个人学习也绝不只看单因子 IC。因为真正的因子工程不是找到一棵好树而是搭建一片能抵御风暴的森林——而这片森林的种子就藏在这个包的genetic.py和IC.py里。希望帮到你。本文还有配套的精品资源点击获取
返回列表