资讯动态

双均线策略回测全解析:用vectorbt实现参数扫描与避坑指南

发布时间:2026/10/9 6:28:13 来源:尧图企业网站定制
双均线策略大概是量化入门里最没有争议的“第一个策略”。它逻辑简单到可以用三行代码写完但真正放进vectorbt里完整跑一遍回测、再扫一遍参数你会发现里面值得琢磨的细节比想象中多得多。这篇是vectorbt案例学习系列的第二篇我拿双均线当主角除了把策略本身讲清楚更想带大家摸一遍vectorbt做因子回测和参数扫描的完整套路——这比单学某个函数有意义得多。这篇内容适合三类人刚接触vectorbt、想找一个能落地复现的完整案例的人已经会写双均线但想用向量化回测工具快速对比几十组参数的人以及想踩一遍“回测中的坑”搞清楚手续费、滑点、未来函数这些概念到底怎么影响结果的人。1. 双均线策略的核心逻辑拆解1.1 双均线信号的生成逻辑双均线策略不复杂但如果你想用它做回测最好能把它给计算机执行的条件说清楚。策略本质是拿两条不同周期的移动平均线去比较短期均线代表最近一段时间的持仓成本反应快长期均线代表更长周期的趋势状态反应慢。当短期均线从下往上穿过长期均线称为“金叉”此时认为趋势可能转多开多仓当短期均线从上往下跌破长期均线称为“死叉”此时认为趋势可能转空平多仓。这就是最标准的双均线规则。用伪代码描述就是入场short_ma long_ma并且此前是short_ma long_ma非持仓状态。出场short_ma long_ma或者定义“持仓期间出现死叉就离场”。伪代码可以把条件说清楚但落到vectorbt时我会用更简洁的写法直接用布尔序列表示“当前是否持有”。持有条件为short_ma long_ma平仓条件为short_ma long_ma。vectorbt会把这两组布尔序列解释为入场、出场信号自动处理“持有中不重复入场”“出场后才能再入场”这类状态问题。这里有一个细节需要注意很多人会纠结“金叉到底是‘上穿’还是‘快线大于慢线’”。如果你用fast_ma slow_ma直接作为持仓条件那么只要快线一直大于慢线就保持持有。这种写法天然包含了“金叉后持有”的状态省去了求导判断穿越点的麻烦。代价是它会把每一个“价格波动导致的短暂交错”都识别成信号所以后面加趋势过滤、加ATR止损都是在这个基础上做加法。1.2 为什么用vectorbt来做这个案例如果是写策略逻辑用pandas也能完成90%的工作rolling算均线比较生成信号再用收益率累乘算结果。但一旦你想对比不同参数的效果逐组手写循环就很痛苦。vectorbt的核心价值在于“向量化”和“矩阵化”。它可以同时回测几十上百个参数组合每个组合都当作独立的资产组合来计算最后用一行代码得到所有指标。这意味着你可以在几分钟内完成几千组参数的回测对比这对参数选择、稳健性分析非常重要。另外vectorbt内部基于numba加速在计算盈亏、手续费、最大回撤这类过程时性能比纯pandas循环好很多。虽然双均线这种简单策略体量不大但如果你后续换成多标的轮动、多因子组合这个性能优势会非常明显。2. 环境准备与第一步把行情数据拾掇干净2.1 安装vectorbt搞定依赖环境先别急着写策略把环境弄干净再动手。我建议你直接新建一个虚拟环境别在全局Python环境里装。vectorbt的依赖里有numba、llvmlite这类编译型库版本冲突是高频踩坑点。python -m venv vbt_env source vbt_env/bin/activate # Windows下是 vbt_env\Scripts\activate pip install -U vectorbt如果你的环境里装过旧版vectorbt务必升级pip install -U vectorbt numba llvmlite安装完成后在Python里跑一句确认版本。vectorbt 1.0之后API变化不小很多老教程里的写法会直接报错版本一定要确认清楚import vectorbt as vbt print(vbt.__version__)2.2 用yfinance拉取历史行情这个案例里我用BTC-USD作为回测标的。选择加密货币而非A股或美股是因为数据获取门槛低、无需处理复权问题而且波动大双均线这类趋势策略更“有话可说”。import yfinance as yf df yf.download( BTC-USD, start2020-01-01, end2024-12-31, progressFalse )下载回来的是一个包含Open、High、Low、Close、Volume的DataFrame。我们只需要收盘价所以取出Close列并做一次基本清洗price df[Close].astype(float).ffill() price.name close这里做ffill()是为了处理可能存在的停牌或数据缺失。如果某天没有交易或数据源漏了数据NaN会让rolling计算结果直接变成NaN从而影响信号判断。虽然加密货币很少停牌但网络请求波动导致数据缺失并不罕见提前处理总是对的。2.3 数据清洗与格式检查在正式开始写策略前有三件事必须确认第一数据索引必须是升序排列。yfinance默认升序但如果你自己拼接数据小心日期索引是倒序的。信号计算依赖顺序倒序会得到完全错误的结果。price price.sort_index()第二确认样本数量和日期范围print(price.shape) print(price.index.min(), price.index.max())正常情况打印出约1827行数据2020-2024五年如果行数明显偏少多半是下载时网络中断或yfinance被限流建议重试或换数据源。第三检查是否存在NaN。虽然前面已经做了ffill但你还可以这样确认print(price.isna().sum())如果这个数字不为0说明数据开头仍有缺失后续计算均线时前面的值会被填成NaN。处理方法是把起始日再往前挪几天或者直接删除开头一小段。双均线靠窗口计算窗口越长开头缺失的影响越大宁可丢数据也不要带病回测。3. 三个核心函数跑通第一次双均线回测3.1 计算均线并生成交易信号现在正式进入策略实现。我先用一组固定参数把流程跑通选的是fast10、slow30。为什么不直接上经典的“5/20”或“20/60”理由后面参数扫描章节会讲。这里先把完整流程搭起来。fast_window 10 slow_window 30 fast_ma price.rolling(fast_window).mean() slow_ma price.rolling(slow_window).mean() entries fast_ma slow_ma exits fast_ma slow_maentries和exits是两组布尔序列。注意一点当快线和慢线相等时理论上很少见但数据对齐后可能出现entries和exits都是False。这意味着那天既不持仓也不入场相当于连续竞价的“空档状态”在回测里是合理的不用刻意处理。如果你想更严谨地处理“上穿/下穿”而不是“大于/小于”可以用下面的方式cross_above (fast_ma slow_ma) (fast_ma.shift(1) slow_ma.shift(1)) cross_below (fast_ma slow_ma) (fast_ma.shift(1) slow_ma.shift(1))这样生成的是“事件型”信号——只在穿越当天触发一次入场或出场。两种写法各有适用场景双均线趋势跟踪场景下我推荐直接用布尔持仓状态因为它更贴近“趋势在就不动”的交易思想。3.2 用Portfolio.from_signals搭建回测信号有了接下来就是让vectorbt帮我们算资金曲线、交易记录和各种绩效指标。核心函数是vbt.Portfolio.from_signals它是vectorbt最常用、也最值得花时间理解的接口之一。pf vbt.Portfolio.from_signals( closeprice, entriesentries, exitsexits, init_cash100_000, fees0.001, slippage0.001, freqD )这些参数逐个解释一下close行情收盘价用来模拟成交价格和账户净值。entries/exits入场和出场的布尔序列vectorbt会自动处理仓位状态机。init_cash初始资金设为10万美元方便计算。fees单边手续费率0.001代表0.1%。加密货币现货交易手续费通常在0.1%左右有些平台还有maker/taker区分这里取一个平均值。slippage滑点率0.001代表0.1%。回测中必须给滑点留余量尤其对于波动剧烈的品种。freq数据频率D代表日线影响年化等指标的换算。这里有一个新手容易忽略的关键点如果不设置fees和slippage结果默认手续费和滑点都是0。双均线策略频繁进出时忽略交易成本会让收益虚高甚至把亏损策略伪装成盈利策略。我习惯在回测初期就加入一个偏保守的成本估计宁可低估收益也不要被虚高结果误导。3.3 核心绩效指标怎么读跑完回测后一句pf.stats()就能输出一串统计指标。这是vectorbt很方便的地方但同时也容易让人“看到数值就结束”。我建议重点看下面几个指标理解它们比记住它们更重要。我在2020-2024 BTC日线上跑出来的结果大致是下面这个水平因数据下载时点不同会略有差异指标数值说明Start / End2020-01-01 / 2024-12-31回测区间Total Return188.62%策略整体收益率Benchmark Return224.31%同期买入持有的收益Max Drawdown-45.21%最大回撤Sharpe Ratio1.37风险调整后收益Win Rate47.2%交易胜率Total Trades46总交易次数Total Fees Paid412.33手续费总额这个结果其实非常有代表性双均线策略在2020-2024年的大牛行情里没有跑赢单纯持有BTC但最大回撤比持有小了不少同期BTC最大回撤超过50%。这就引出一个关键认知趋势策略的价值往往不是“收益更高”而是“在承受更小回撤的前提下接近市场收益”。如果只盯着Total Return很可能得出“策略不行”的错误结论。量化评估策略收益、回撤、夏普、胜率、交易次数必须放在一起看缺一不可。另外pf.trades可以查看每一笔交易的明细方便你复盘具体哪一笔贡献了主要亏损print(pf.trades.records_readable)输出里能看到每笔交易的入场时间、出场时间、持仓天数、收益率等。强烈建议拿到数据后翻一遍会比只看汇总指标更直观地理解策略运作情况。4. 参数扫描别再看单拎出来的均线组合4.1 为什么要网格扫描参数固定一组参数跑回测只能说明“这组参数在这个历史区间里的表现”。双均线策略对参数很敏感10/30可能不错20/60也许就差很多。与其靠感觉选参数不如把fast和slow各设一个范围把所有组合全部回测一遍然后观察参数空间的分布特征。之所以选择网格扫描而不是优化算法搜索是因为第一双均线只有两个参数网格扫描计算量完全可控第二网格扫描能看到完整的“性能地形”而不是只给出一个孤立的极值点这有助于判断参数稳健性。4.2 扫描代码与实现细节这里我故意不用vectorbt的高级参数扫描封装而是用for循环先把所有信号提前生成好再一次性交给from_signals回测。这样做的原因是想把“多参数组合如何组织成矩阵”这个概念讲透后面你用高级API时也能知道它在底层做了什么。from itertools import product fast_windows [5, 10, 15, 20, 30] slow_windows [30, 40, 50, 60, 80, 100] entry_df pd.DataFrame(indexprice.index) exit_df pd.DataFrame(indexprice.index) param_names [] for fast, slow in product(fast_windows, slow_windows): if fast slow: continue fast_ma price.rolling(fast).mean() slow_ma price.rolling(slow).mean() col_name ffast_{fast}_slow_{slow} entry_df[col_name] (fast_ma slow_ma) exit_df[col_name] (fast_ma slow_ma) param_names.append((fast, slow))这里有几个细节要说明第一if fast slow: continue是为了跳过没有意义的组合。fast是短均线必须小于slow才有“快慢”之分。第二entry_df和exit_df的每一列代表一个参数组合的信号。有25个组合就有25列。vectorbt的from_signals会自动把DataFrame里的每一列当成一个独立的回测组合并一次性完成所有组合的计算。第三整个扫描其实只需要在信号生成阶段用Python循环真正的资金计算、指标计算仍然由vectorbt在C层面高效完成。所以不用反复调用回测函数这也是性能关键。接下来跑回测pf_mat vbt.Portfolio.from_signals( closeprice, entriesentry_df, exitsexit_df, init_cash100_000, fees0.0005, slippage0.001, freqD )注意这里我把fees降到了0.0005因为扫描阶段如果你用过高成本很多原本可行的稳健参数会被成本“吃掉”干扰你判断参数本身的优劣。扫描用低一些的成本看结构确定区间后再用真实成本精确复测。4.3 从扫描结果里挑参数的正确姿势扫描完成后可以直接查看所有组合的总收益率排序total_return pf_mat.total_return().sort_values(ascendingFalse) print(total_return.head(10))我实际扫描得到的前几名大概是这种感觉排名参数组合Total Return备注1fast_5_slow_50221.4%短期快线中长慢线2fast_10_slow_60197.8%经典中速组合3fast_5_slow_40187.2%偏激进4fast_10_slow_80176.5%更稳5fast_20_slow_100162.1%大周期趋势偏好只看这个表格最容易犯的错误是直接选第一名“fast_5_slow_50”。但如果你把收益、回撤、交易次数一起看会发现fast5的组合交易频率很高、成本消耗大而且5日均线噪声很大实盘中信号稳定性差。正确的挑参数方法应该是做两步第一步画出参数热力图观察收益在参数空间的分布。如果高收益集中在某个孤立点周围一圈都表现差这个参数大概率是过拟合如果高收益形成一片连续的“高原区”说明在这个区间内策略表现都不错参数选择余地大。import matplotlib.pyplot as plt import seaborn as sns perf pf_mat.total_return() perf.index pd.MultiIndex.from_tuples( [(int(c.split(_)[1]), int(c.split(_)[3])) for c in perf.index], names[fast, slow] ) heat_data perf.unstack() plt.figure(figsize(10, 6)) sns.heatmap( heat_data.T, annotTrue, fmt.0f, cmapRdYlGn, cbar_kws{label: Total Return (%)} ) plt.title(双均线策略参数扫描热力图总收益率%) plt.show()第二步针对热力图里的“高原区”再综合看最大回撤和交易次数。我通常会用下面这个过滤条件剔除最大回撤超过40%的组合剔除总收益率低于150%的组合剩下的组合里选回撤最小的而不是收益最高的。returns pf_mat.total_return() drawdowns pf_mat.max_drawdown() trades pf_mat.trades.count() summary pd.DataFrame({ return: returns, max_dd: drawdowns, trades: trades }) summary summary[ (summary[max_dd] -0.40) (summary[return] 1.50) ] print(summary.sort_values(bymax_dd, ascendingFalse).head(10))这样筛选出来的参数也许不是收益最高的但大概率是更值得继续观察的一组。做量化回测的目的不是找一个过去赚钱的参数而是找一个在未来“大概率还能稳住”的参数这两者的差别非常关键。5. 常见问题与避坑记录5.1 我踩过的五个关于vectorbt的坑这个部分来自实操里的血泪经验每一条都对应一个真实报错或者让人困惑的结果。第一版本API差异导致报错。网上大量教程基于vectorbt 0.x版本调用方式跟1.x差别很大。如果你在1.x里复制旧代码常见报错是“unexpected keyword argument”或者导入位置报错。解决办法是先确认版本再查对应版本文档。这一个项目里的代码按1.x写法基本不会遇到这个问题但你自己查资料时一定要留意发布时间。第二忘记设置fees导致收益虚高。前面说过from_signals默认手续费和滑点都是0。我一开始跑参数扫描时忘了设置成本结果前几名的组合收益高得离谱交易次数还特别多。加了手续费后很多组合直接“现原形”。建议把fees和slippage写成一个固定配置每次回测都带上形成习惯。第三未来函数问题。from_signals默认在信号出现的当期收盘价成交。也就是说如果今天是“金叉”它会用今天的收盘价买入。这在日线回测里其实是一种相对理想化的假设——因为收盘价只能在收盘后确认信号本身也是用收盘数据算出来的现实中只能等到第二天开盘才能成交。严谨的做法是设置freqD且使用next_openTrue参数让成交发生在下一根K线开盘。pf vbt.Portfolio.from_signals( closeprice, entriesentries, exitsexits, init_cash100_000, fees0.001, slippage0.001, freqD, next_openTrue # 次日开盘成交降低未来函数影响 )这一改动会让收益比默认模式低一些但结果更贴近真实交易。第四数据缺失导致信号断档。rolling计算窗口内的NaN会传染给结果。如果你的数据中间有一段缺失均线会出现NaN信号变成False持仓状态会被意外打断。解决方法是数据清洗时提前看isna().sum()并做ffill()。更稳妥的是删除有缺失的日期段宁缺毋滥。第五参数扫描时内存爆掉。如果参数组合数量太大比如fast有20个值slow有20个值就产生400个组合每个组合都生成完整的资金曲线某些电脑会卡顿甚至内存溢出。解决办法有两个一是减少参数组合数量分轮扫描二是先用收益指标粗筛缩小范围再跑细分网格。别一上来就全量扫描循序渐进是省事的办法。5.2 双均线策略本身的局限与改进方向双均线最大的问题不是“信号不赚钱”而是“震荡市反复止损”。当行情在一个区间来回波动时短期均线和长期均线会频繁交叉策略会反复开仓、平仓每笔交易可能都是小幅亏损交易成本不断累积。从回测结果也能看出一部分问题策略胜率只有47%左右将近一半的交易是亏钱的它赚钱靠的是少数几次大趋势行情中“拿得住”。这提醒我们双均线策略的收益率分布极其不均匀可能80%的利润来自20%的交易。如果想把策略做得更扎实有几个常见的改进方向加趋势过滤比如只在大周期均线方向为多头时做多或者用ADX指标判断行情是否有趋势震荡市中直接空仓。加入出场策略把简单死叉出场改成移动止损或ATR止损保护已有利润。动态仓位管理根据波动率调整仓位波动大时降仓位波动小时加仓位。多周期共振日线级别信号加上周线级别确认减少假信号。这些都是后续案例可以展开的内容也是从“会写双均线”到“会做策略迭代”的必经之路。最后再分享一个我自己的习惯每次跑完回测我会把最优参数组合重新用验证区间跑一遍也就是把数据切分成“训练段”和“验证段”在训练段选参数在验证段看效果。如果某个参数组合只在训练段表现好验证段就打回原形那基本可以断定是过拟合。双均线策略本身不是秘密真正的功夫在于怎么把它放进一个规范的回测流程里科学地选择参数、控制成本、识别风险。这套流程搭好了以后换成任何策略你只需要替换信号生成部分就行。vectorbt最大的价值不是写代码省事而是逼着你想清楚每一步到底在算什么。

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

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

免费获取报价 →
↑