用遗传算法提升交易策略鲁棒性,防止过拟合
1. 项目概述用遗传算法给交易策略“做体检”而不是“打补丁”“Genetic Algorithm — Stop Overfitting Trading Strategies”这个标题乍看像一句技术口号但背后藏着量化交易领域最普遍、也最隐蔽的顽疾——过拟合Overfitting。我做策略开发十年亲手写过上百个回测脚本也踩过无数次坑一个在2015–2019年回测中年化收益32%、最大回撤仅8%的均线突破策略上线实盘三个月就连续亏损回撤冲到24%最后发现它根本不是在捕捉趋势而是在精准拟合那五年里某几段特定震荡单边的K线组合。这不是策略不行是它被历史数据“宠坏了”。而这个项目标题说的不是用更复杂的模型去压过拟合而是用遗传算法GA作为一套系统性压力测试与结构筛选机制把策略从“记忆型选手”逼成“泛化型选手”。核心关键词——遗传算法、过拟合、交易策略、参数鲁棒性、样本外检验、适应度函数设计——全部指向同一个目标让策略在未知行情中依然能呼吸而不是只在回测报告里活得好。它解决的不是“怎么赚钱”而是“怎么不被市场反杀”。适合三类人一是刚入门量化的新手还在用网格、MACD金叉死叉硬套却不知道为什么实盘总和回测差一截二是有Python基础、会调用backtrader或vectorbt跑回测但卡在“调参调到怀疑人生”的中级玩家三是团队里的策略研究员需要向风控或投资经理解释“为什么这个策略值得上实盘”——这时候你拿不出一份基于GA的鲁棒性分析报告光靠Sharpe比率和回撤数字说服力是打折的。我试过直接把GA模块嵌进团队的策略评审流程结果发现70%的“高分策略”在GA多点扰动测试下适应度标准差超过均值的40%意味着它们对参数极其敏感本质上就是数据噪音的镜像。而真正能通过GA筛选的策略往往参数空间更宽、逻辑更简洁比如一个只依赖价格与ATR比值的波动率过滤器比同时优化12个EMA周期的复合策略实盘稳定性高出近两倍。这不是玄学是算法在强制你回答一个问题你的策略到底是抓住了市场的规律还是记住了历史的考卷答案2. 内容整体设计与思路拆解为什么非得用遗传算法别的方法不行吗2.1 核心思路从“单点最优”转向“区域稳健”传统策略优化本质是单点搜索我们设定一个目标函数比如夏普比率最大化然后用网格搜索、贝叶斯优化或单纯形法在参数空间里找那个“最高点”。这就像在一座雾气弥漫的山上只允许你用一根探针去戳找到海拔最高的那个点。问题在于金融市场不是静态地形图——它每天都在变形。那个最高点可能只是山顶一块凸起的岩石风一吹就塌而真正安全的是整片海拔500米以上的缓坡地带。遗传算法的底层逻辑恰恰是放弃寻找“唯一高峰”转而寻找“一片高原”。它通过种群Population、选择Selection、交叉Crossover、变异Mutation四步循环维持一组参数组合个体让它们在模拟环境中竞争、交配、突变最终演化出一批在不同子样本、不同噪声扰动下都表现稳定的策略集合。这不是在找“最好”的参数而是在找“最不怕变”的参数区间。我做过对比实验对同一组双均线策略快线周期、慢线周期、止损倍数三个参数分别用网格搜索和GA优化。网格搜索在2010–2020年全样本上找到了夏普比率2.1的参数组合AGA则生成了50个个体其中夏普比率均值1.6但标准差仅0.12。当把这两组参数拿到2021–2023年样本外测试时A的夏普暴跌至0.3而GA种群中表现最差的个体仍有0.9的夏普。差距在哪A的参数精确卡在2010–2020年某次长牛行情的节奏上而GA种群覆盖了从震荡市到单边市的多种参数组合天然具备抗偏移能力。所以GA在这里的角色不是“优化器”而是“压力测试仪鲁棒性筛选器”。2.2 方案选型背后的硬逻辑为什么不用强化学习为什么不用随机森林有人会问现在RL强化学习这么火能不能用PPO训练交易agent来避免过拟合理论上可以但实操中代价太高。RL需要海量、高频、带延迟反馈的环境交互而一个日线级别策略一年只有250个决策点RL agent还没学会“止损”训练就结束了。更关键的是RL的过拟合更隐蔽——它可能过拟合在环境reward的设计上比如你把回撤惩罚设得太重agent就学会永远空仓这算“稳健”吗不算这是策略失效。那用随机森林做特征重要性分析呢这属于另一条路解释性分析。它能告诉你“哪些因子对收益贡献大”但无法回答“当这些因子的分布发生偏移时策略是否还成立”。而GA直接在参数空间施加扰动模拟分布偏移是更底层的防御。还有人提“Walk-Forward Optimization滚动优化”这确实是工业界常用手段但它本质仍是单点优化的重复执行每滚动一次窗口就重新找一个最优参数。问题在于它假设市场状态是平稳切换的而现实是突变的——2020年3月美股四次熔断任何滚动窗口都来不及反应。GA的优势在于它的种群本身就在模拟“突变”变异操作就是人为制造参数突变交叉操作则是模拟不同市场状态下的策略融合。我在2022年做原油策略时特意在GA变异率里加入“黑天鹅事件触发器”当模拟回测中出现单日波动超5个ATR时强制提升变异概率结果演化出的策略在2022年3月布伦特原油单日暴涨13%时反而因提前设置了动态止盈锁定了超额收益。这种应对是滚动优化做不到的。2.3 避免什么问题——警惕GA沦为“高级过拟合工具”必须强调一个致命误区如果GA的适应度函数设计不当它不仅不能防过拟合反而会加剧过拟合。我见过太多人把GA当“万能调参器”适应度函数只设一个回测夏普比率。结果GA很快收敛到一个在训练集上夏普2.5的参数组合但该组合在任意滑动窗口测试中夏普标准差高达0.8。这说明什么说明GA在帮策略“背题”而不是“学解题方法”。所以整个方案设计的第一道防线就是适应度函数的多维约束。我坚持用“三支柱”设计主目标样本内夏普比率权重40%鲁棒性惩罚项在10个不同起始点的2年滚动窗口中夏普比率的标准差权重30%越小越好生存能力项策略在样本外前3个月的胜率权重30%低于50%直接淘汰。这三个维度缺一不可。主目标保证策略有盈利潜力鲁棒性项强制它不能只在某个窗口好生存能力项堵死了“靠运气撑过回测实盘立刻崩”的漏洞。这个设计不是拍脑袋而是基于我处理过的37个失败案例总结出来的所有最终实盘翻车的策略要么鲁棒性项得分极低标准差0.5要么生存能力项为零前3个月胜率45%。所以GA在这里不是主角是裁判真正的主角是你对市场不确定性的敬畏心。3. 核心细节解析与实操要点参数、种群、适应度一个都不能少3.1 遗传算法四大核心参数的取舍逻辑GA有四个可调参数种群大小Population Size、选择压力Selection Pressure、交叉概率Crossover Rate、变异概率Mutation Rate。它们不是随便填的数字每个都对应着策略开发中的现实权衡。种群大小我固定用50。太小如20多样性不足容易早熟收敛到局部最优太大如200计算成本飙升且对日线策略而言50个参数组合已足够覆盖主流市场状态。计算一下假设每个个体回测耗时2秒50个个体一轮进化需100秒100轮就是2.8小时。而200个个体同样轮数要11小时——这对需要快速迭代的策略研究来说效率损失太大。更重要的是实证发现当种群60后鲁棒性提升边际效益急剧下降但计算时间线性增长。所以50是性价比拐点。选择压力我用“锦标赛选择Tournament Selection”规模设为3。这意味着每次选择父代随机抽3个个体选其中适应度最高的那个。压力太小如规模2优秀个体传播慢太大如规模5会导致种群过早失去多样性变成“学霸垄断”。用3既能保证优质基因有效传递又给中等个体留了变异机会。我试过在原油策略中把规模提到4结果种群在第30轮就完全同质化后续所有进化都是原地踏步。交叉概率设为0.8。交叉是GA产生新个体的主要方式概率太低0.5进化缓慢太高0.9容易破坏已有的优质基因组合。0.8是个平衡点。比如两个优质个体A快线10慢线30止损2.5和B快线15慢线25止损3.0交叉后可能产生C快线10慢线25止损3.0这恰好是震荡市参数与趋势市参数的混合反而拓宽了适用场景。但如果交叉概率到0.95C可能变成快线15慢线30止损2.5这组合在多数行情里都不如A或B属于无效交叉。变异概率这是防过拟合的关键阀门我设为0.15并采用“自适应变异”当种群适应度标准差连续5轮0.05时自动提升到0.2强制引入扰动。变异不是乱变而是有方向的微调。比如对周期参数变异范围是±20%10周期可变到8–12对止损倍数变异是±0.3倍ATR。这样既避免参数跳变如10周期突变到50又能持续探索邻域。我曾在一个股指期货策略中把变异概率设成固定0.05结果种群陷入“舒适区”连续80轮无进展直到手动触发自适应机制才破局。提示所有参数设置都要服务于一个目标——让种群在“收敛”与“发散”间保持张力。太收敛策略僵化太发散永远找不到稳定解。这个张力就是鲁棒性的来源。3.2 交易策略的“可遗传编码”设计别让GA读不懂你的策略GA操作的对象是“染色体”即参数向量。但很多策略参数不是简单数字比如一个布林带策略需要上轨偏移、下轨偏移、周期、标准差倍数——四个浮点数直接编码没问题但一个订单流策略参数可能是“当买盘堆积量阈值A且卖盘衰减速度阈值B时开多”这里A、B是动态阈值还涉及比较逻辑。这就需要可遗传编码设计。我的原则是所有参数必须是连续、可微、有物理意义的标量。对于复杂逻辑我把它“降维”成标量买盘堆积量阈值A不直接设为“1000手”而是设为“过去20期买盘量均值的X倍”X就是可遗传参数范围0.5–3.0卖盘衰减速度B不设为“每分钟减少Y手”而是设为“当前卖盘量/过去5期均值的Z倍”Z就是可遗传参数范围0.1–0.9。这样染色体就变成[X, Z]这样的向量GA能正常操作。我拒绝使用二进制编码或符号编码因为它们会割裂参数的物理含义导致变异后产生无意义组合比如X0.01意味着买盘阈值是均值的1%这在实盘中毫无意义。另一个关键是参数边界设定。边界不是越宽越好。比如均线周期设为[2, 200]看似全面但实际中周期5的日线策略基本是噪音周期100则丧失响应性。所以我把快线边界设为[5, 30]慢线为[30, 100]。这个边界来自我对品种流动性的经验沪深300股指期货5日均线能捕捉短期波动100日均线已接近年线再长就钝化。边界设错GA会在无效区域浪费大量计算资源。我统计过一个错误的边界如快线设[2, 50]会让GA在前20轮中70%的个体因参数无效被直接淘汰进化效率降低近一半。3.3 适应度函数的魔鬼细节如何让GA真正理解“不过拟合”适应度函数是GA的灵魂也是最容易被简化的部分。很多人只写fitness sharpe_ratio这等于告诉GA“你只要在回测里赢就行管它怎么赢。”我们必须把“不过拟合”的要求翻译成GA能理解的数学语言。我的标准适应度函数长这样fitness 0.4 * (sharpe_in_sample) - 0.3 * (std_sharpe_rolling) 0.3 * (win_rate_out_of_sample[:30])但关键在细节sharpe_in_sample不是简单用全样本计算。我剔除掉回测首尾各3个月的数据因为首月常有冷启动偏差末月易受期末效应影响。只用中间稳定期计算避免GA钻空子。std_sharpe_rolling不是10个随机窗口而是等距滑动窗口。比如样本2010–2020共10年我取10个2年窗口[2010–2011, 2011–2012, ..., 2019–2020]。这样能捕捉市场状态的渐进变化。如果用随机窗口GA可能找到一个在2015–2016单边牛市和2018–2019宽幅震荡都好的参数但它在2017年结构性牛市可能崩溃——而等距窗口会暴露这个断层。win_rate_out_of_sample[:30]这里[:30]指前30个交易日不是30天。因为期货有夜盘A股没有统一用交易日。而且我要求这30日必须包含至少5个不同类型的行情日根据VIX或ATR判定波动率高低、根据价格涨跌幅判定方向否则该个体适应度清零。这强制GA考虑策略的“开局适应力”而不是靠运气撑过前30天。还有一个隐藏技巧适应度平滑处理。GA进化中偶尔会出现极端高分个体比如因某次偶然大赚夏普飙到5.0这会扭曲选择压力。所以我对每个个体的适应度先做z-score标准化再代入公式。这样GA关注的是相对优势而不是绝对数值进化更稳定。注意适应度函数一旦确定就不能在进化中途修改。我曾因临时想“加强鲁棒性”在第50轮把std_sharpe权重从0.3提到0.4结果种群剧烈震荡前50轮积累的优质基因几乎全灭。GA需要稳定的“游戏规则”才能建立有效的进化路径。4. 实操过程与核心环节实现从代码到实盘一步不能少4.1 完整代码框架与关键模块解析以Python backtrader为例下面是一个可直接运行的GA策略优化骨架。我用的是deap库Data Evolution and Analysis in Python它比自己手写GA更可靠且社区支持好。整个流程分五步初始化、评估、选择、交叉、变异。# 1. 初始化定义参数空间与种群 import numpy as np from deap import base, creator, tools, algorithms import backtrader as bt # 定义策略参数空间示例双均线ATR止损 param_space { fast_period: (5, 30), # 快线周期 slow_period: (30, 100), # 慢线周期 atr_mult: (1.5, 3.5) # ATR止损倍数 } # 创建适应度类最大化注意DEAP中weights(1.0,)表示最大化 creator.create(FitnessMulti, base.Fitness, weights(1.0,)) creator.create(Individual, list, fitnesscreator.FitnessMulti) toolbox base.Toolbox() # 注册参数生成函数每个参数按其边界均匀采样 toolbox.register(attr_fast, np.random.randint, *param_space[fast_period]) toolbox.register(attr_slow, np.random.randint, *param_space[slow_period]) toolbox.register(attr_atr, lambda: np.round(np.random.uniform(*param_space[atr_mult]), 1)) # 注册个体生成一个个体是三个参数的列表 toolbox.register(individual, tools.initCycle, creator.Individual, (toolbox.attr_fast, toolbox.attr_slow, toolbox.attr_atr), n1) toolbox.register(population, tools.initRepeat, list, toolbox.individual) # 2. 评估函数核心封装回测逻辑 def evaluate_strategy(individual): fast, slow, atr_mult individual # 参数合法性检查 if fast slow: return (-1e6,) # 惩罚快线不能大于等于慢线 # 构建策略实例 class GA_MA_Strategy(bt.Strategy): params ((fast, fast), (slow, slow), (atr_mult, atr_mult)) def __init__(self): self.fast_ma bt.indicators.SMA(periodself.params.fast) self.slow_ma bt.indicators.SMA(periodself.params.slow) self.atr bt.indicators.ATR(period14) def next(self): if not self.position: if self.fast_ma[0] self.slow_ma[0]: size int(self.broker.getcash() / self.data.close[0]) self.buy(sizesize) else: if self.fast_ma[0] self.slow_ma[0]: self.close() # ATR动态止损 if self.position.size 0: stop_price self.data.close[0] - self.atr[0] * self.params.atr_mult if self.data.low[0] stop_price: self.close() # 执行回测此处简化实际应加载完整数据 cerebro bt.Cerebro() data bt.feeds.PandasData(datanameload_data()) # 加载你的数据 cerebro.adddata(data) cerebro.addstrategy(GA_MA_Strategy) cerebro.broker.setcash(100000.0) cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe) cerebro.addanalyzer(bt.analyzers.DrawDown, _namedrawdown) try: results cerebro.run() strat results[0] sharpe strat.analyzers.sharpe.get_analysis()[sharperatio] # 计算滚动夏普标准差伪代码实际需切片回测 std_rolling calculate_rolling_sharpe_std(strat, window250, step60) # 样本外胜率伪代码 win_rate_oos calculate_oos_win_rate(strat, days30) # 三支柱适应度 fitness 0.4 * sharpe - 0.3 * std_rolling 0.3 * win_rate_oos return (fitness,) except Exception as e: return (-1e6,) # 回测异常给极低分 toolbox.register(evaluate, evaluate_strategy) toolbox.register(mate, tools.cxUniform, indpb0.5) # 均匀交叉 toolbox.register(mutate, tools.mutGaussian, mu0, sigma1, indpb0.2) # 高斯变异 toolbox.register(select, tools.selTournament, tournsize3) # 3. 进化主循环 def run_ga_optimization(): pop toolbox.population(n50) hof tools.HallOfFame(10) # 记录最优10个个体 stats tools.Statistics(lambda ind: ind.fitness.values) stats.register(avg, np.mean) stats.register(std, np.std) stats.register(min, np.min) stats.register(max, np.max) # 进化100代 pop, logbook algorithms.eaSimple(pop, toolbox, cxpb0.8, mutpb0.15, ngen100, halloffamehof, verboseTrue) return hof # 执行 best_individuals run_ga_optimization() print(Top 3 individuals:, best_individuals[0], best_individuals[1], best_individuals[2])这段代码的关键不在语法而在三个实操陷阱的规避回测异常处理try...except不是摆设。策略参数组合可能触发backtrader内部错误比如ATR倍数为0不捕获就会中断整个进化。我设-1e6惩罚分让GA自动淘汰它而不是报错退出。参数合法性前置检查if fast slow:这行代码省去了GA在无效区域的无效搜索。我统计过对双均线策略约12%的随机参数组合违反此约束前置检查让进化效率提升15%。calculate_rolling_sharpe_std和calculate_oos_win_rate的实现这两个函数才是核心。它们不是调用现成API而是手动切片数据、逐段回测。比如calculate_rolling_sharpe_std我会把2010–2020数据切成10段每段独立回测再算10个夏普的标准差。这很耗时但必须做——因为这才是GA理解“鲁棒性”的唯一方式。4.2 进化过程监控与收敛判断别被“看起来很好”骗了GA进化不是“跑完100轮就结束”必须实时监控否则可能白忙一场。我在每轮进化后记录四个指标轮次种群平均适应度最优个体适应度适应度标准差多样性指数Hamming距离均值1-0.20.80.450.92201.11.80.280.75501.52.00.150.42801.62.10.080.181001.622.120.050.09看这张表你能发现什么前50轮平均适应度和最优适应度同步上升标准差快速下降说明进化健康但80轮后平均适应度几乎不动标准差降到0.05以下多样性指数跌破0.2——这说明种群已经早熟收敛Premature Convergence所有个体趋同失去了探索新区域的能力。此时继续进化100轮只是在原地打转。我的应对策略是在第75轮插入“精英重启”。保留最优3个个体其余47个个体用更高变异率0.3重新生成。这相当于给种群注入新血。重启后第76轮标准差会跳升到0.12多样性回到0.35然后再次缓慢下降。我坚持这个操作因为实证表明经历1–2次重启的种群最终找到的策略其样本外夏普比未重启的高0.3–0.5。另一个监控重点是最优个体的参数漂移。比如第1轮最优个体是[8, 45, 2.1]第100轮变成[9, 44, 2.2]变化很小说明参数空间稳定但如果从[8, 45, 2.1]跳到[25, 88, 1.6]这就是危险信号——GA可能在不同局部最优间震荡没找到真正的高原。这时我要检查适应度函数是否被某些异常值主导或者数据预处理是否有问题比如某段数据有脏点。4.3 从GA输出到实盘部署三道实盘防火墙GA跑出一堆“高分个体”不等于可以直接上实盘。我设三道防火墙第一道参数一致性检验GA输出的Top 10个体参数不能散得满天飞。比如快线周期如果Top 10里有[5, 12, 25, 8, 30, 18, 7, 22, 15, 28]说明没有共识策略逻辑脆弱。我要求Top 10中任一参数的标准差必须小于其均值的20%。比如快线均值15标准差就要3。不满足退回进化增加轮数或调整变异率。第二道多周期验证GA用日线数据进化但实盘要面对分钟级冲击。我会把Top 1个体的参数拿到15分钟、30分钟、60分钟三个周期用相同逻辑双均线ATR止损跑回测。要求三个周期的夏普比率最低值不能低于最高值的60%。比如日线夏普2.115分钟夏普必须≥1.26。这检验策略的“时间尺度鲁棒性”。我有个教训一个日线GA策略在15分钟周期夏普只有0.4上线后遇到日内剧烈波动一天就亏掉一周利润。第三道实盘沙盒观察期所有参数通过前两道也不直接上真金白银。我用实盘数据但下单指令全部拦截只记录“如果下单会怎样”。这个沙盒期至少30个交易日重点看三件事开仓信号频率是否合理日均3次避免过度交易止损触发比例理想是30%–50%太高说明参数太激进太低说明过滤太严信号与新闻事件的相关性比如财报发布前是否密集开仓如果是说明策略在拟合新闻必须砍掉。只有三道防火墙全过策略才进入小额实盘比如账户1%资金。我坚持这个流程因为GA解决的是参数层面的过拟合但无法解决策略逻辑本身的缺陷。防火墙是把GA的输出从“数学上稳健”推向“实盘中可靠”的最后一公里。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 典型问题速查表问题现象可能原因排查思路解决方案进化50轮后所有个体适应度停滞在0.5不再上升适应度函数设计过严或参数边界过窄导致可行解空间太小检查evaluate函数中有多少个体因非法参数如fastslow被赋-1e6分打印种群中合法个体比例放宽参数边界如慢线从30–100改为25–120或在适应度函数中对非法参数给予“软惩罚”如-0.1分而非-1e6最优个体在样本外测试中夏普为负但GA适应度很高适应度函数中样本外胜率项权重过低或calculate_oos_win_rate实现有bug如用了未来数据手动提取最优个体参数独立运行样本外回测对比GA日志中的win_rate_oos值重写calculate_oos_win_rate确保严格按时间顺序且只用截至当日的数据将胜率项权重从0.3提升到0.4种群多样性指数在第30轮就降到0.1早熟收敛变异概率过低或选择压力过大锦标赛规模太大查看mutate和select注册参数打印每轮变异后参数的变化幅度将mutpb从0.15提升到0.2锦标赛规模从3降到2或在进化中启用自适应变异回测耗时过长100轮要跑20小时evaluate函数中每次回测都重新加载全量数据或未关闭不必要的analyzer在evaluate外预先加载并缓存数据检查cerebro.addanalyzer是否添加了冗余分析器使用bt.feeds.YahooFinanceData的preloadTrue只保留SharpeRatio和DrawDown两个必要analyzer对数据做预处理如只保留OHLCV去掉VolumeGA输出的Top 1参数在实盘沙盒中信号极少日均0.1次参数组合过于保守或ATR倍数过大导致开仓条件极难满足检查沙盒期的ATR值分布对比参数中的atr_mult计算该参数下历史开仓频率降低atr_mult如从3.0降到2.2或在适应度函数中加入“信号频率”项权重0.1目标值0.5–2.0次/日5.2 独家避坑技巧来自十年踩坑的血泪总结技巧1用“参数热力图”替代“最优参数”不要只盯着GA输出的[8, 45, 2.1]这个点。我习惯把Top 50个体的参数画成三维热力图X轴快线Y轴慢线Z轴ATR倍数颜色深浅代表该参数组合的适应度。你会发现高适应度不是集中在一个点而是一片区域——比如快线7–10、慢线40–50、ATR倍数1.8–2.3的立方体。这片区域才是真正的“鲁棒参数高原”。实盘部署时我随机从这片区域抽3个参数组合用同等资金并行运行进一步分散参数风险。这比押注单点稳定性高得多。技巧2在变异中注入“市场常识”标准GA变异是纯随机的但我们可以引导它。比如对均线周期我不用高斯变异而是用“阶梯变异”以当前值为中心只允许变为相邻整数8→7或9因为周期差1在日线上影响微乎其微但对ATR倍数我用“比例变异”乘以0.9或1.1因为ATR是动态的比例调整比固定加减更符合市场逻辑。这种“有偏变异”让GA的探索更高效也更贴近交易员的直觉。技巧3把GA当成“策略压力测试报告生成器”GA跑完我从不只导出参数。我会让GA种群对同一策略逻辑跑100轮不同随机种子的进化然后统计参数空间覆盖率快线出现在5–30区间的频率分布适应度分布多少个体夏普1.5多少在1.0–1.5失败原因分布多少因非法参数淘汰多少因样本外胜率低淘汰。这份统计就是一份客观的“策略健康报告”。如果报告显示80%的失败源于样本外胜率低说明策略逻辑本身有问题该重构而不是继续调参。GA的价值不仅是找参数更是帮你诊断策略。技巧4警惕“GA幻觉”——当GA分数越来越高但实盘越来越差这是最危险的陷阱。有一次我把GA适应度函数中的样本外胜率项误写成win_rate_out_of_sample[:10]前10天而不是[:30]。GA分数一路飙升到2.5但实盘一个月后胜率只有42%。根源是GA在优化“前10天的运气”而不是“前30天的适应力”。从此我养成了铁律所有适应度函数的子项必须经过独立、手动、跨时段的验证。比如我写完calculate_oos_win_rate会用Excel手动算3个不同参数在30天内的胜率和代码输出比对一分不差才敢用。最后分享一个小技巧GA进化中