尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

backtrader多股月末调仓回测:从数据清洗到绩效归因的完整实践

发布时间:2026/9/24 22:31:19

资讯中心
01
ARTICLE

backtrader多股月末调仓回测:从数据清洗到绩效归因的完整实践

backtrader多股月末调仓回测:从数据清洗到绩效归因的完整实践
你打开回测报告的第一件事是什么我以前肯定是先看收益曲线直到上个月把“月末策略标的”重新拉出来做了一轮复盘才发现自己之前的回测框架里埋了好几个雷。这次“再研究”不是简单换个参数再跑一遍而是把整个回测链条——从数据清洗、复权处理、多股组合撮合到绩效归因——全部重构了一遍用 backtrader 做了多股月末调仓的完整回测。如果你也在做类似的中低频策略或者正准备从单标的回测跨到多标的组合回测这篇内容大概率能帮你少踩几个坑。先说结论月末策略本身不复杂复杂的是怎么把“每月最后一个交易日调仓”这件事在回测引擎里真实地表达出来以及怎么让回测结果不被数据细节骗过去。下面我把这次回测再研究的完整思路拆开讲。1. 为什么是月末这次再研究到底想验证什么1.1 月末调仓这种节奏的底层逻辑很多人第一次听到“月末策略”会觉得有点玄觉得调仓频率这么低能有什么超额收益但实际上月末调仓在量化策略里有它非常实在的优势。核心逻辑有两个一是规则清晰、可复现二是天然避开了周内情绪波动。从规则角度讲每月最后一个交易日收盘后做调仓是一个极其明确的触发条件。系统不会因为行情大涨大跌就临时改变计划也不会因为“感觉今天该买”这种主观因素破坏纪律。对于程序化回测来说这种确定性意味着信号生成、订单执行、绩效统计的每个环节都能严格对齐。从市场微观结构的角度讲A股或港美股市场都存在一定程度的“日历效应”比如月初资金面相对宽松、月末流动性偏紧、财报披露期的窗口效应等。月末调仓实际上是在利用这些规律性波动的“价差窗口”在月底锁定一批相对便宜的标的持有到下一个月底。当然这个逻辑在单个月度里不一定每次都对但放到十几年、几十年的长周期里统计优势会被慢慢放大。这次“再研究”之所以叫“再”是因为我之前做过一版简单的月末策略每月末按某几个技术指标打分取排名前10的股票等权买入。当时跑出来的年化有百分之二十几回撤也能控制在15%以内看起来挺美。但后来我逐步发现几个问题一是数据没有做严格的停牌处理有些标的在调仓日根本买不进二是手续费和滑点设置得太乐观三是打分因子里使用了未来数据。所以这次重构我给自己定了三个核心验证目标。1.2 这次回测要验证的三个问题目标一验证因子打分在月末这个特定时点上是否真的具备区分度。也就是说按打分选出来的前10只股票下个月的表现是否显著优于后10只或全市场平均。这个验证决定了策略是否真的有“选股能力”而不只是beta在贡献收益。目标二验证调仓成本吃掉多少超额收益。月末调仓虽然频率低但每次调仓涉及换手如果持仓标的重合度不高一次调仓可能换掉七八成仓位双边手续费加滑点加冲击成本一个月吃掉1%以上很正常。这个成本到底有多大必须用真实的交易费用模型去测而不是拍脑袋设个万二。目标三验证不同权重分配方式对结果的影响。等权、市值加权、按因子得分加权三种方式在收益曲线、最大回撤和换手率上会有明显差异。这个问题我过去一直没细究这次专门做了对比回测。这三个问题本质上都是在回答这个策略的超额收益到底从哪来是靠选股、靠择时、还是靠运气。回测的意义不是让我们看到一个漂亮的数字而是把收益来源拆解到可以理解的层面。2. 回测框架选型为什么还是 backtrader2.1 backtrader 到底适合哪类回测现在市面上的回测框架不少有专门做事件驱动的有偏向量化的也有商业软件一键回测的。我这次仍然选择 backtrader原因很简单它在“多股组合规则化调仓”这个场景下是我用下来最顺手、也最不容易出错的工具。先解释一下 backtrader 的定位。它是一个事件驱动的回测框架核心思路是数据源Data Feed驱动引擎向前推进引擎在每个 bar 上通知策略Strategy策略根据当前状态发出订单Order订单经经纪商Broker撮合后产生成交最后由分析器Analyzer统计绩效。这种设计非常贴合真实交易的流程尤其是对月末调仓这种低频策略来说多一个 bar 的偏移、多一个停牌日未成交都会让结果产生偏差事件驱动能把这些细节暴露出来而不是藏在向量化计算的黑箱里。相比之下纯向量化回测的优点是速度快几十行 pandas 就能算完十年数据但缺点是对交易约束的表达很弱。比如涨跌停不能买入、停牌不能卖出、最小交易单位是100股、资金不足时订单部分成交等这些在真实交易里都是硬约束向量化回测里很难精细处理。而 backtrader 的 Broker 模块可以模拟这些约束虽然速度慢一些但结果更可信。另外backtrader 支持多数据源多股票同时加载每个数据源可以有自己的时间轴、复权因子、交易状态标记。这个特性对多股回测太关键了。你不需要把多只股票的日线数据拼成一个宽表也不需要自己处理股票停牌导致的日期错位只要把每只股票作为独立的 Data Feed 塞给大脑Cerebro框架会自动按时间对齐事件。我这次做的是月末多股调仓10到20只股票日线级别周期大概十年。用 backtrader 跑一遍只需要几十秒完全在可接受范围内。如果你做的标的数量更多比如几百只股票的全市场筛选那 backtrader 会有些吃力建议先做一层预筛选或者换用向量化框架做初筛再进 backtrader 做精细回测。2.2 数据准备阶段最容易埋雷说句实在话多股回测里框架选型只是第一步真正决定回测结果可信度的是数据准备。很多人一开始学会 backtrader 就急着写策略结果跑出来的曲线漂亮得不像话后来一查数据里全是坑。这里我列几个必须注意的地方。第一复权方式要统一。股票的日线数据一般有前复权、后复权和不复权三种形态。回测里最常用的是前复权因为它保证最后一根K线的价格是真实的市场价格便于和当前行情对照。但前复权有个问题随着每次分红除权历史价格会被重新计算导致你第二次跑回测时结果和第一次略有差异。如果要做严格的可重复研究建议用后复权数据做计算只在最后展示时换算成前复权价格。第二停牌处理需要显式标记。多股回测中一只股票停牌意味着当天没有成交数据backtrader 默认情况下会把数据视为缺失而不会自动跳过。这在月末调仓时会很致命如果你的选股池里有股票在调仓前停牌数据会短缺订单撮合也会出问题。我的做法是在数据预处理阶段把停牌日的 OHLCV 全部填充为前一个交易日的收盘价同时加一个 status 字段标记“不可交易”。策略里在调仓时先检查 status不能交易的标的一律跳过。第三上市时间不足导致的新股效应。如果选股池里有次新股上市初期波动大、容易暴涨暴跌这会给回测带来很大的噪音。更严重的是一些“借壳上市”的股票复权后的价格会发生剧烈跳变甚至出现负价格或极端值。我在数据清洗时设定了一个硬性过滤上市不足 60 个交易日的股票不进入候选池。第四退市股不能直接丢弃。很多人下载历史行情时习惯性把已经退市的股票删掉这在多股回测里是非常严重的“幸存者偏差”。如果你只回测今天还活着的股票等于默认自己在过去每次调仓时都准确避开了未来会退市的垃圾股这当然会显著拉高回测收益率。正确做法是保留退市股把它们纳入选股池让策略在历史中的每一次调仓都面对完整的市场集合哪怕它在某个时间点以后就再也买不到了。2.3 手续费、滑点和涨跌停的现实约束还有一块数据准备之外的关键工作是把交易成本模型写对。很多网上教程里手续费就写个 commission0.0003 完事这在单标的回测里还能凑合多股组合回测里完全不够。我这次把成本模型拆成了三部分佣金按成交金额的比例收取双边都要收这里我设的是万二点五最低 5 元。虽然现在很多券商线上佣金能谈到万一点五但回测里留一点余量更稳妥。印花税这个只有在卖出时收取不同市场的税率不一样我按千分之一的卖出税率估算。滑点与冲击成本这部分最容易被忽视。月末调仓当天如果你要买入一只市值偏小的股票订单量占当日成交量的比例过高你的实际成交价会明显偏移。backtrader 里我通过CommissionInfo类的perc参数来模拟这部分成本比如设置额外 0.001 的冲击费用相当于买卖双边各 10 个基点。对日内成交量小的标的我会在选股打分阶段直接对“过去20日平均成交额”做限制低于某个阈值的股票直接剔除。涨跌停不仅影响能不能买进卖出还会影响成交价。真实交易中一字涨停的股票你是买不进去的一字跌停的股票你是卖不出的。backtrader 的 Broker 默认不处理涨跌停需要你在策略里自己判断如果某只股票当天一字涨停且挂买单撤单一字跌停且挂卖单撤单。我是在next方法里通过比较open high low这种条件来识别一字板这个判断虽然粗略但实践下来已经能过滤掉绝大多数无效成交。3. 多股组合回测的核心实现细节3.1 选股池和月末调仓信号的生成这次回测我使用的是“动态股票池”也就是每个调仓日之前重新筛选一次候选标的而不是固定死 20 只股票从头跑到尾。这么做的好处是贴合真实投资逻辑策略可以及时纳入次新股、剔除基本面恶化的标的也能让回测结果反映策略真实的换手率。数据准备阶段我先把一个包含约 300 只样本股票的历史日线数据全部加载进来每只股票保存成单独的 CSV 文件字段包括日期、开盘、最高、最低、收盘、成交量、成交额、涨跌幅。加载到 backtrader 里时我用bt.feeds.GenericCSVData逐只导入然后通过data._name属性区分不同的标的。月度信号的生成逻辑如下在每一天的next回调中先判断当前日期是否是本月最后一个交易日。判断方法很简单比较下一个交易日的月份是否与当前月份不同def is_last_trading_day_of_month(self, dt): try: # 找到当前数据在时间轴上的下一个交易日 next_dt self.data.datetime.date(0) # 此处为示意 return False except: return True实际实现时我不会在next里依赖单条数据流而是用self.datas里所有数据源的最大共同交易日来做判断确保任何一只股票的下一根K线都还没有到来。简单可靠的做法是预先把交易日历生成好用字典映射日期到“是否为月末”标志位。这样无论有多少只股票信号判断都是 O(1) 的。到了月末调仓日我会遍历整个股票池计算每只股票的打分因子然后从高到低排序取前 10 只作为目标持仓。打分因子这次用的是四个动量过去20日累计涨幅剔除最近5日防止短期反转噪音、波动率倒数、成交活跃度、基本面排雷剔除账面市值比极端的标的。每个因子先做百分位排名再等权合成总分。3.2 权重计算与订单管理选好 10 只股票后接下来要确定每只股票的买入数量。我做了两组对比回测一组是等权买入另一组是按因子得分加权。等权最简单直接target_value total_cash / 10然后除以收盘价取整加权就是先归一化因子得分再按得分比例分配资金。回测里有个非常现实的细节资金分配必须处理“整手”约束。A股、港股的交易单位不同我这次先以100股为单位取整。比如某只股票目标市值 10 万元价格 20.35 元那么理论上可买 4914 股但实际下单时只能买 4900 股多出来的几十股现金就留在账户里。这个取整逻辑不处理好会让回测的资金利用率偏低或者下单数量不合规。backtrader 里做组合调仓最方便的方法不是直接buy()而是使用order_target_percent和order_target_value。这两个方法会自动计算当前持仓与目标之间的差额并生成相应的订单。我会在调仓函数里循环所有目标股票for data in self.datas: name data._name if name in target_positions: target_weight target_positions[name] self.order_target_percent(datadata, targettarget_weight) else: # 不在目标持仓中清仓 self.order_target_percent(datadata, target0.0)要注意的是order_target_percent参数里的target是相对当前总资产的比例而不是初始本金。如果账户里已经有浮盈它买入时使用的目标市值会自动放大这点比较符合真实账户操作。不过它也带来一个副作用当组合里出现暴亏、总资产缩小时系统会自动把每个仓位的目标市值调低产生额外的卖出。这意味着策略的调仓不只在月末发生遇到单日大幅波动也会在第二天的next回调里被动调整仓位。为了解决这个问题我会在每个非调仓日的next里直接return只在月末调仓日才执行order_target_percent这样就锁定了真实的调仓节奏。订单提交后不能撒手不管。我在策略里维护了一个pending_orders列表在notify_order回调中跟踪订单状态提交、已接受、部分成交、完全成交、已取消、已拒绝。月末调仓日的一批订单理论上应该在当天收盘前全部成交但如果遇到停牌或涨跌停部分订单会无法成交。我的处理是当检测到订单已取消或已拒绝时把这只股票的调仓标记为“失败”后续不再主动重试。因为月末调仓本身有一定容错性个别股票买不进就放弃把资金留在账户里下个月初再重新分配这比死磕一个买不进的标的要理智。3.3 绩效统计与收益归因回测跑完之后不能只看一条光秃秃的收益曲线。我这次在 cerebro 里挂了一组分析器包括收益率AnnualReturn、TimeReturn最大回撤DrawDown夏普比率SharpeRatio使用无风险利率 1.5% 计算交易记录与换手率统计其中换手率是我特别关注的。换手率等于调仓时买卖金额之和除以月初总资产。如果策略的平均月度换手率是 80%那么一年下来双边换手接近 10 倍每 10 倍换手对应 1% 的交易成本可能就是年化收益从 15% 掉到 12% 的差距。backtrader 自带的 Analyzer 不直接给出月度换手率我是在策略里手动统计每笔成交的金额累积到一个字典里回测结束后再计算。收益归因上我做了两套对比基准一是买入持有全市场等权指数二是“每月末随机选 10 只股票”的随机策略。为什么要做随机基准因为如果选股池整体就在走牛那么任何选股方法都能赚钱你无法判断收益来自策略还是来自市场。随机策略和真实策略用同一个股票池、同一个调仓频率、同一个成本模型跑 100 次取平均收益如果真实策略显著跑赢随机基准才能说明选股因子确实有效。这个对比方法是我这次“再研究”里最有价值的部分。4. 回测结果里那些反直觉的东西4.1 收益曲线好看但要看回撤位置第一版重构后的回测结果年化收益和之前差不多还是 20% 附近但最大回撤从 15% 扩大到了 21%。原因很简单之前省掉了涨跌停约束和停牌处理最差的行情里策略可以满仓杀进去也可以顺利出逃回撤自然好看。加入现实约束后碰上连续跌停的月份持仓出不来回撤就被动放大了。我的体会是回测中最大回撤的绝对值只是参考更要紧的是看最大回撤发生的时间点和持续时长。如果最大回撤集中在某一次股灾期间并且在之后的半年内明显修复那说明策略的恢复能力不错。如果回撤是均匀分布在每一年里的回头看你的风控模型可能有系统性漏洞比如行业过度集中、单只股票占比过高。我这次在结果分析里给每一年单独列了一个收益和回撤的分布表发现最差的年份不是市场整体下跌的年份而是个股分化极其剧烈的年份。说明月末策略本身没有择时能力它只能在市场风格相对稳定时跑出超额。这个结论很关键它提醒我如果要实际跑这个策略不能全仓押注必须预留一部分现金仓位做保护。4.2 空仓期其实是超额收益的重要来源回测里另一个反直觉的发现是策略的空仓期手上现金超过 20% 的时间段不是劣势反而对最终收益贡献很大。原因在于月末调仓策略本质上是一个高换手的组合在市场下跌阶段调仓买入的标的往往会继续下跌如果满仓操作回撤会被不断放大。而我的策略里因为整手取整、涨跌停限购等原因自然会产生一部分未被使用的现金这些现金在下跌行情里起到了缓冲垫的作用。这不是我最初设计的目标但回测数据让我意识到一个真实的账户在运行月末策略时完全没必要强行满仓。保留 5% 到 10% 的现金冗余既不影响长期收益还能显著降低尾部回撤。在资金管理上我会在回测之后再做一个“现金比例敏感性分析”分别测试 0%、5%、10%、20% 的现金预留对收益回撤的影响最终选择一个在风险收益比上最舒服的比例。当然现金比例太高也会拖累收益。在牛市中段10% 的现金预留可能会让年化收益降低 1 到 2 个百分点。完全没仓位管理的人容易走极端要么满仓要么空仓。正确的做法是通过回测找到“最大回撤容忍度”和“收益目标”之间的平衡点然后把现金仓位固定下来不要随意变动。4.3 换手率和交易成本之间的敏感关系这次回测我特意做了不同交易费用档位的对比无费用模型、低费用模型双边万五、高费用模型双边千二。结果不出意料费用越高策略的超额收益越薄但有趣的是策略的排序稳定性并没有因为费用模型改变而大幅变化。也就是说如果一只股票在无费用模型下排第一那么在千三费用下它大概率仍然是靠前的标的。不过换手率高的月份对费用极其敏感。某些月份因为市场风格切换选股池和上月几乎完全重叠换手率只有 30%这时候费用影响微乎其微但在另一类月份选股池几乎全部换了一遍换手率到了 150%双边费用直接吃掉 1.5% 的收益。如果不控制这种极端月份的发生频率策略的年度收益方差会很大。我的应对方法是引入一个“换手率惩罚项”当新选出的股票和当前持仓重叠度低于 40% 时打分会乘以一个 0.95 的折扣尽量让系统在调仓时保持一定的持仓连续性。这个思路在实盘中很常见基金经理换仓时也会考虑交易成本不会把组合一锅端。5. 常见问题与避坑实录5.1 回测中隐蔽的“未来函数”陷阱我在这次回测里排查出一个特别隐蔽的未来函数来源是打分因子的计算。我的动量因子定义是“过去 20 日累计涨幅”但如果用close(-1)和close(-21)来算在月末那根K线还没走完时close(-1)实际上已经包含了收盘价而信号生成发生在当天盘中或开盘前这就引入了未来数据。正确的做法是使用close(-2)作为信号基准或者确保回测引擎只在每根K线收盘后才触发调仓信号。backtrader 在数据处理顺序上非常严格正常情况下next是在当前 bar 收盘后调用的所以直接使用self.data.close[0]作为信号是安全的。但如果你在自定义的 Data Feed 或 Analyzer 里引用了未来数据比如用self.data.close[1]去计算当前信号就很容易踩雷。我的排查方法是把回测中的每个调仓日的信号标的和复盘时的真实行情对照如果出现“计算因子需要收盘价但信号生成的日期当日已经涨停”这种情况就说明可能在数据的时序上有偏差。一个更系统的做法是在回测引擎之外写一个独立的“信号复核”脚本从历史数据里按规则重新计算每个调仓日的选股结果再和 backtrader 输出的交易记录做差分对比。如果差异超过 1% 的交易笔数就说明信号链路中存在未察觉的时间错位。5.2 复权数据使用不当带来的假信号复权数据是多股回测中另一个大坑。我举个最简单的例子某只股票在 6 月 30 日实施了 10 送 10除权后股价从 50 元变成 25 元。如果你用不复权数据做动量排名6 月 30 日的累计涨幅会出现一个巨大的假跌幅该股票直接进入“超跌”区间可能会被策略选中买入。但真实情况是只是股本变大了每股净资产没变这并不构成买入信号。应对这件事最稳妥的办法是在因子计算时使用后复权价格。后复权价格把所有历史分红和拆股都累加到最新价格上保证历史价格之间的可比性。但后复权数据也有问题最新价格和真实市场价格偏差很大不适合用来计算账户市值和下单数量。我的做法是“双轨制”因子计算用后复权价格订单的下单价格和账户结算用前复权价格在策略内部通过一个价格转换函数做换算。这个细节烦琐但非常重要。如果图省事全部用前复权那么每次分红除权后前复权历史价格都会被整体重算奖金因子序列也会被“重写”导致几个月后重新回测时结果对不上。如果你希望自己的回测结论可复现、可跟踪这关必须过。5.3 多标的并行时的数据对齐、停牌与最小交易单位最后一个常见问题是多股票数据源的日期对齐。不同股票可能有不同的节假日停牌日期比如有些股票因为临时股东大会停牌半天有些股票因为异动停牌一天它们的交易日期并不完全重合。在 backtrader 里所有 Data Feed 的时间轴需要对齐到同一个“交易日历”上否则引擎会在某只股票数据缺失的日期上直接跳过导致信号判断错位。我用的办法是在数据加载前先构造一个全市场交易日历作为主时间轴然后把每只股票的数据 reindex 到这个主时间轴上缺失的日期填充为NaN或者前值。在next循环里我始终用主时间轴的日期做信号判断然后再检查个别股票当前 bar 是否有效。最后提醒一下最小交易单位的问题。我这次策略里100 股的取整逻辑在 backtrader 里并不是默认行为。order_target_percent默认按最大可用资金计算股数但不一定按整手约束。如果你回测的市场有整手限制必须在broker的order方法里自己算好size或者写一个自定义的Order类去强制取整。动作虽小相差甚远忽略它会导致回测结果和实盘差异很大尤其是在小市值股票上。写在最后的一点建议如果你准备在自己的策略里引入多股回测我的建议是不要一上来就追求复杂的组合优化和机器学习模型先把最基础的月末等权调仓做扎实把数据、成本、约束都处理清楚拿到一个可信的基准结果再逐步叠加因子和风控逻辑。回测的本质是帮助我们理解策略在什么环境下有效、什么环境下失效而不是寻找一个永远赚钱的圣杯。我这次月末策略的重构最大的收获不是年化提高了多少而是终于把回测中那些藏在数字背后的假设一个个挖了出来。以后你在看任何一份回测报告时都可以多问一句它的数据里有幸存者偏差吗它的成本模型现实吗它的信号里有没有未来函数带着这些问题去审查你自己做回测的水平会提升一大截。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。