vectorbt向量化回测实战:5000次多元化投资组合模拟与调优

发布时间:2026/10/8 4:11:52
vectorbt向量化回测实战:5000次多元化投资组合模拟与调优 做资产配置、模拟组合的朋友多少都遇到过这种尴尬想法很简单——拿五六个低相关的品种搭一个多元化投资组合跑一跑历史数据看看回撤扛不扛得住、夏普高不高。可一到动手就露怯用pandas一层层for循环去刷权重组合5000次模拟跑下来电脑风扇像飞机起飞等结果等到怀疑人生。后来换用vectorbt这套基于NumPy和Numba的向量化回测框架同样5000次组合模拟耗时从小时级压到分钟级直接刷新了我对回测效率的认知。这篇就把我用vectorbt模拟多元化投资组合的完整过程写下来包括方案设计、数据清洗、核心代码、参数调优和踩坑实录给想用向量化方式做资产配置试验的朋友当个参考。1. 项目概述与方案选型1.1 这个模拟到底在做什么先把这个案例的本质说明白。模拟多元化投资组合核心就一句话给定一组资产让它们的权重自由组合观察在不同权重分配下组合整体会呈现出什么样的风险收益特征。这里说的“多元化”不是简单地把钱分成几份买不同的东西而是基于一个底层逻辑——资产之间的相关性越低组合的波动越可能被对冲掉也就是所谓的“免费午餐”。我们这个模拟项目里的具体做法是选定五六个ETF作为资产池假想一笔初始资金按照若干种权重分配方式去构建组合。权重怎么来两条腿走路。第一条腿是固定策略比如等权重、风险平价这种有明确规则的方案第二条腿是蒙特卡洛随机权重随机生成几千组权重向量每组合成一个组合然后批量回测看所有这些组合在历史数据上的表现分布。最后再用夏普比率、最大回撤、年化波动这些指标去筛找出哪些权重分配在历史上相对更优。这个项目最适合谁参考一类是想做个人资产配置、但对量化工具不熟的投资爱好者另一类是已经会用pandas做简单回测、但被性能卡住、想升级到向量化思路的开发者。它的门槛其实不高懂一点Python、知道ETF是什么概念基本上就能跟着流程把模拟跑起来。它能解决的痛点是过去需要手写循环、一个个组合去试的方案现在可以批量、系统化地在几分钟内完成。1.2 为什么选vectorbt而不是pandas手撸聊到组合模拟绝大多数人第一反应是用pandas手算拉价格、算收益、写循环去调权重最后再手搓一个回撤计算函数。这个思路本身没错问题在于效率。一个3年周期、5个资产的组合如果做5000次随机权重模拟每次模拟都要遍历上千个交易日pandas的循环写法在性能和代码复杂度两个维度上都会被拖垮。vectorbt解决的就是这个问题。它底层用NumPy的广播机制配合Numba做即时编译把原本需要逐行循环的计算变成矩阵运算。你可以把一个三维张量直接丢进去第一维是模拟次数第二维是资产数量第三维是交易日——然后向量化地算出5000个组合在几千个交易日里的逐日权益、收益曲线、回撤序列。这种代码跑起来不是快了一点半点而是数量级的差距。我自己在实际对比中试过同样的数据、同样的组合逻辑pandas循环写5000次模拟大概需要几十分钟到一个小时换成vectorbt批量计算加上Numba首次编译的热身时间全程基本在几分钟内完成。如果只算纯计算时间差的会更多。当然vectorbt也有代价就是它的API风格和常规pandas习惯不太一样初次接触会有个适应期。但一旦理解了“一切皆张量”的思路写起来会比pandas还顺手。下面用一张表直观对比一下三种方案对比维度pandas手写循环PyPortfolioOpt等传统库vectorbt批量模拟计算方式Python逐层循环部分向量化全链路张量运算5000组组合耗时小时级中等分钟级含编译组合指标获取手写函数自带部分指标内置完整指标体系自定义再平衡代码复杂不太灵活参数直接控制学习曲线平缓中等前期略陡我的建议是如果你只是想算一个组合的最优权重用传统优化库就够了但如果你要做大量“如果这样配比会怎样”的探索性模拟vectorbt这种批量处理能力的价值就彻底体现出来了。我们这个项目正好属于后者。2. 数据准备与模拟框架设计2.1 数据从哪来怎么处理干净数据是一切模拟的地基。这个案例里我用的资产池是五只流动性较好的ETFSPY标普500、QQQ纳指100、TLT美国长期国债、GLD黄金、IWM罗素2000小盘股。选它们的理由很直白——覆盖了股票的不同风格同时加入了债券和商品资产之间的相关性有明显分层非常适合用来演示多元化逻辑。数据源方面我这次直接用yfinance拉日线收盘价。选它的原因没有多高深免费、维护成本低、对历史行情回溯足够。如果你在国内网络环境拉取这些数据不稳定也可以换成其他数据接口只要最后拿到的是标准DataFrame格式就没问题。时间范围我取的是2015年到2024年这十年横跨牛熊和多次市场剧烈波动组合表现的数据会比较有说服力。数据拿到手之后最关键的步骤是对齐和清洗。不同ETF的上市时间、分红时间、交易日历不完全一样直接拼在一起会出现大量NaN。我的处理方式很简单先取所有品种的日线收盘价合成一个宽表然后用dropna()把某一天有品种缺数据的行删掉。这样做的代价是损失少量交易日但换来的是一个完全对齐的矩阵后面算收益率、算协方差都干净利落。import yfinance as yf import pandas as pd import numpy as np tickers [SPY, QQQ, TLT, GLD, IWM] prices yf.download(tickers, start2015-01-01, end2024-12-31)[Close] prices prices.dropna() returns prices.pct_change().dropna()这里有个细节值得多说一句我存了两份数据一份是原始价格prices一份是简单收益率returns。为什么两份都要因为组合回测有时候要基于价格做复利计算有时候又要基于收益率做近似加权两种方式各有使用场景后面我会专门讲它们可能导致的差异。2.2 资产池怎么选低相关是多元化的灵魂构建组合之前先做一道“体检”——算资产之间的相关性。这个步骤很多人会跳过但它的意义非常大。多元化组合的本质不是“买的品种多”而是“品种之间最好不要同涨同跌”。如果五只ETF其实都在同一类因子上暴露那它们的相关性会非常高组合看起来很多元遇到市场下跌时仍然会一起跌对冲效果为零。算相关性的代码极其简单一行returns.corr()就能得到相关矩阵。我拿到的结果大致是这样的逻辑SPY和QQQ的相关性很高因为它们都属美股大盘科技权重股IWM和它们的相关性中等而TLT、GLD和其他股票资产的相关性明显偏低有的甚至接近零或负值。这就是整个模拟项目的基石——有了低相关的资产对才谈得上构建真正的多元化组合。回到框架设计层面资产池的筛选逻辑可以总结成三个检查点第一资产覆盖要足够分散股票、债券、商品至少跨两类第二两两相关性矩阵里要有一批数值低于0.5甚至更低的组合第三每只资产都应当有足够长的历史数据至少经历过一轮完整市场周期。用这三条去卡基本就能排除掉那些貌似多元、实则同质化的资产池。2.3 权重生成逻辑与业绩指标权重生成是整个模拟的“引擎”。我这次用了三种方式第一种是固定规则权重比如等权重每个品种各20%还有风险平价思路——按每类资产的波动率倒数分配权重波动大的少配波动小的多配第二种是随机权重用蒙特卡洛方式生成几千组随机权重向量第三种是在随机权重的基础上加约束后面详述。这里重点说一下随机权重的生成细节。如果直接对每只ETF随机生成一个0到1之间的数然后归一化这种方法其实有偏差因为它会让每个品种的权重分布不完全均匀。更合适的是用Dirichlet分布这个名词听起来唬人实际上可以理解为一个“保证一组数都大于0且和为1的随机数生成器”。在Python里就是一行n_assets len(tickers) n_sim 5000 weights np.random.dirichlet(np.ones(n_assets), sizen_sim)这里的np.ones(n_assets)表示每个资产在随机抽样中地位相等生成出来的5000组权重向量会自动满足“所有权重都非负且加起来等于1”的约束。这比先生成随机数再手动归一化要干净、均匀得多。业绩指标的选择上我重点关注五个年化收益率、年化波动率、夏普比率、最大回撤、Calmar比率年化收益除以最大回撤。这些指标组合在一起基本能回答一个投资者最关心的两个问题——赚得够不够多、亏的时候疼不疼。指标太多反而干扰判断我一直主张宁精勿滥。3. 核心实现组合模拟的完整代码与参数解读3.1 环境准备和最小骨架动手之前先搭环境。vectorbt的安装很简单一条pip install vectorbt搞定但它依赖的Numba会要求本机有合适的编译器通常Windows和主流Linux环境都没问题Mac偶尔会遇到编译器版本匹配问题遇到的话按提示装一下对应版本的Xcode命令行工具就行。导入vectorbt后先建立最小骨架——把价格数据准备好再做一次单组合的快速验证。我用的是等权重组合每个品种20%每月再平衡一次。为什么每月再平衡因为太频繁比如每日会放大交易成本和摩擦损耗太宽松比如一年一次又会让权重偏离目标越来越远。月频是组合管理里一个比较均衡的选择。核心代码结构我写成这样import vectorbt as vbt # 构造每月最后一天再平衡的等权重目标矩阵 target_weights np.full((prices.shape[0], len(tickers)), 0.2) pf_bench vbt.Portfolio.from_weights( prices, weightstarget_weights, init_cash100000, fees0.001, rebalance_freq1M, )这段代码看起来不长但在vectorbt内部做了一整套事情它按照目标权重把初始资金分配到五只ETF上然后到每月月底按照最新价格重新计算持仓有偏离就交易交易时扣掉千分之一的手续费最后生成完整的逐日持仓和资金曲线。取指标也是一行一个print(pf_bench.sharpe_ratio(freqD)) print(pf_bench.max_drawdown()) print(pf_bench.annualized_return())首次调用from_weights时会触发Numba编译耐心等十几秒或者几十秒后面再跑同类模拟就快了。这一步验证通过就等于把整个框架跑通了后面升级到批量模拟才会真正感受到vectorbt的威力。3.2 批量模拟5000个组合等权组合跑通之后重头戏来了——把5000组随机权重一次性丢进去做批量模拟。这一部分的核心思路是把“单组合生成的静态目标权重矩阵”升级成“多组权重拼接成三维张量”。具体来说from_weights的weights参数支持三维数组第一维是模拟编号第二维是时间序列第三维是资产。我们这里每组模拟都用相同的再平衡频率但各自的权重基准不同。实现起来并不复杂关键是把5000组随机权重广播到时间轴上# 把每组的权重矩阵广播到与价格时间轴等长 target_weights_3d np.repeat(weights[:, None, :], len(prices), axis1) pf_mc vbt.Portfolio.from_weights( prices, weightstarget_weights_3d, init_cash100000, fees0.001, rebalance_freq1M, )这里weights的形状是(5000, 5)np.repeat之后变成(5000, 2518, 5)也就是5000个组合分别沿时间轴展开。内存上要注意这个数组大概占5000乘以2518乘以5个float64约500MB属于可以接受的范围。批量回测完成后vectorbt返回的是一个组合对象集合可以一次性提取所有模拟的性能指标。比如sharpe_all pf_mc.sharpe_ratio(freqD) dd_all pf_mc.max_drawdown() ret_all pf_mc.annualized_return()现在你手里就有5000组组合的夏普、回撤、年化收益数组了。筛选逻辑非常直观比如找到夏普最高的那组权重用np.argmax(sharpe_all)拿到索引再到weights数组里把对应那组权重取出来。我跑出来的历史最优组合往往不是某个极端全配而是股票和债券、商品之间相对均衡的一组权重这一点其实很符合多元化的直觉。3.3 结果解读怎么从几千个组合里筛选拿到了几千组指标之后下一步不是直接挑最大夏普就完事而是要把结果放到一起看分布再结合个人偏好去筛选。我这里习惯先做两件简单的事第一是画散点图横轴是年化波动率纵轴是年化收益率每一个点代表一个模拟组合第二是对比几类代表性组合的指标。示意数据整理成表格大概是这样组合类型年化收益率年化波动率夏普比率最大回撤等权重组合7.2%12.8%0.55-24%风险平价组合6.1%9.5%0.62-16%历史最高夏普组合8.5%11.2%0.73-19%历史最低波动组合4.8%7.6%0.58-11%上表的数值是我跑出来结果的示意不同时间区间和资产池会不一样但趋势通常是一致的风险平价组合的波动和回撤明显小于纯股票历史最高夏普组合则出现在股票与避险资产配比较均衡的位置。这个规律值得注意——历史收益最高的组合不一定波动最大因为多元化本身就在扣除风险的条件下帮收益率做了优化。筛选之后的权重如果落到某个具体标的上可能不是整数字而是类似“SPY 28%、QQQ 17%、TLT 32%、GLD 15%、IWM 8%”这样挺符合直觉。因为TLT和GLD提供了组合里宝贵的防御属性在股票下跌阶段能把整体回撤拉低。关于潜在的有效前沿这里也提一句大量随机权重组合的散点图会自然形成一条包络线也就是蒙特卡洛方式近似的有效前沿。虽然没有经典优化算法那么精确但足够直观且可以轻易加上任何你想加的约束比如“单品种不超过40%”“波动率不超过12%”这些约束在随机模拟里加起来的成本比解析优化低得多。4. 常见问题与排查实录4.1 数据对齐与NaN的坑组合模拟里最常见的翻车点就是数据对齐。不同ETF可能有不同的节假日和交易时段直接用yf.download拿下来的数据会自带一些NaN。如果不去管这些NaN后面的协方差矩阵、组合收益都会受到影响轻则结果带偏重则直接报shape mismatch。我的处理顺序是先拿原始收盘价然后dropna(axis0)把任何一天有品种缺数的行删掉再算收益率。这样做比较保守会损失一小部分交易日但换来的是后面全过程不会因为数据空洞出幺蛾子。这里要特别提醒一个误区不要为了“保留更多数据”而去用ffill()填充价格缺口。填充后的价格会让对应日期收益率变成0回测里会显示这一天组合没什么波动这其实是虚假信息尤其遇到长期停牌品种会被严重扭曲。我在早期项目里吃过这个亏现在原则很简单宁可删行绝不乱填。4.2 权重归一化与再平衡逻辑随机权重生成后一定要检查归一化。Dirichlet分布生成的数据天然满足和为1但如果你后续自己手写了归一化逻辑要注意一个常见bug直接把原始随机数除以总和这样处理很方便但会在权重数组里引入浮点误差累积。尤其是当权重数量特别多的时候误差会传递到目标权重矩阵最终导致from_weights在某个日期点位上觉得权重“不够数”。再平衡逻辑是另一个容易踩坑的地方。rebalance_freq1M指的是每一个自然月按目标权重调整一次不是每个交易日都调。如果你传进去的权重矩阵是全时段常量vectorbt会从月初到月末维持上期实际持仓、月末调到新目标。这个逻辑本身没问题但如果你自己写循环做再平衡很容易在“目标权重”和“当前持仓权重”之间犯迷糊——我们设定的是目标比例实际持仓因为市场价格每天都在浮动这是两码事。4.3 内存、Numba编译和运行速度性能问题在任何向量化框架里都是绕不开的话题。5000次模拟、2500个交易日、5个资产这个规模听起来很吓人但vectorbt处理起来其实尚可。我估算过一个内存占用核心的价格张量约5000乘2500乘5乘8字节大约是500MB加上中间过程和指标计算整体在2GB以内。如果你的机器内存低于8GB建议把模拟次数降低到2000次或者分批模拟每次丢1000组循环五次最后把指标数组拼起来。Numba的首次编译速度也是一个“伪卡死”高发区。从导入vectorbt到第一次调用from_weights如果没有任何报错提示只是等的时间比想象中长那多半是在编译几十秒到几分钟都正常。不要急着关进程给足编译时间。另外vectorbt对Numba、NumPy版本比较敏感装的时候尽量按官方推荐版本组合来否则可能会遇到版本冲突导致无法import的诡异问题。4.4 价格回测还是收益率回测最后讲一个比较隐蔽的口径坑。同一个组合用prices传给from_weights和用returns传给组合计算结果会不一样。原因是基于收益率计算时组合整体收益近似为各资产收益率的线性加权忽略了每日再平衡的复利细节而基于价格计算时vectorbt会按真实价格动态推导持仓市值更贴近实际交易。简单说如果你追求模拟真实必须用价格数据走from_weights如果你只是快速算一个近似组合收益用收益率矩阵做线性加权也能得到一个八九不离十的结果但别把两种口径的结果混在一起比较指标。我自己现在都会同时留一份价格和收益率数据快速探索用收益率出正式结论和回撤数字时用价格。写在最后的一个朴素建议模拟做了这么多轮我个人的体会是多元化组合的价值不在于给你一个超级漂亮的收益率而在于当你遇到真正的市场下跌时组合回撤可控心态就不会崩反而能拿住仓位等来反弹。vectorbt这个工具让我从“模拟一次半天”的窘境里解放出来可以把精力放在思考权重逻辑、约束条件和结果解读上而不是和计算性能较劲。如果你刚接触这套流程我的建议是先用五六个品种跑通全流程验证一下工具链再考虑扩展到更复杂的资产池或者因子暴露分析。工具再快方案里的假设条件也得自己审清楚别让精确的数字给你一种虚假的确定感。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询