news 2026/10/3 7:09:41

遗传编程与遗传算法在算法交易中的分工与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
遗传编程与遗传算法在算法交易中的分工与实践

简介:一款以苹果股票价格预测为研究对象的算法交易程序,采用Python实现,面向对量化交易与进化计算感兴趣的开发者。项目内置两种独立策略:遗传编程模块通过进化树形种群最小化预测价格与实际价格的误差,融合纳斯达克、苹果及标普市场信号;遗传算法模块则进化字符模型,用于判断次日股价涨跌。压缩包共40个文件,其中含21个py脚本、8个csv历史行情样本、7个txt设计说明及2个md文档,整体大小约1.21MB,目录按两种算法分模块组织,并配有算法设计说明和运行指南。目前已有330人学习下载。读者可对照代码梳理数据预处理、信号构造、进化迭代与回测评估的完整流程,也可在此框架上替换股票代码开展二次实验,是掌握算法交易与进化计算的实用参考。

1. 算法交易程序里的遗传编程和遗传算法:这不是 AI 荐股,是让程序自己长出因子表达式

拿到这个标题的读者,多半不是在找“稳赚策略”,而是想搞清楚:遗传编程和遗传算法在同一套算法交易程序里到底怎么分工、代码怎么写、跑出来靠不靠谱。先给结论:用遗传编程(GP)预测股票价格,本质是从历史行情里演化出一组数学表达式,比如mul(add(mom20, vol20), log_abs(ret60))这样的因子公式;遗传算法(GA)则在表达式定下来之后,专门去搜参数——止损放多少、持仓拿几天、开仓阈值设多高。GP解决“长什么样”,GA解决“具体调成什么值”。它不是黑匣子,每一步演化出来的结果都能打印成公式、写进文档、放进回测。适合有 Python 基础、想自己搭一套可解释量化策略框架的从业者,也适合想验证“进化计算在因子挖掘上到底值不值得投入”的团队。它不是荐股工具,不承诺收益率,它是一套你能控制每一步的预测与回测流水线。

2. 遗传编程和遗传算法各自解决哪一环:GP 找表达式结构,GA 找参数

2.1 遗传算法原理:从一个参数空间搜索的例子说起

遗传算法原理说起来不复杂:把一组待优化参数编码成一个“个体”,比如[持仓天数, 止损线, 开仓阈值],给每个个体算一个适应度分数,然后用选择、交叉、变异迭代出下一代。它不依赖梯度,所以适应度函数哪怕是个有大量毛刺的回测结果,搜索照样进行。在算法交易里,最常见的 GA 用法就是做参数寻优。

举个例子,你写了一个双均线策略,快线周期、慢线周期、止损比例三个参数。手调只能试几十组,穷举三万个组合又太慢。GA 的做法是:随机生成 40 组参数作为初代种群,每组参数跑一遍历史回测,把夏普比率当适应度;然后锦标赛选择留下表现好的,对参数做算术交叉和高斯扰动,再迭代 30 代。整个过程一晚上能跑完,最后拿到的参数组合往往比手调稳定得多。

需要特别注意的是,GA 把参数空间搜索做得很溜,但它不会告诉你“双均线这个结构本身行不行”。如果策略结构是错的,GA 搜到的最优参数也只是在错误结构里相对没那么差。这个边界感必须建立起来,否则很容易出现“最优参数回测漂亮、实盘一塌糊涂”的翻车事故。

在遗传算法原理的三个操作子里,选择压力是最容易被忽略的。锦标赛大小设成 3 和设成 6,搜索结果差异很大:前者多样性更好,但收敛慢;后者收敛快,却容易在局部最优里出不来。量化场景的特征维度高、噪声大,我一般优先偏探索,锦标赛大小取 3 或 4,宁可多跑几代,也不要让它早早锁定一个次优解。

2.2 遗传编程:一棵公式树是怎么长出来的

遗传编程和 GA 最大的区别在个体的编码方式。GA 是定长的参数向量,GP 是一棵树。这棵树可以由函数节点和终端节点组成,比如mul(add(mom20, vol20), log_abs(ret60)),其中add、mul是函数节点,mom20、vol20、ret60是终端节点,也就是你提前构造好的特征。

演化过程跟 GA 类似:初代用genHalfAndHalf生成一堆深浅不一的随机树;评估时把每棵树编译成 Python 函数,逐行套到历史数据上算出预测值;然后按预测值和未来收益的秩相关系数(IC)打分;接着锦标赛选择、子树交叉、子树变异,迭代几十代。树交叉不是交换两个参数,而是交换两个子树,所以后代可能比父代更深,也可能更浅。树的最大深度是一个硬约束,我一般限制在 6 层以内,太深的树在样本外几乎没有生存能力,只有拟合噪声的本事。

GP 做股票价格预测,和深度学习比最大的优势是可解释性。LSTM 给你一个无法读懂的浮点输出,GP 给你一棵公式树,你可以gp.stringify打印出来,扔给投研同事看,哪里有问题改哪里——某个特征量级太大、某个常数范围不合理,一眼就能发现。相比“黑匣子”,GP 更像一个结构搜索工具。代价是计算量更大、随机性更强,同一个种子和不同种子跑出来的结构差异可能非常大,这一点后面专门讲怎么处理。

2.3 GP 与 GA 的流水线分工:先找结构,再定参数

GP 和 GA 在同一套算法交易程序里不是并列关系,而是接力关系。常见做法是 GP 先跑:给定一组原始特征,演化出若干候选预测表达式。注意,这一步的目标不是直接给出买卖信号,而是生成一个“预测未来 N 日收益排序”的因子表达式。拿到这个表达式后,再进入 GA 阶段:把表达式里的常数、开仓阈值、持仓窗口、止损止盈交给 GA 去优化——一个进化算法管结构,另一个管参数,两件事互不干扰,各自在自己的空间里搜。

很多人误以为 GA 能直接替代 GP,把表达式结构编码成一个字符串丢给 GA 去变异,实践里这样做收敛极慢,因为结构空间是离散的、非连续的,字符串交叉很难产生有意义的子树组合。反过来,用 GP 去调参数也不合适,树结构里的常数通常靠 ephemeral constant 随机生成,精度远不如 GA 的浮点编码。把结构搜索和参数搜索分开,是这条技术路线上性价比最高的分工方式。

3. 用 Python 从零跑通最小方案:数据准备、GP 演化、GA 调参、回测一条线

3.1 数据准备:用 yfinance 拉沪深300收盘价并切成对数收益

环境配置上只要装好 Python 3.8 以上的环境,配齐pandas、numpy、scipy、deap、yfinance这几件套就能开跑。建议单独建一个虚拟环境,避免把系统 Python 搞乱。数据获取直接用 yfinance 拉沪深300指数日线,作为演示标的它流动性好、成分稳定,适合当基准。

import yfinance as yf import pandas as pd import numpy as np df = yf.download("000300.SS", start="2015-01-01", end="2023-12-31", auto_adjust=True) close = df["Close"].dropna() ret = np.log(close / close.shift(1)).dropna() print(close.head()) print(ret.describe())

这里有两个细节值得说明。第一,用对数收益而不是原始价格训练预测表达式,是因为价格的绝对值随时间漂移,直接用价格做回归会学到“价格越来越高”这种没意义的趋势,而对数收益是平稳的。第二,dropna()必须做,close.shift(1)会让第一行变成 NaN,特征矩阵和标签对不齐是后续所有莫名其妙的 IC 异常的第一个嫌疑点。如果你的 yfinance 返回的是多列 MultiIndex,只保留单标的的Close列即可。

3.2 GP 演化预测表达式:DEAP 里的算子集合与适应度设计

DEAP 是做这项实验最顺手的库。下面这段代码演示从算子集合到个体评估的完整骨架。特征先手工构造三个动量类因子:20 日动量、60 日动量、20 日波动率,作为树的终端节点。

import math import random import operator import numpy as np from deap import base, creator, tools, gp, algorithms from scipy.stats import spearmanr def log_abs(x): return math.log(abs(x) + 1e-8) def protected_div(x, y): return x / y if abs(y) > 1e-8 else 1.0 mom20 = close / close.shift(20) - 1.0 mom60 = close / close.shift(60) - 1.0 vol20 = ret.rolling(20).std() X = pd.DataFrame({"mom20": mom20, "mom60": mom60, "vol20": vol20}).dropna() y = close.shift(-5) / close - 1.0 y = y.reindex(X.index).dropna() X = X.loc[y.index] pset = gp.PrimitiveSet("MAIN", 3) pset.addPrimitive(operator.add, 2) pset.addPrimitive(operator.sub, 2) pset.addPrimitive(operator.mul, 2) pset.addPrimitive(protected_div, 2) pset.addPrimitive(log_abs, 1) pset.addEphemeralConstant("rand_const", lambda: random.uniform(0.1, 10)) pset.renameArguments(ARG0="mom20", ARG1="mom60", ARG2="vol20") creator.create("FitnessIC", base.Fitness, weights=(1.0,)) creator.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessIC) toolbox = base.Toolbox() toolbox.register("expr", gp.genHalfAndHalf, pset=pset, min_=1, max_=2) toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.expr) toolbox.register("population", tools.initRepeat, list, toolbox.individual) toolbox.register("compile", gp.compile, pset=pset) toolbox.register("select", tools.selTournament, tournsize=3) toolbox.register("mate", gp.cxOnePoint) toolbox.register("expr_mut", gp.genFull, min_=0, max_=2) toolbox.register("mutate", gp.mutUniform, expr=toolbox.expr_mut, pset=pset) def eval_ic(individual): func = toolbox.compile(expr=individual) pred = np.array([func(*row) for row in X.values], dtype=float) valid = np.isfinite(pred) & np.isfinite(y.values) if valid.sum() < 100: return (-1.0,) ic = spearmanr(pred[valid], y.values[valid])[0] depth_penalty = 0.05 * max(0, len(individual) - 24) return (ic - depth_penalty,) toolbox.register("evaluate", eval_ic) stats = tools.Statistics(lambda ind: ind.fitness.values[0]) stats.register("avg", np.mean) stats.register("max", np.max) hof = tools.HallOfFame(1) pop = toolbox.population(n=120) pop, log = algorithms.eaSimple(pop, toolbox, cxpb=0.85, mutpb=0.15, ngen=20, stats=stats, halloffame=hof, verbose=True) print(gp.stringify(hof[0]))

这段代码有几个关键参数直接影响结果。种群大小 120、代数 20 是通用起步值,如果数据量大可以适当缩到 80 和 15,优先保证一次实验能在几分钟内跑完。算子集合里我刻意加入了log_abs和protected_div,因为金融数据里经常出现负数和零值,原生的log和除法会在评估时抛异常,这一层保护必须做在算子层,而不是事后清洗。适应度用秩相关系数 IC 而不是 MSE,是因为股票预测更关心排序而不是精确价格,MSE 会被极端值带偏,而 Spearman 相关性对单调变换不敏感。深度惩罚0.05 * max(0, len(individual) - 24)是个实用技巧:表达式越大,适应度扣分越多,防止演化出一棵包含 50 个节点的复杂树去硬记噪声。

3.3 GA 接力寻参:把表达式参数和交易参数一起优化

GP 演化出的表达式只输出一个连续预测值,离可交易还有距离。下一步交给 GA,优化三个交易参数:持仓窗口缩放系数、止损线、开仓阈值。这段遗传算法 Python 代码沿用 DEAP,但适应度类换一个名字,避免与 GP 的FitnessIC冲突。

creator.create("FitnessSharpe", base.Fitness, weights=(1.0,)) creator.create("Individual", list, fitness=creator.FitnessSharpe) LOW = [0.02, 0.005, 0.0] HIGH = [1.0, 0.05, 1.0] def uniform_init(): return [random.uniform(lo, hi) for lo, hi in zip(LOW, HIGH)] def eval_sharpe(individual): hold_scale, stop_loss, threshold = individual hold_days = max(1, int(round(hold_scale * 30))) pred = pd.Series(gp_pred, index=y.index) raw_weight = np.where(pred > threshold, 1.0, -1.0) position = pd.Series(raw_weight, index=y.index).rolling( hold_days, min_periods=1).mean().fillna(0.0) strat_ret = position.shift(1).values * ret.reindex(y.index).values strat_ret = strat_ret[np.isfinite(strat_ret)] if len(strat_ret) < 30 or np.std(strat_ret) < 1e-12: return (-10.0,) sharpe = np.sqrt(252) * np.mean(strat_ret) / np.std(strat_ret) return (sharpe,) toolbox2 = base.Toolbox() toolbox2.register("individual", tools.initIterate, creator.Individual, uniform_init) toolbox2.register("population", tools.initRepeat, list, toolbox2.individual) toolbox2.register("evaluate", eval_sharpe) toolbox2.register("mate", tools.cxSimulatedBinaryBounded, low=LOW, up=HIGH, eta=20.0) toolbox2.register("mutate", tools.mutPolynomialBounded, low=LOW, up=HIGH, eta=20.0, indpb=0.2) toolbox2.register("select", tools.selTournament, tournsize=4) pop2 = toolbox2.population(n=40) algorithms.eaMuPlusLambda(pop2, toolbox2, mu=40, lambda_=40, cxpb=0.7, mutpb=0.3, ngen=30, verbose=True)

这里最值得讲的参数是cxSimulatedBinaryBounded的eta=20.0。模拟二进制交叉的eta控制子代与父代的相似程度:eta越大,子代越贴近父代,搜索越精细;eta越小,子代跳跃越大,越容易探索新区域。在量化参数寻优里,我一般先粗搜用eta=10,找到高适应度区域后再细搜用eta=30,一次性跑完的实验通常取 20 作折中。另外,position.shift(1)这行是未来函数防火墙——当天的信号只能次日生效,跳过这步的人会在回测里看到一条几乎完美的净值曲线,实盘第一周就被市场教训。止损线在这个向量化版本里没有直接参与回测,实际落地时建议单独用逐日循环模块叠加,避免向量化代码里写复杂的状态逻辑。

3.4 最小回测:先算清你赚的是不是手续费

框架可以不上,回测必须自己写一遍。一个 20 行的最小回测足够回答“这个策略到底行不行”:

position = position.shift(1).fillna(0.0) daily_ret = position * ret.reindex(y.index).fillna(0.0) cum = (1 + daily_ret).cumprod() max_drawdown = (cum / cum.cummax() - 1.0).min() sharpe = np.sqrt(252) * daily_ret.mean() / (daily_ret.std() + 1e-12) cost_rate = 0.00025 + 0.0005 turnover = (position.diff().abs().sum() / len(position)) * 252 net_ret = daily_ret - position.diff().abs() * cost_rate net_sharpe = np.sqrt(252) * net_ret.mean() / (net_ret.std() + 1e-12) print(f"毛夏普: {sharpe:.2f}, 最大回撤: {max_drawdown:.2%}, 年化换手: {turnover:.1f}") print(f"扣成本后夏普: {net_sharpe:.2f}")

这么短的回测已经覆盖了三个核心问题:毛夏普能不能看、回撤能不能忍、换手率会不会把收益吃掉。position.diff().abs()算的是每期仓位变化,乘以双边成本率再乘 252 就是年化磨损。做这一步的核心目的是在投入复杂回测框架之前,先淘汰掉一批依赖超高换手才能盈利的伪策略。完整工程落地时再替换成支持逐日撮合、手续费滑点分项统计的回测框架,但最小回测永远是最快的那把尺子。

4. 关键参数怎么调才不翻车:种群、代数、树深度与成本敏感性

4.1 GP 与 GA 关键参数速查表

参数设置有时候是玄学,但有这张表兜底,至少不会在起步阶段翻得很惨。下面是我在类似项目里常用的范围,新手可以直接照抄起步,再根据自己的数据量和机器性能调整。

参数常见范围影响翻车特征
GP 种群大小80–200太小早熟,太大每代评估太慢适应度前几代猛涨后彻底卡住
GP 代数15–30越久越容易过拟合后期适应度微涨但样本外崩掉
树最大深度5–7深度直接决定表达式复杂度表达式超过 30 个节点且很难读懂
锦标赛大小3–5选择压力取 6 以上多样性快速丢失
交叉率0.8–0.9结构重组频率小于 0.6 时收敛极慢
变异率0.1–0.2引入新结构高于 0.3 时最优个体反复丢失
ephemeral 常数范围0.1–10表达式里的尺度因子范围太窄导致预测值量级过小
GA 种群大小30–50参数空间维度不高时可小小于 20 容易陷入局部最优
GA 代数20–40参数寻优收敛速度超过 50 代适应度不再变化
多项式变异 eta10–30变异步长控制eta 太大接近无变异,太小到处乱跳

这里最容易被忽略的是 GP 的代数其实不宜太长。很多人误以为进化代数越多越好,实际上 GP 在 15 代左右就已经把训练期里的有效结构挖得差不多了,后面每一代都在往噪声上贴合。判断依据很简单:把每一代的最优 IC 和验证集 IC 同时打点,训练 IC 还在涨而验证 IC 开始掉的时候,就是该停的时候。没有后悔药,提前做好早停观察比事后调参重要得多。

4.2 标签与数据切分:预测未来几个 bar 决定问题性质

预测股票价格,具体化之后的监督信号是“未来 N 日收益”。N 选 1 天,策略变成高频择时;选 20 天,变成波段因子;选 60 天,变成偏中低频的截面排序。N 的选择决定了整个问题的性质,也决定了换手率的天花板。用 5 天预测做演示最合适:信号有一定持续性,换手又不会高到离谱。

forward = close.shift(-1) / close - 1.0 forward = forward.rolling(5).mean().shift(-5) y = forward.reindex(X.index).dropna() X = X.loc[y.index] split_train = int(len(X) * 0.6) split_valid = int(len(X) * 0.8) X_train, y_train = X.iloc[:split_train], y.iloc[:split_train] X_valid, y_valid = X.iloc[split_train:split_valid], y.iloc[split_train:split_valid] X_test, y_test = X.iloc[split_valid:], y.iloc[split_valid:]

这段切分代码里藏着一个很容易犯的错:窗口函数和shift的组合要想清楚时序对齐。close.shift(-1)已经是未来数据,再做rolling(5).mean()会继续向前扩展窗口,最后的shift(-5)是为了把标签中心对齐到预测日。任何一步方向错了,都会在训练集里混入未来信息,导致 IC 虚高。我的一线习惯是:标签构造完立刻打印y.tail(5)和close.tail(6),肉眼确认最后一行标签是 NaN、倒数第二行对应的是未来第五天的收益,确认无误再往下走。

时序切分和机器学习的随机切分是两回事,不能用train_test_split打乱数据。金融时间序列一旦打乱,未来信息直接泄漏进训练集,回测再漂亮也是自欺欺人。按时间顺序切成训练、验证、测试三段,测试段必须是最后 20% 的时间区间,不能混进训练。

4.3 成本模型:换手率一上来,收益全是假的

所有进化类策略都有同一个毛病:为了提升适应度,会演化出频繁改变预测符号的表达式,表现在持仓上就是每天都换仓。GP 和 GA 都不会主动控制换手率,如果你的适应度函数里没有交易成本,它们就会把交易成本这个约束丢到一边,疯狂追求信号拟合精度。

我在上一节最小回测里已经给出成本计算的骨架。实际调参时,建议把成本直接写进 GP 的评估函数,而不是只放在回测阶段。做法是:对种群里的每个个体,先算预测值,再换算成平均持仓变化,估算年化换手,最后在 IC 适应度里减去一个换手惩罚项。改造方案是在eval_ic里加这么一段逻辑:

pred_series = pd.Series(pred, index=y.index) weight = np.where(pred_series > pred_series.median(), 1.0, -1.0) pos_series = pd.Series(weight, index=y.index).rolling( max(1, int(round(hold_days_est))), min_periods=1).mean().fillna(0.0) annual_turnover = pos_series.diff().abs().mean() * 252 ic = spearmanr(pred[valid], y.values[valid])[0] score = ic - 0.02 * max(0.0, annual_turnover - 20.0)

这里的0.02是换手惩罚系数,表示年化换手每超过 20 倍,IC 就扣 0.02。系数取值因策略而异,但思路是固定的:让进化过程自己去权衡“预测准”和“少交易”这两件事,而不是等回测阶段才发现问题。血泪经验是:不加这个惩罚的 GP 策略,样本内 IC 能到 0.08,看起来很不错,扣完手续费之后夏普直奔负数——赚的全是手续费。

5. 五个高频踩坑记录:从 DEAP 报错到样本外失效

5.1 DEAP 同一进程重复调用 creator 直接报错

现象:在同一脚本里先跑完 GP,再跑 GA,第二次执行creator.create("FitnessSharpe", ...)时抛出类似TypeError: FitnessSharpe already exists的异常。

原因:DEAP 的creator.create是一旦执行就永久注册在模块命名空间里的,同一进程重复创建同名类是设计上的问题,不是你的代码逻辑错误。

解决:给每个流程不同命名,比如 GP 用FitnessIC,GA 用FitnessSharpe。如果同一个个体类型需要反复重建,用if not hasattr(creator, "FitnessSharpe")包裹创建语句,保证只创建一次。这是 DEAP 工程化里最常见的第一道坎,踩过一次之后就会习惯性起名加上前缀。

5.2 适应度卡住不涨:早熟收敛的特征和解法

现象:演化跑到第 5 代,种群平均 IC 就不再变化,最优个体也没更新,后续 15 代全部白跑。

原因:锦标赛选择压力太大,加上cxOnePoint交叉后优秀个体的子树在种群内快速扩散,多样性在几代之内耗尽,所有个体长得越来越像,近亲繁殖导致搜索停滞。

解决:先把锦标赛大小从 5 降到 3,再把变异率从 0.1 提到 0.2。如果还是卡,就在每代结束后随机往种群里注入 10% 的新个体,用genHalfAndHalf重新生成,给演化池补充新鲜结构。另一个有效做法是引入共享适应度机制:计算个体与最优个体的树结构相似度,相似度越高适应度折扣越大,强制种群保持分布。这个操作虽然会让最优解搜索变慢,但能明显提高最终结果的稳定性。

5.3 样本内 IC 高、样本外崩溃:过拟合和未来函数的锅

现象:训练集上最优 IC 达到 0.1,验证集掉到 0.02,测试集直接变成 -0.03。最气人的是,换一个随机种子跑一遍,结果又不一样。

原因:两个可能性叠加。一是 GP 表达式过深过大,在训练集上记住了噪声;二是标签构造用了未来窗口,训练时看似完美,实盘里根本没有那个信息。

解决:先从数据层排查未来函数,把shift方向和窗口对齐逐行核验。确认没有泄漏后,在适应度里同时加深度惩罚和换手惩罚,把表达式压到 20 个节点以内。最后做滚动验证:把训练区间每次向后滑 20 个交易日,反复演化并记录验证 IC 的分布区间。如果验证 IC 的上下四分位跨越 0 太多,说明结构本身不稳定,再换一组原始特征或调整预测周期,不要硬撑。

5.4 高换手策略把收益全赔给手续费

现象:扣成本前夏普 1.5,扣成本后夏普 -0.2。查看逐日仓位,发现信号几乎每天在 1 和 -1 之间反复横跳。

原因:GP 演化出的表达式可能在某个特征穿过阈值时输出剧烈跳变,比如protected_div(mom20, vol20)在波动率很小时会产生极端值,导致预测符号频繁翻转。GA 在优化持仓窗口时也会把窗口缩到最短以追求适应度提升。

解决:在 GP 评估里加入换手惩罚,把年化换手上限写进适应度函数;同时把 GA 的持仓窗口参数下限从 1 天抬到 3 天。表达式输出层再做一层平滑:预测值取过去 3 日的加权平均,再和阈值比较,能显著降低信号的毛刺。量化交易策略代码里最容易低估的就是这个环节,进化算法不会替你考虑交易成本,你不写进去,它永远不会自己学会。

5.5 两次运行结果差异巨大:随机种子和一致性检查

现象:同一份数据、同一套代码,昨天跑出来的最优表达式是mul(mom20, vol20),今天再跑变成add(mom60, log_abs(vol20)),完全对不上。

原因:初代种群是随机生成的,GP 的搜索结果高度依赖初代结构分布,固定了随机种子并不能解决问题,它只是让两次运行完全一样,而不是让结果更可靠。

解决:第一,每次运行前固定random.seed(seed)和np.random.seed(seed),并把 seed 记录进实验日志,保证任何一次结果可追溯。第二,每个种子独立跑 5 次,取验证 IC 的中位数作为该种子的评价,再比较不同种子之间的结果一致性。最终是否采用某个表达式,不看单次最优,而是看它在 80% 的随机种子下都有正 IC。这是对抗进化随机性的唯一可靠方式,没有捷径。

6. 进阶验证技巧:滚动重演化比单次演化可靠得多

单跑一次 GP、再跑一次 GA、回测通过,这只是入门。真正让这套算法交易程序具备实盘研究价值的,是把“单次演化”升级成“滚动重演化”,让因子表达式跟随市场风格变化持续更新。

做法是按步长滚动切片:每次拿最近 250 个交易日的窗口去演化和寻参,表达式只在接下来 20 个交易日内使用,到期后滑到下一个窗口重新演化。这样保证预测表达式永远基于“最近的市场模式”,而不是一次性用一个五年前的数据演化出的公式去打明天的仗。代码骨架如下:

window = 250 step = 20 factor = pd.Series(index=close.index, dtype=float) for start in range(0, len(close) - window - step, step): train_win = close.iloc[start:start + window] best_expr = run_gp_on(train_win) future_win = close.iloc[start + window:start + window + step] factor.loc[future_win.index] = apply_expr(best_expr, future_win)

滚动重演化还有个副产品:表达式存活监测。每个表达式投入使用后,按 30 天滚动窗口重新计算它与未来收益的 IC,并记录这条 IC 时间序列。当滚动 IC 连续 10 个交易日低于零,就把它标记为退化,提前切换到下一个候选表达式,而不是等回撤扩大了才反应过来。我最早做的项目就是吃了全样本做特征选择的亏,一次演化定终身,半年后市场风格切换,策略直接进入漫长回撤,那段时间每天看着净值曲线往下掉,才真正明白滚动重演化不是进阶技巧,而是生存必需。

最后补一句实盘习惯:任何进化出来的表达式,上实盘前必须经过一次人工审查,打印出公式、理解每个特征的经济学含义、确认没有用到未来信息。进化算法给你的是候选方案,不是免检结论。这一条希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 7:09:09

国内吧台椅生产工厂哪家性价比高?

本文为第三方独立整理的便民科普参考内容&#xff0c;无任何商业合作关系&#xff0c;内容仅基于公开信息整理&#xff0c;不构成消费推荐或决策依据。一、吧台椅采购前置需求梳理选择生产工厂前&#xff0c;建议先明确自身核心需求&#xff0c;缩小筛选范围&#xff1a;明确使…

作者头像 李华
网站建设 2026/10/3 7:07:41

OpenClaw技术架构与智能体:从网关到统一API的TaoToken接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华