ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

vectorbt双均线策略回测与参数优化实战指南

2026/10/8 3:05:15 拓冰建站 浏览量
vectorbt双均线策略回测与参数优化实战指南 做量化回测的人手里一定得有个顺手的引擎。我最早是用pandas自己写双均线策略的回测一个for循环逐根K线推持仓跑一次要几十秒改个参数又得全部重来效率低到怀疑人生。后来换到vectorbt同样一个双均线策略所有信号用向量化一次性算完几千组参数组合也就一两秒的事。这篇文章就基于“vectorbt-案例学习-2 双均线策略”把从数据获取、指标计算、回测执行到参数优化和避坑的完整流程走一遍代码尽量贴全让你拿去就能改、就能跑。先说清楚这东西适合谁看刚刚接触向量化回测、想把手动循环改成高效写法的人已经在用backtrader但嫌速度太慢的人以及想做双均线参数网格搜索却不知道怎么下手的人。如果你是第一次听说vectorbt也不用担心我会把关键概念用大白话拆开。1. 项目拆解双均线策略和vectorbt的定位1.1 双均线策略到底在做什么双均线策略也称均线交叉策略属于趋势跟踪里最朴素的一种用一条短周期均线和一条长周期均线做比较当短均线上穿长均线时买入持有当短均线下穿长均线时卖出离场。简单理解就是“短期趋势已经压过长期趋势时进场趋势走坏时撤退”。这个策略设计的巧妙之处在于它不预测方向只做跟随。价格上涨会推动短均线快速抬升一旦越过长期均线说明多头力量正在增强反过来价格下跌带动短均线下穿长均线说明空头力量占优。所以它天然适合有明确趋势的市场阶段赚的是趋势行情中一段相对完整的利润。放在震荡行情里均线会反复交叉连续止损就成了家常便饭这是双均线的天然弱点后面我会专门讲怎么规避。从量化开发的角度看这个策略逻辑极其简单信号生成规则清晰正好适合作为vectorbt的入门案例。因为回测框架本身的学习曲线已经够陡了策略就别再搞复杂了。1.2 为什么选vectorbt而不是自己写循环我以前自己写回测循环的时候核心逻辑大概是从第一根K线开始逐个判断要不要买卖每根K线都要更新持仓市值和现金余额还要单独记录交易流水。这种写法在数据量小时没问题但一旦做参数网格搜索比如把短均线参数从5到30、长均线参数从30到120全部跑一遍那就要套好几层循环运行时间随参数数量线性增长改个参数等半天的场景我实在不想再体验。vectorbt给了完全不同的思路把整个价格序列当作一个向量所有信号、持仓、资金曲线都是用向量化的方式一次性算出来。它还借助Numba把核心计算编译成机器码大数据量下不是快几倍而是快几个数量级。再加上它对“参数矩阵”的原生支持可以同时回测几百甚至几千组参数结果直接堆成一张二维表配合热力图就能直观看出哪些参数表现好。我做了个简单的对比表格方便你理解不同方案在你心里的位置方案编程难度单策略速度批量参数搜索可扩展性自制pandas循环低慢极慢弱backtrader中中弱中vectorbt中极快原生支持强如果你只是想跑一次结果backtrader完全够用。但如果你跟我一样喜欢把参数空间整个跑一遍再挑参数vectorbt的优势就是决定性的。它让“大量实验”变成一种舒适的日常操作而不是一种折磨。1.3 案例的目标与边界这个案例做的事情可以总结成一句话用vectorbt跑通双均线策略的完整回测流程再用网格搜索看参数对收益的影响。数据我用的是日线价格策略方向只做多long only交易标的以宽基ETF为例。这不是一份投资建议书实盘运行需要考虑的细节远比这个案例多比如资金管理、涨跌停无法成交、停牌、分红除权等等。这篇的目的是把框架和流程用明白让你后面换任何标的、任何策略都能套用同样的骨架。2. 环境准备与行情数据获取2.1 安装vectorbt及依赖管理vectorbt的安装非常简单pip一行搞定。建议在虚拟环境里装别图省事直接往全局环境里扔这已经是我不止一次踩过的坑了。pip install vectorbtvectorbt依赖numpy、pandas、numba这些核心库。需要注意numba版本兼容性如果你机器上已经有旧版本的numba装新vectorbt可能起冲突遇到问题时可以先执行pip install --upgrade numba pandas numpy把基础库升到比较新的版本。我自己日常用的Python版本是3.10以上建议你也保持在3.9及以上老版本容易碰到各种奇奇怪怪的兼容问题。装完后可以在Python里输入import vectorbt as vbt验证如果能正常导入环境就算通了。2.2 行情数据的获取与清洗双均线策略只需要收盘价就能算指标但回测时要计算次日开盘价成交所以最好把OHLCV全取齐。数据源上国内比较方便的是akshare和baostock这类免费接口国外常用yfinance具体看你拿什么数据方便就用什么。以akshare获取沪深300ETF日线为例代码可以这样写import akshare as ak import pandas as pd df ak.fund_etf_hist_em( symbol510300, perioddaily, start_date20180101, end_date20231231, adjustqfq ) # 统一列名方便后面处理 df df.rename(columns{ 日期: date, 开盘: open, 最高: high, 最低: low, 收盘: close, 成交量: volume }) df[date] pd.to_datetime(df[date]) df df.set_index(date)[[open, high, low, close, volume]] close df[close].astype(float) open_price df[open].astype(float)这里有个关键习惯拿到数据后别急着算指标先检查数据质量。我一般三步走看时间索引是否连续有没有缺失日期看价格列有没有空值、异常值看有没有重复索引。如果数据里有Null值用close close.replace(0, np.nan).dropna()处理一下或者用前向填充补齐但要注意填充逻辑不能引入未来信息。回测的数据干净程度直接决定结果可信度这一步省不了。2.3 用vectorbt计算均线和交叉信号确认数据没问题后就可以用vectorbt计算均线了。计算均线本身用pandas的rolling().mean()就能做但既然要用vectorbt我习惯直接走它封装好的vbt.MA.runimport vectorbt as vbt fast_window 10 slow_window 30 fast_ma vbt.MA.run(close, fast_window) slow_ma vbt.MA.run(close, slow_window)信号生成就是判断两条均线的交叉关系entries fast_ma.ma_crossed_above(slow_ma) exits fast_ma.ma_crossed_below(slow_ma)这里的entries表示金叉那一个bar的布尔值True代表短均线上穿长均线是买入信号exits表示死叉那一个bar的布尔值是卖出信号。仅从指标计算这个环节看代码量已经比pandas少了不少而且vbt.MA.run内部还会自动处理滚动窗口前期的NaN问题不用我们额外操心。3. 回测核心逻辑与信号处理3.1 防止未来函数信号必须往后挪一根K线信号出来之后不能直接拿去回测这是我最想强调的一个点。双均线交叉的计算依赖于收盘价也就是说一根K线的收盘价确定之后我们才知道当天金叉了。而实际交易中你最快也只能在这根K线结束之后也就是下一根K线开盘时才能下单。如果直接让vectorbt在信号出现的当天收盘价上成交就等于你提前偷看了当天收盘数据这在回测里叫未来函数会让结果虚高到失真。更严谨的做法是把信号往后挪一根K线然后以第二天的开盘价成交。entries entries.vbt.shift(1).fillna(False) exits exits.vbt.shift(1).fillna(False)这样处理后今天收盘出现金叉明天开盘才买入完全符合实际交易的时点。你可能会问能不能直接不shift用next_open之类的方式vectorbt的from_signals确实有一种写法可以指定成交时机但shift 开盘价这个组合是最直观、最不容易出错的我绝大多数情况下都用它。3.2 用Portfolio.from_signals执行一次完整回测信号准备好后核心回测就是一行代码pf vbt.Portfolio.from_signals( close, entries, exits, priceopen_price, # 以开盘价成交 init_cash100000, fees0.0003, slippage0.001, directionlongonly, freqD )我简单拆解一下每个参数的含义close收盘价序列用来计算净值曲线和持仓盈亏entries、exits买入和卖出信号必须是布尔值序列price实际成交参考价格这里用次日开盘价init_cash初始资金10万fees手续费率万三左右在A股算常见水平想更严苛可以设到万五slippage滑点千分之一是相对保守的假设实际按品种流动性来微调directionlongonly表示只做多不参与做空freq数据频率是日线vectorbt需要用它来做收益和夏普等指标的年化换算。from_signals内部生成持仓记录然后模拟每笔资金的进出最终得到完整的资金曲线和交易流水。跑完回测所有指标都在pf.stats()里print(pf.stats())输出会包含总收益率、基准收益率、最大回撤、夏普比率、胜率、交易次数等一大堆指标。第一次看的人容易一头雾水我的建议是先关注下面这几个Total Return整个回测区间的累计收益率Max Drawdown资金从峰值到谷底的最大回撤直观衡量你心理承受能力Sharpe Ratio单位风险获得的超额收益越高越稳Total Trades交易次数用来判断策略是不是过于频繁Win Rate胜率和盈亏比一起看别单独看胜率。3.3 双均线回测结果怎么看我在演示数据上跑了一遍双均线10/30结果大致会出现一个典型形态交易次数不少胜率不到一半但盈亏比尚可整体收益可能跑不赢一直持有但回撤会相对平滑一些。为什么会出现这个结果因为双均线在震荡期连续触发交叉每次都小亏等趋势来了才赚一波大的。胜率低、盈亏比高正是趋势跟踪策略的典型画像。如果你发现自己的双均线回测胜率超过六成反而要警惕通常意味着信号处理出现了未来函数或者手续费设得过于宽松。做回测分析时对比“策略收益”和“基准收益”比如一直持有该ETF也很关键这样你才能判断这套择时规则到底有没有创造超额价值。3.4 可视化收益曲线和回撤一眼看懂回测指标只能给数字要判断策略是否值得信任图更直观。vectorbt内置了可视化方法pf.plot().show()这会画出一张包含资金曲线、回撤区间和交易买卖点的综合图。如果不想用内置绘图也可以直接看净值序列import matplotlib.pyplot as plt portfolio_value pf.value() benchmark_value close / close.iloc[0] * 100000 plt.figure(figsize(12, 6)) plt.plot(portfolio_value, label双均线策略净值) plt.plot(benchmark_value, label买入持有净值) plt.legend() plt.show()把策略净值跟买入持有净值放一起比较能比较直观地看到双均线在上涨趋势里跟得上收益在震荡市里有没有躲过回撤。比如2018年那种单边下跌行情双均线策略大概率会在趋势转空后空仓躲过一部分跌幅这就是择时的价值。4. 参数网格搜索与稳健性验证4.1 为什么要做参数优化双均线的表现对短、长均线参数很敏感。5/20、10/30、20/60在同样的数据上收益率可能差异很大。这不是双均线特有的问题所有带参数的策略都这样。所以找到一个适合目标品种和行情环境的参数区间本身就是策略开发工作的一部分。但我要重点提醒你参数优化不是把历史数据上收益最高的那组参数找出来就完事了。那种“盯着结果调参”的搞法本质上是在拟合历史噪音换个时间段就会失效。正确的做法是在全样本里先划出一段“训练期”用来选参数再用另选一段“验证期”检验选出来的参数是否仍然有效。如果验证期效果显著变差说明参数过拟合了这个策略逻辑可能本身不够稳。4.2 用网格搜索一次运行成千上万组参数下面这套网格搜索写法是vectorbt最爽的地方。我们先把短均线参数和长均线参数分别定义成范围用笛卡尔积生成全部组合然后循环生成所有信号矩阵最后构造一个二维的entries/exits矩阵一次性交给Portfolio.from_signals回测。你不需要手动跑几千个循环vectorbt会把所有组合当做一个批量任务来处理。from itertools import product fast_range [5, 10, 15, 20, 30] slow_range [30, 40, 60, 90, 120] param_names [] entries_matrix [] exits_matrix [] for fast_window, slow_window in product(fast_range, slow_range): if fast_window slow_window: continue fast_ma vbt.MA.run(close, fast_window) slow_ma vbt.MA.run(close, slow_window) entries fast_ma.ma_crossed_above(slow_ma).vbt.shift(1).fillna(False) exits fast_ma.ma_crossed_below(slow_ma).vbt.shift(1).fillna(False) entries_matrix.append(entries) exits_matrix.append(exits) param_names.append(f{fast_window}_{slow_window}) # 拼接成二维信号矩阵 entries_matrix pd.concat(entries_matrix, axis1) exits_matrix pd.concat(exits_matrix, axis1) entries_matrix.columns param_names exits_matrix.columns param_names # 批量回测 pf_matrix vbt.Portfolio.from_signals( close, entries_matrix, exits_matrix, priceopen_price, init_cash100000, fees0.0003, slippage0.001, directionlongonly, freqD ) # 所有参数组合的回测指标 stats_df pf_matrix.stats(dfTrue)stats(dfTrue)会返回一个DataFrame行是指标名称列是每组参数。比如想看所有参数组合的总收益和最大回撤print(stats_df.loc[[Total Return, Max Drawdown, Total Trades]])如果你只想快速挑出总收益最高的一组参数可以用best_col pf_matrix.total_return().idxmax() print(收益最佳参数组合, best_col)4.3 用热力图寻找“参数平原”网格搜索之后别急着选最大值。更实用的方法是把所有参数组合的结果画成热力图观察整个参数空间的形态。如果最优参数周边一圈参数表现都很好说明这组参数是稳健的属于“参数平原”如果只有一个孤零零的尖峰旁边全都表现很差那就明显是过拟合换个时间段大概率翻车。import numpy as np heatmap_data stats_df.loc[Total Return].astype(float) heatmap_pivot heatmap_data.reset_index() heatmap_pivot.columns [fast, slow, total_return] # 转换成行列对应的矩阵 heatmap_pivot heatmap_pivot.pivot(indexslow, columnsfast, valuestotal_return) plt.figure(figsize(10, 8)) plt.imshow(heatmap_pivot.values, cmapRdYlGn, aspectauto) plt.colorbar(labelTotal Return) plt.xticks(range(len(heatmap_pivot.columns)), heatmap_pivot.columns) plt.yticks(range(len(heatmap_pivot.index)), heatmap_pivot.index) plt.xlabel(Fast Window) plt.ylabel(Slow Window) plt.title(双均线参数组合收益热力图) plt.show()我个人的习惯是优先选择热力图中“颜色较好且范围较大”的那块区域比如10/60、15/60、10/90表现都不错那我会从中挑一个作为备选参数。我不会只用收益最高的那一格因为那往往是噪声。4.4 样本外验证让参数接受时间洗礼参数初步选出来后必须做样本外验证。最简单的方式是把历史数据一分为二比如前70%作为训练期后30%作为验证期。在训练期做网格搜索选出最优参数或者最优参数区间固定参数后到验证期再跑一遍。如果验证期收益和回撤都还在可接受范围内这组参数才有一点实战价值。这里有一条经验规律长期均线参数越大的策略换手率越低受手续费影响越小参数稍微偏移一点也不会让结果崩掉因此往往更稳健。如果你在训练期选出来的最优参数是5/30这种短期组合在验证期里效果突然变脸不要惊讶这是常见现象。短周期均线对噪声更敏感天然更容易过拟合。5. 常见问题与实战避坑5.1 未来函数信号产生当天成交的陷阱这个坑我见过不止一次。很多人用vectorbt写双均线直接拿crossed_above的结果传给from_signals又不指定price结果默认用收盘价在信号当根K线成交。这就是典型未来函数。虽然省了一行shift但实测下来收益曲线会好看到失真尤其双均线这种频繁交叉的策略虚高的部分会让你错误判断策略的真实水平。正确的做法我现在固定成一条规矩所有基于收盘价计算的信号都做一次shift(1)然后以次日开盘价成交。就算你懒得用priceopen_price至少也要shift(1)后再用close成交虽然有瑕疵但比不处理强太多。5.2 手续费和滑点设太乐观回测白做双均线策略属于中高频交易一年交叉几十次很正常每一次买卖都要支付手续费和滑点。如果你把fees设成0或者设成万分之一这种不切实际的水平回测里的优势会显著放大但实盘根本实现不了。我在文章里用的是手续费万三加滑点千分之一不算特别严格但对日线级别的双均线来说已经具备参考价值。你可以自己跑一组对比实验把fees分别设为0、万三、千一看总收益和交易次数之间的敏感度这会让你对手续费威力有深刻理解。5.3 NaN数据造成信号错位计算长周期均线时前N根K线天然没有均值crossover的结果前期是False。用concat拼矩阵时如果不注意索引对齐NaN和False混在一起会导致信号位置偏移。解决办法是每次生成entries/exits后用.fillna(False)把NaN清洗掉。另外如果数据本身有停牌或日期缺失回测出来的持仓周期是错乱的。处理停牌最简单的思路是把停牌日的价格填充为前值然后设置禁用交易但vectorbt的Portfolio本身不直接管停牌需要你自己在信号或价格层面做预处理。5.4 vectorbt在大规模搜索时的内存和性能优化网格搜索参数组合很多时一个常见的瓶颈是内存。比如你把ohlc全量数据都保留再复制几千份信号矩阵内存容易直接打满。应对方式有几个数据列只保留close和open别把高开低收全带着跑批量回测价格数据统一转到float32能省一半内存先用大步长粗筛参数空间锁定潜力区域后再在小范围细调。不要在起步阶段就上几十万的组合数。如果搜索量极大还可以用vbt.Portfolio.from_signals配合分批处理跑完一批保存一批避免一次性全部载入内存。5.5 双均线策略的天然弱点和改进思路最后说点策略层面的心得。双均线的逻辑决定了它在单边趋势行情中表现很好但在震荡市里反复磨损是逃不掉的。几个常见的改进方向供你参考增加趋势过滤条件比如只在大周期均线方向向上时才开多仓过滤掉部分横盘震荡信号引入ATR动态止损让每笔交易的亏损可控把固定参数改成自适应参数比如用波动率调整均线周期叠加一个行情状态识别器先用波动率或ADX判断是不是适合做趋势的环境不适合就空仓。这些改进会明显拉高策略复杂度但作为vectorbt练习来说都是很好的下一步方向。你完全可以在学完今天的案例后自己尝试在其中一种思路上做扩展再把结果比较一下。6. 结尾一点实操体会这套流程我用在很多策略验证上最大的感受是回测框架本身不是秘密关键是你怎么把信号处理、成本假设和参数验证做扎实。vectorbt的价值在于把“反复试参数”的成本降到极低但这也意味着你要更克制不然很容易陷入过度优化的陷阱。我自己的习惯是每跑完一轮网格搜索都强制自己去看热力图上最优参数周围的“平原”有多大只选那些周围一片都还不错的参数而不是单点最高值。双均线策略虽然简单但用好了它就是一个很实用的入门框架。你可以先按这篇文章的代码跑通再慢慢替换成自己的标的、自己的参数范围体验一遍完整的向量化回测流程。这套流程一旦熟练了后面换任何策略都能快速上手。