资讯动态

量化回测的陷阱:告别因子收集癖,用backtrader做严谨的多股回测

发布时间:2026/9/9 16:30:01 来源:尧图企业网站定制
先交代一下背景我现在打开电脑里的“因子”目录里面躺着两百多个文件文件名从“动量_v1”到“动量_最终版V7_不可修改”一应俱全。这还不算那些被我收藏但再也没点开过的论坛帖子。如果你听到这个描述也觉得自己被点了名那你可能和我一样患上了一种量化投资里的常见病——因子收集癖。这篇文章想聊的就是“回测”这件事本身带来的幻觉。很多做量化的人尤其是靠自学历程走过来的朋友很容易陷入一个循环找因子、跑回测、看曲线、调参数、再跑回测、再换因子最后实盘一开账户教你做人。为什么回测里漂亮得像艺术品的策略一进实盘就变成惨案到底是策略不行还是回测本身有问题这个问题我琢磨了很多年也踩过无数坑现在把能说的都写出来。文章的读者应该是正在积累因子但还没有稳定盈利的人也适合已经会用 backtrader 跑多股回测、却总觉得自己“差一步”的策略工程师。看完你会明白问题往往不出在某个因子身上而出在整个策略验证链路里。1. 因子收集癖是怎么养成的1.1 收集因子带来的即时快感如果你也经历过“刷到一篇新因子文章赶紧复制代码跑出一条漂亮曲线然后心跳加速”的时刻那你就知道我在说什么。因子收集癖的本质不是理性研究而是情绪驱动。每多一个因子就多一份“我掌握了别人不知道的规律”的错觉每跑出一条向上平滑的净值曲线大脑会分泌多巴胺让你误以为自己离圣杯又近了一步。我2018年刚开始做量化时情况和很多人一模一样。白天上班晚上研究因子周末全搭在回测平台上。有一次我把某个量价因子在五分钟K线级别上反复优化硬是从普通水平调到了“年化85%、最大回撤不到8%”的惊人状态。当时我兴奋得在房间里来回走觉得自己马上要去换个城市生活了。结果呢实盘头一个月就亏了12%而且亏得毫无还手之力。后来我回想那个过程真正让我兴奋的其实不是策略有效而是“我让它看起来有效”这个行为本身。回测给了我即时反馈每一轮参数调整都可能让曲线变好一点这种游戏化体验比真实的交易要刺激得多。但问题恰恰出在这里你以为自己在做研究其实只是在玩一个可交互的图表。1.2 “收藏柜”里的伪圣杯随着时间推移因子库会越来越庞大。估值、动量、反转、质量、低波、量价、资金流、事件驱动每个大类下还衍生出几十个变体。我见过一个网友的截图光他自建的指标体系就有300多个因子每季度更新一次Excel文件大到打开都要卡几秒。收藏本身没有错问题在于很多人把“收集”当成了“开发”把“有一个因子库”当成了“有一套策略体系”。真正的策略体系需要回答三个问题因子为什么有效因子什么时候失效失效了怎么办但因子收集癖患者通常只关心一个问题——这个因子回测过没有这就导致一个奇特的局面因子库越来越厚可实盘账户的曲线却比任何单因子回测曲线都要惨。因为回测曲线是你在无数个平行宇宙里挑出来的那条最漂亮的而实盘只能走在一条真实且唯一的时间线上。2. 回测和实盘之间到底隔了多少层“虚假容易”我把“回测美丽实盘翻车”拆成三个层次来看分别是数据层、模型层和执行层。每个层次都有具体的坑而且这些坑往往同时出现叠在一起最后的结果就是灾难。2.1 数据层的三个陷阱幸存者、未来函数、复权误差先说幸存者偏差。这是最基础也最隐蔽的一个坑。做回测时如果你用的是“当前存在的股票列表”去倒推历史那你的样本里天然淘汰了那些已经退市的公司。退市股大多数是亏损股、问题股它们的长期收益非常差。你把它们从样本里剔除回测的收益自然被系统性高估了。我当时做一个业绩暴雷因子时就踩过这个坑。用现在的股票池去测“财报发布后跳空低开买入”的逻辑回测结果好得惊人。但后来仔细一看很多2015年、2016年触发信号的公司后来都ST、退市或者跌到无人问津了。它们还在历史数据里但因为不在今天的股票池我的脚本根本没有把它们选进来。这等于考试前先告诉了你哪些题不考那成绩还能差吗前视偏差也一样常见。所谓前视偏差就是用未来才知道的数据做决策。比如某个技术指标用到当天的收盘价来决定当天是否买入而实际回测时分时级的成交根本等不到收盘价出来。更隐蔽的是有些平台自带的财务数据标注的是“发布时间”但程序里访问的字段却是“数据截止日”中间隔了几个月等于你在用未来报表做历史交易。复权和除权处理也是重灾区。前复权数据适合看图形但对计算真实的收益率和历史价格水平并不友好。尤其是做分红率、股息率类因子如果处理得不干净一次大额分红就会被误判成价格跳空或者异常收益。很多漂亮的长周期回测曲线仔细拆开收益里相当一部分来自这种价格修正的假象。2.2 模型层的过度拟合机器数据层的问题可以通过仔细清洗来解决但模型层的过拟合才是“回测艺术”的核心来源。先做一个简单的思想实验你抛一枚硬币100次理论上连续出现正面的最长长度应该在7次左右。如果你把这100次结果当成10个因子每个因子测试5种参数组合那你相当于做了50次独立实验。哪怕这些因子全是随机噪音你也会发现至少有几次实验看起来“表现优异”。做量化的人每天就在做这种实验——只是没人统计自己到底做了多少次尝试。数据窥探Data Snooping的后果就是你在同一份数据上反复尝试直到某组参数碰巧和历史行情吻合。这就好像你去参加一场只考一道题的考试而且可以先偷看答案、再调整解题步骤最后把答案交上去成绩当然好看。但真实考试永远只有一次你没有重来的机会。参数寻优的时候过度拟合的迹象非常明显最优参数是某个孤单的点稍微调一格收益就大幅下降甚至亏钱。比如你发现“持有周期17天”时收益最高但16天和18天都明显变差那这个17天大概率是在拟合噪音而不是在捕捉规律。真正的规律通常在一个参数邻域内都表现稳定只是最优位置略微凸起而已。还有一个被忽略的问题是“选择偏差”。很多人跑回测时会不自觉地选择“看起来合理”的起点。比如2018年年初开始回测因为2019到2021年行情好或者避开2015年的股灾区间因为那段走势会让策略现原形。这种选择性报告会让回测看起来比真实水平高出一大截。2.3 执行层的隐形税滑点、手续费和流动性即便数据干净、模型也没过拟合执行层依然有一道很难跨过的坎你的回测假设市场永远“刚好够买”。拿A股来说卖出股票要交印花税现在费率是成交额的0.05%虽然看起来不高但对于高频调仓的策略一年下来是个不小的数字。再加上佣金、过户费以及最致命的是滑点——当你真的下单时成交价往往不是你看K线时的那个价格而是市场深度里和你方向相反的那个价位。信号越拥挤滑点越大。如果一个回测模型只按收盘价成交、不包含滑点那它的收益曲线任何一条都不能信。流动性约束更是小市值因子的天敌。小市值策略在回测里几乎无敌因为过去十年小盘股确实跑赢了大盘而且回测框架默认你可以按收盘价买进任何想买的数量。但现实是一只日成交额只有3000万的股票你下个500万的单子可能直接把价格推高2到3个百分点买入成本瞬间吃掉你几个月的期望收益。做多股回测时尤其要注意组合里加权最多的那几只票是否真的吃得下你要下的量。我见过最离谱的回测展示是有人把某个策略用在了一只日成交额不足1000万的ST股上单笔下单金额却写了300万。回测软件照单全收曲线优雅得像教科书但现实是这订单挂进去第二天能不能全部成交都难说。2.4 一个“完美策略”是如何炼成的为了让上面的内容更具体我拆解一个典型的“完美回测”案例。这个案例是我早期在某论坛上看到的作者很自豪地贴出了净值曲线年化120%、最大回撤只有9%、夏普比率超过3.5。评论里一群人喊大佬只有几个人问了一句成本怎么算的。把这个策略拆开看其实集齐了所有雷数据用的是前复权日线但财务指标更新没有做时间对齐等于用了未来数据股票池是“当前市值为0到30亿的小盘股”但用的却是当前市值天然排除了已经涨上去的标的买入信号是“放量突破20日均线”但成交价设定为当天收盘价没有加滑点卖出信号是“跌破10日均线”但也没有考虑跌停时无法卖出的情况回测区间选在2019年1月到2021年12月恰好错过了2018年熊市和2022年回调手续费只算了佣金印花税没算参数是反复寻优后挑出来的没有做样本外测试。你看每一个单独的坑都不难识别但叠在一起就能造出一个“看起来是圣杯、实则是捡漏”的完美曲线。最可怕的是如果作者不主动交代这些细节看贴的人很难从净值曲线本身发现任何问题。这也是为什么我后来养成了一个习惯任何回测结果发给我之前我先要求看明细——调仓记录、手续费科目、滑点设置、股票池口径、区间选择逻辑。没有这些收益曲线再漂亮也只是张好看的图。3. backtrader多股回测的正确打开方式3.1 为什么要用backtrader做多股回测聊完这么多坑再回到工具层面。最近这两年backtrader几乎成了个人量化里最普及的多股回测框架GitHub上的示例和教程非常多大家用起来顺手社区活跃度也高。我自己从单标的Excel回测到自写Python循环再到用backtrader每一步都经历过很清楚它好在哪、不够在哪。backtrader是一个事件驱动的回测引擎核心思路是把策略逻辑封装成类回测时按bar逐根推进。每来一根新的K线框架就依次调用策略的next方法在方法里根据当前逻辑决定要不要下单、下多少单。相比自写循环回测backtrader帮我们处理了非常多的边缘情况比如多个标的数据的对齐、订单状态管理、持仓市值计算、复权和资金变动这些模块都是按多年实盘需求打磨过的。它同时支持回测数据和实盘数据源同一个策略类稍微改改就能连接实盘broker做交易这种“写一套逻辑两边用”的设计非常实用。但有一点必须说清楚backtrader做的事情是“如实地模拟执行环境”它不是真理机器。如果数据本身脏、因子本身无效那backtrader再牛也救不了你。它能把你回测时的成交假设、手续费、滑点都暴露出来逼你去面对执行问题但它不能帮你判断因子逻辑是否成立。3.2 一个最小可用的多股回测骨架说了这么多直接上一个可以跑起来的最小示例。我做一个非常简单的多股动量策略在5只股票里每20个交易日调仓一次按过去20个交易日收益排序等权买入排名前2的股票其余平仓。为了贴近真实我会加上手续费和百分比滑点。代码不长但已经覆盖了多股回测的典型环节import backtrader as bt import pandas as pd class MomentumRankStrategy(bt.Strategy): params dict( rebalance_days20, lookback20, top_n2, universe_size5, ) def __init__(self): self.bar_count 0 self.last_rebalance -1 def next(self): self.bar_count 1 if self.bar_count - self.last_rebalance self.p.rebalance_days: return # 收集所有标的过去lookback日的区间收益 returns {} for data in self.datas: if len(data) self.p.lookback: past_return data.close[0] / data.close[-self.p.lookback] - 1 # 简单过滤数据缺失或价格为0时不参与排名 if data.close[0] and data.close[-self.p.lookback]: returns[data._name] past_return if len(returns) self.p.top_n: return # 按收益从高到低排序取前top_n名 ranked sorted(returns.items(), keylambda x: x[1], reverseTrue) buy_list [name for name, _ in ranked[:self.p.top_n]] # 对每只标的调仓 target_weight 1.0 / self.p.top_n for data in self.datas: if data._name in buy_list: total_value self.broker.getvalue() target_value total_value * target_weight self.order_target_value(data, target_value) else: self.close(data) self.last_rebalance self.bar_count # 加载数据假设你有5只股票的CSV字段为 # datetime,open,high,low,close,volume索引为datetime cerebro bt.Cerebro() for stock_name in [stock1, stock2, stock3, stock4, stock5]: df pd.read_csv(f{stock_name}.csv, index_col0, parse_datesTrue) data bt.feeds.PandasData(datanamedf) cerebro.adddata(data, namestock_name) cerebro.addstrategy(MomentumRankStrategy) # 关键参数初始资金、手续费、滑点 cerebro.broker.setcash(1000000.0) cerebro.broker.setcommission(commission0.001) cerebro.broker.set_slippage_perc(perc0.001) # 运行回测 result cerebro.run() cerebro.plot()这个骨架里有几个值得细看的地方。第一调仓逻辑写在next方法里用bar_count和last_rebalance配合控制调仓频率。这样避免在每一根K线上都重复调仓减少换手更贴近真实操作。第二我用order_target_value来调仓它内部会计算当前持仓和目标持仓的差异自动发单不需要你手动计算目标股数。第三我加了set_slippage_perc按成交金额的千分之一模拟滑点这比完全不设滑点要靠谱得多。3.3 多股回测必改的几个默认设置用backtrader跑多股回测时有几个默认设置如果不改很容易踩坑。先是最基础的资金和手续费。backtrader默认的起始资金是1万默认的手续费是0。如果直接跑你的回测结果会显得收益特别高因为完全没有交易成本。我一般上来就先设置setcash和setcommission把资金设成自己实盘计划使用的金额手续费按实际券商费率填写A股还要把卖出印花税一起算进去。然后是成交量限制。backtrader默认认为你的订单在下一个bar就能按目标价全部成交这显然过于乐观。想要更真实可以设置VolumeFillers例如限制一次成交量不能超过某个bar成交量的固定比例否则订单只执行一部分。对小市值组合回测来说这个设置尤其重要它能让流动性问题在回测阶段就暴露出来而不是等实盘被市场教训。另外要注意股票列表的时间对齐。多股回测时不同股票上市时间不同、停牌频率不同backtrader会在bar序列上做对齐操作。你需要在脚本里明确处理缺失值或者干脆统一设定一个共同的开始日期避免因为数据长度不一致导致策略在有些标的上根本没信号。3.4 用backtrader跑完曲线后先别高兴跑完回测看到一条向上的曲线先别急着截图发朋友圈。你得做几件“扫兴”的事情。第一看换手率。如果策略一年换手20倍那手续费和滑点的影响会非常可观。你把手续费改成0.003再跑一次看曲线变化率是多少。如果收益缩水50%以上那这策略在实盘里基本没有竞争力。第二看最大回撤的位置。回撤出现在什么时候如果回撤都集中在牛转熊那一段说明策略风格和市场状态绑定得很深未来能不能适应是个大问号。第三做极端敏感性测试把top_n从2改成3、4把lookback从20改成10、30看策略表现是不是大幅波动。如果某个参数的微小变化会导致结果剧烈恶化那你手里的不是一个稳健策略而是一颗定时炸弹。我记得有一次帮朋友看一个ETF动量策略回测结果年化25%看起来中规中矩但很稳定。我让他把调仓周期从5天改成6天结果年化直接掉到11%。这说明策略对调仓频率非常敏感实盘操作中稍微一犹豫收益就跑没了一大半。这种策略就算曲线再好看也没有实盘价值。4. 用“体检思维”给策略做减法4.1 给每个因子做“单独体检”因子太多怎么办我的建议是别急着合并成策略先给每个因子做“单独体检”。用行业里的说法就是测单因子的IC值、IR值、分组收益以及换手率。IC衡量的是因子值和未来收益的相关性IR是IC的均值除以标准差IR越高说明因子状态越稳定。但只看IC还不够。两个IC差不多的因子可能在因子构成上是高度相关的比如“市盈率倒数”和“盈利收益率”本质上是同一个东西。你先用相关性矩阵筛查一遍把相关度超过0.7的因子归成一个大类每个大类里只保留逻辑最顺、经济学解释最扎实的那一个。这样因子库可以迅速瘦身策略逻辑也更容易被自己理解。具体操作上你可以把每只股票的历史数据按因子值从高到低分成5组或10组然后看分组的未来收益。如果因子有效应该呈现单调性——最高组的平均收益显著高于最低组。如果分组收益图是一条乱糟糟的脉冲线中间还有一些奇异点那这个因子大概率只是统计噪音。4.2 样本外测试和Walk-Forward分析这是我从“回测艺术”走向“实盘不至于太惨”最重要的一步。最简单的做法是把历史数据切成两段前80%作为样本内用于探索因子和初步确定参数后20%作为样本外只能跑一次不能看到结果不好就反复调整。如果你在样本外区间上策略依然保持了不错的收益水平和合理的回撤那至少说明它没有纯靠过拟合历史。更进阶一点可以用Walk-Forward Analysis滚动前推分析。思路是设定一个优化窗口比如24个月每12个月滚动一次用窗口内的数据选参数然后在下一次12个月的样本外区间里进行模拟交易。每个区间只测一次测完就把这段结果记下来绝不回头修改参数。这种“滚动优化外推验证”的方式非常接近实盘环境下的决策过程能有效减少未来函数的影响。我当时开始用Walk-Forward后最大的感受是很多在传统回测里看起来不错的因子在滚动验证里根本活不过三期。倒是那几个逻辑简单到“土”的因子比如低波动、质量分红类的反而能稳定跑赢。这不是偶然因为越简单的因子背后越可能有长期存在的行为学偏差支撑而不是短暂的市场风格偏好。4.3 模拟盘和最小实盘把成本全部暴露出来回测通过之后不要急着上大仓位。先开一段时间的模拟盘或者用最低资金跑实盘核心目的不是赚钱而是记录“回测假设价”和“实际成交价”的差距。具体做法很简单每天记录策略给出的目标价位和模拟盘实际成交价连续记录一个月。月底算一笔账实际成交平均比回测价差多少如果是买入实际成交价是不是普遍比回测价高如果是卖出是不是普遍比回测价低这个差值就是真实滑点的“账本”。如果一个月下来滑点成本超过你策略预计月收益的20%那你这策略离实盘还有很远的距离。我做最小实盘时还有一个习惯一开始把回测收益预期下调30%到50%作为心理准备。不是自欺欺人而是给执行偏差留出缓冲地带。假如策略回测年化20%那你就按年化10%到14%来预期实盘表现。这样万一实盘收益只有一半你的情绪也不会崩依然有耐心按纪律去执行。4.4 给“因子收集癖”立个规矩管理因子收集癖不能用“从今天起不再收集新因子”这种戒断法效果通常很差。我自己的经验是立具体的规矩把收集行为放进一个可控的框架里。规矩一新因子必须过三关才能进入正式候选池。第一关有明确的经济学逻辑或行为金融学解释第二关在样本外区间IC均值超过现有因子池的中位数第三关与新因子池里已有因子的相关性低于0.6。三关都过才值得花时间深入测试。规矩二固定“策略体检日”。我每月第一个周末做一次全面的因子池回顾平时不回看、不调整。这个规则能有效减少因为盘中冲动而产生的无效优化。很多过度拟合都是在你“临时起意改参数”的那一瞬间产生的。规矩三定期“清理库存”。每个季度把因子池里表现落后、逻辑陈旧、无法解释的因子删掉保留质量最好的那十几个。一个精简的因子池比一个庞大但混杂的因子库更有战斗力决策速度也快得多。5. 实战中的翻车现场与排查手册5.1 五大经典翻车现场为了让你对“回测艺术”有更直观的印象我把这些年见过的翻车案例总结成一个速查表。你对照着看自己的项目中了几条。翻车类型典型表现排查方式未来函数回测收益平稳但调仓记录里出现“收盘价买入后当天就大涨”检查信号用到的字段是否在决策时点已经可获幸存者偏差股票池用的是当前市值或当前成分股回测区间拉长到10年以上改用历史的股票池快照或者用全市场剔除ST成本低估回测佣金设0印花税没算没有滑点换手率还特别高分别按0、1倍、2倍成本跑三遍对比收益衰减流动性陷阱策略选出的标的基本是微盘股组合里单票仓位过大统计每天信号的成交额占比超过5%就要警惕参数孤岛最优参数只有一个点邻域参数表现明显变差画出参数平面热力图看是否存在连续高收益区域这五类问题我在自己项目里全踩过。尤其是流动性陷阱当年做小市值因子时回测年化能到50%但实盘账户却原地踏步。后来一查才发现信号股里很大一部分是日成交额几百万的票我的实际下单量一进去就把价格推高了买到的全是别人塞出来的货。5.2 一步一步排查从数据到执行如果你已经有一个回测结果但不确定能不能信我建议按下面的顺序排查。先看数据层。检查股票池口径、复权方式、财务数据发布时间、停牌处理。把任意一个交易日的持仓明细拉出来手工核对几笔看信号产生时所有用到的字段是否已经真实存在。再看模型层。重点检查参数选择过程。如果优化了超过20组参数最后拿出来的最优结果天然带有数据窥探风险。把样本外测试结果和样本内结果放一起对比如果差距巨大说明策略对历史数据依赖过重。最后看执行层。回测环境里设置的成本、滑点、成交量模型是否贴近自己实际交易的券商环境你可以用一天的实盘成交记录反向到回测框架里模拟一遍看订单成交价和实际成交价的偏差有多大。我做一次完整的排查通常要花掉和开发策略差不多的时间但收益率值得。因为如果你发现了某个隐藏的未来函数那不是修一个bug的问题而是避免了一场“看着稳健但实际必然亏钱”的实盘灾难。5.3 写了几年实盘才想明白的事最后说几点我自己这几年最深的体会。第一最有效的因子往往来自业务理解而不是纯数据挖掘。我手上最稳定赚钱的因子大多和上市公司的财务质量、盈利稳健性、行业景气度有关。它们的逻辑清晰也更容易解释即使一段时期表现一般也不至于让你彻底怀疑人生。相比之下那些靠复杂卷积网络从行情里挖出来的“玄学因子”回测再漂亮我也会自动降低信任等级。第二回测不是用来证明自己对的而是用来发现自己在哪些地方可能错了。带着这个心态去跑回测你会更关注那些回撤巨大、不赚钱的区间而不是紧紧盯着收益曲线。那些亏损区间往往藏着策略的真正命门。能回答“策略在什么行情下会亏钱”的人才算是摸到了实盘的门槛。第三心态和资金管理比任何因子都重要。如果你只有10万块钱却做了一个最大回撤30%的策略那就算逻辑再正确你也很难坚持到盈利那天。仓位控制、杠杆水平和回撤承受能力应该在你开始研究因子之前就定好。这些不是回测能帮你解决的只能靠自己的纪律。我自己的因子目录现在还是会不断增加新文件但我已经很少为一条漂亮曲线心跳加速了。因为我知道把那些坑全部排掉之后真正能剩下的东西其实很少——少到有时候会让人觉得无聊。可就是这个“无聊”的策略在实盘里走得最稳。回测这件事最大的价值不是给你一个漂亮的净值图而是强迫你在投入真金白银之前把所有错误都犯一遍。

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

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

免费获取报价