功能

投资组合优化

用自己的函数或优化器库配置资产,并回测再平衡

如何随时间给资产分配权重,再平衡成本是多少?VBT 在 Python 中按任意计划运行投资组合优化:编写自己的配置函数,或调用 PyPortfolioOpt、Riskfolio-Lib、Universal Portfolios,再对权重进行包含手续费的回测。

Backtest monthly max Sharpe, inverse volatility, and equal weight on 10 ETFs
symbols = ["SPY", "QQQ", "IWM", "EFA", "EEM", "TLT", "IEF", "GLD", "VNQ", "DBC"]
data = vbt.YFData.pull(symbols, start="2016-01-01", end="2025-01-01")

def inverse_vol(returns):
    inv = 1 / returns.std()
    return inv / inv.sum()

wrapper = data.symbol_wrapper
returns = vbt.RepEval("data.returns.iloc[index_slice]", context=dict(data=data))
schedule = dict(every="M", lookback_period="365D")  
optimizers = {
    "Equal weight": vbt.PFO.from_uniform(wrapper, every="M", start="2017"),
    "Inverse volatility": vbt.PFO.from_optimize_func(
        wrapper, inverse_vol, returns, **schedule
    ),
    "Max Sharpe": vbt.PFO.from_pypfopt(
        prices=data.close, target="max_sharpe", **schedule
    ),
}
pfs = {
    name: pfo.simulate(data, init_cash=10_000, fees=0.001).loc["2017":]  
    for name, pfo in optimizers.items()
}
metrics = ["total_return", "sharpe_ratio", "max_dd", "total_fees_paid"]
stats = {name: pf.stats(metrics) for name, pf in pfs.items()}
pd.DataFrame(stats).astype(float).round(2)
                  Equal weight  Inverse volatility  Max Sharpe
Total Return [%]         85.49               60.56       58.00
Sharpe Ratio              0.85                0.79        0.59
Max Drawdown [%]         23.17               21.80       28.11
Total Fees Paid          46.63               53.14      479.83
2017 至 2024 年十只 ETF 的等权重、波动率倒数和最大夏普配置的投资组合价值 图表数据(JSON)

最大夏普优化器每次选择过去一年夏普比率最高的权重,却在之后月份表现最差:夏普最低、回撤最深、手续费为十倍,因为权重在少数资产间跳动。最简单的等权重配置表现最好。这类结果说明为何每个优化器旁都应有基线。

回测自己的权重

已有模型、电子表格或其他优化器生成的配置?以 pandas DataFrame 传入,资产为列,再平衡日期为索引。VBT 将这些目标转为投资组合模拟,让您检查交易、手续费和绩效。

此示例使用虚构价格和预先给定的两次权重变化。组合最初持有 60% 的 A 和 40% 的 B,在 1 月 3 日切换为 30% 和 70%。

Backtest a supplied weight table
close = pd.DataFrame(
    {"A": [100, 120, 110, 121], "B": [100, 100, 110, 110]},
    index=pd.date_range("2025-01-01", periods=4),
)
weights = pd.DataFrame(
    [[0.6, 0.4], [0.3, 0.7]],
    index=close.index[[0, 2]],
    columns=close.columns,
)
pfo = vbt.PFO.from_allocations(close.vbt.wrapper, weights)
portfolio = pfo.simulate(close, init_cash=1000, fees=0.001, freq="1D")
pfo.allocations
              A    B
2025-01-01  0.6  0.4
2025-01-03  0.3  0.7
portfolio.get_allocations().round(3)
                A      B
2025-01-01  0.600  0.400
2025-01-02  0.642  0.358
2025-01-03  0.300  0.700
2025-01-04  0.321  0.679
round(portfolio.orders.fees.sum(), 2)
1.66

目标权重仅在指定的两个日期改变。其间实际权重随价格漂移,模拟记录手续费为 1.66。一次买入后持有的篮子,可用 from_initial 和一组权重;需要持续回到固定权重,则用 from_allocations 和再平衡计划。

配置与优化函数

VBT 将何时再平衡与如何分配权重分开:

构造器权重来源
vbt.PFO.from_allocate_func在每个再平衡日调用您的函数
vbt.PFO.from_optimize_func在每个日期之前的历史窗口上调用您的函数
vbt.PFO.from_uniform、vbt.PFO.from_random等权重或固定随机种子的随机权重
vbt.PFO.from_initial、vbt.PFO.from_allocations您提供的固定权重,一次应用或按计划应用
vbt.PFO.from_filled_allocations预先计算的逐 K 线权重表,每行变化时再平衡

优化函数接收您传入的任意参数,index_slice、index_start、index_end 等模板会填入当前窗口信息。它为每个资产返回一个权重,因此波动率目标、风险平价或按预测比例分配权重,都只需几行 pandas。lookback_period 设置窗口历史长度,独立于调度计划;alloc_wait 控制窗口结束多少根 K 线后新权重生效。用 vbt.Param 封装任意参数即可一次测试多个值;Numba 函数配合 jitted_loop=True 可快速运行数千个窗口。

函数也可在每个窗口内筛选资产池,例如计算剩余权重前排除历史或成交量不足的资产。请为每个资产返回权重,要排除的资产返回零。

每个窗口从没有此前历史的状态开始,因此在函数内部计算滚动指标会损失每个窗口的初始 K 线。应先在完整历史上计算一次指标,再使用 index_slice 切片。

权重是投资组合价值的比例,不必加总为一。未配置部分保留为现金,让波动率目标只需几行代码:

Scale equal weights down to target 8% annual volatility
def vol_target(returns, target=0.08):
    vol = returns.mean(axis=1).std() * np.sqrt(252)  
    weight = min(1, target / vol) / returns.shape[1]
    return np.full(returns.shape[1], weight)

pfo = vbt.PFO.from_optimize_func(wrapper, vol_target, returns, **schedule)
pf_target = pfo.simulate(data, init_cash=10_000, fees=0.001).loc["2017":]
compare = {"Equal weight": pfs["Equal weight"], "Vol target 8%": pf_target}
pd.DataFrame({
    name: {
        "invested": pf.get_allocations().sum(axis=1).mean(),  
        "volatility": pf.annualized_volatility,
        "sharpe": pf.sharpe_ratio,
        "max_dd": pf.max_drawdown,
    }
    for name, pf in compare.items()
}).T.round(2)
               invested  volatility  sharpe  max_dd
Equal weight       1.00        0.14    0.85   -0.23
Vol target 8%      0.75        0.11    0.57   -0.23

目标方案约将四分之一组合保留为现金,并降低波动,但由于过去一年滞后于市场,少获得了 8% 的收益。它也损失了三分之一夏普比率,却未减少回撤:新冠暴跌发生在平静年份之后,当时目标方案满仓;随后的恢复阶段反而持有现金。

优化器库

vbt.PFO.from_pypfopt、vbt.PFO.from_riskfolio 和 vbt.PFO.from_universal_algo 按与自定义函数相同的计划运行 PyPortfolioOpt、Riskfolio-Lib 和 Universal Portfolios。各库模型、约束和目标通过关键词参数传入,因此可像其他参数一样参数化。某窗口没有返回权重时,VBT 发出警告并丢弃该窗口,投资组合保持当前持仓至下一日期。上方最大夏普运行中发生过一次。优化器抛出错误会终止运行,除非设置 ignore_errors=True,以同样方式丢弃该窗口。

PyPortfolioOpt 集成也接受自定义目标、权重边界和行业限制。例如,可比较默认配置与限制单一资产或行业权重的配置,并在再平衡成本后衡量差异。库在约束下选择权重,VBT 则评估这些权重随时间产生的交易。

Universal Portfolios 工作方式不同:算法在整个价格历史上运行一次,为每根 K 线返回权重,因此除非通过 on 指定再平衡日期,VBT 会在权重变化时再平衡。该库使用普通 Python 逐 K 线迭代,因此在长历史上远慢于另外两者。

再平衡计划

every 接受周、月、季等 pandas 频率。配置函数还可通过 on 或 index_points 接受准确日期,优化函数可通过 index_ranges 接受准确窗口。测试多种计划可以显示再平衡成本:

Rebalance equal weights weekly, monthly, quarterly, and yearly
uniform = vbt.PFO.from_uniform(
    data.symbol_wrapper, every=vbt.Param(["W", "M", "Q", "Y"]), start="2017"
)
pf = uniform.simulate(data, init_cash=10_000, fees=0.001).loc["2017":]
pd.DataFrame({
    "sharpe": pf.sharpe_ratio,
    "fees": pf.orders.fees.sum(),
    "orders": pf.orders.count(),
}).round(2)
       sharpe   fees  orders
every
W        0.85  87.74    4180
M        0.85  46.63     960
Q        0.88  32.06     320
Y        0.86  20.07      80

从每季改为每周再平衡,手续费几乎增加至三倍,却未提高夏普比率。如果仅在权重漂移超过阈值时再平衡,或根据模拟组合当前状态计算权重,请通过 vbt.PF.from_order_func 将再平衡逻辑写为模拟回调,详见会员专属的动态投资组合优化(英文)教程。

从配置到投资组合

pfo.simulate(data) 将配置历史转为使用共享现金的目标百分比订单,默认使用 vbt.PF.from_orders。传入 pf_method="from_signals" 可添加止损与止盈页面中的止损及其他退出。模拟前也可检查优化器本身:

  • pfo.allocations 保存各再平衡日期的权重,pfo.alloc_records 将每组权重关联到来源数据窗口。
  • pfo.mean_allocation、pfo.stats() 和 pfo.plot() 汇总并绘制权重。
  • rescale_to 将正负权重映射到一个范围,例如多空组合的 -1 至 1;pf.get_allocations() 显示实际权重在再平衡日间如何漂移。
  • pfo.replace(allocations=...) 替换为编辑后的权重;vbt.PFO.column_stack 将在不同列上分别构建的优化器(例如每策略一个)组合为一次模拟。

比较与组合配置方法

配置历史在模拟前已可访问,因此可平均两个模型的权重或应用自己的调整,再回测修改后的目标。组合前请匹配资产和再平衡日期。这适合在相同手续费和计划下,将各模型与混合配置比较。

比较回看期时,请像开头示例一样,在所有方法都有足够历史后,使用共同区间评估。检查订单,了解目标实际交易频率,并在收益之外比较回撤和手续费。前向滚动与交叉验证页面展示如何跨时间重复选择与评估。

新权重在窗口结束 alloc_wait 根 K 线后生效,默认为一根。因此,结束于数据最后一根 K 线的窗口没有位置放置权重,VBT 会发出警告。实盘使用时,如需获取下一笔交易的权重,可传入 alloc_wait=0,让权重落在最后一根 K 线上。

教程

会员专属的投资组合优化(英文)教程介绍配置与优化函数、索引点和区间、库集成以及动态再平衡。

Riskfolio-Lib

✅ Riskfolio-Lib 是另一个日益流行、已集成到 VBT 的投资组合优化库。集成将其典型流程自动化并放入单个函数,让许多优化问题可用一组关键词参数表达,并轻松参数化。

Run hierarchical risk parity (HRP) with Riskfolio-Lib on a monthly basis
data = vbt.YFData.pull(
    ["SPY", "TLT", "XLF", "XLE", "XLU", "XLK", "XLB", "XLP", "XLY", "XLI", "XLV"],
    start="2020",
    end="2023",
    missing_index="drop"
)
pfo = vbt.PFO.from_riskfolio(
    returns=data.close.vbt.to_returns(),
    port_cls="hc",
    every="M"
)
pfo.plot().show()
2020 至 2022 年 Riskfolio-Lib 层次风险平价的月度资产权重 图表数据(JSON)

教程

更多内容请参阅投资组合优化教程。

投资组合优化

✅ 投资组合优化是构建资产组合以追求收益最大化、风险最小化的过程。通常定期执行,生成新权重来对现有组合再平衡。与 VBT 大多数功能一样,用户将权重生成步骤实现为回调,由优化器定期调用。最终结果是返回的配置权重集合,可分析、可视化并用于实际模拟 🥧

Allocate assets inversely to their total return in the last month
def regime_change_optimize_func(data):
    returns = data.returns
    total_return = returns.vbt.returns.total()
    weights = data.symbol_wrapper.fill_reduced(0)
    pos_mask = total_return > 0
    if pos_mask.any():
        weights[pos_mask] = total_return[pos_mask] / total_return.abs().sum()
    neg_mask = total_return < 0
    if neg_mask.any():
        weights[neg_mask] = total_return[neg_mask] / total_return.abs().sum()
    return -1 * weights

data = vbt.YFData.pull(
    ["SPY", "TLT", "XLF", "XLE", "XLU", "XLK", "XLB", "XLP", "XLY", "XLI", "XLV"],
    start="2020",
    end="2023",
    missing_index="drop"
)
pfo = vbt.PFO.from_optimize_func(
    data.symbol_wrapper,
    regime_change_optimize_func,
    vbt.RepEval("data[index_slice]", context=dict(data=data)),
    every="M"
)
pfo.plot().show()
2020 至 2022 年十一种资产的月度市场状态切换投资组合配置 图表数据(JSON)

教程

更多内容请参阅投资组合优化教程。

PyPortfolioOpt

✅ PyPortfolioOpt 是流行的金融投资组合优化包,涵盖经典方法(Markowitz 1952 与 Black-Litterman)、建议的最佳实践(例如协方差收缩),以及许多近期发展和新功能,例如 L2 正则化、收缩协方差和层次风险平价。

Run hierarchical risk parity (HRP) with PyPortfolioOpt on a monthly basis
data = vbt.YFData.pull(
    ["SPY", "TLT", "XLF", "XLE", "XLU", "XLK", "XLB", "XLP", "XLY", "XLI", "XLV"],
    start="2020",
    end="2023",
    missing_index="drop"
)
pfo = vbt.PFO.from_pypfopt(
    returns=data.returns,
    optimizer="hrp",
    target="optimize",
    every="M"
)
pfo.plot().show()
2020 至 2022 年 PyPortfolioOpt 层次风险平价的月度资产权重 图表数据(JSON)

教程

更多内容请参阅投资组合优化教程。

Universal Portfolios

✅ Universal Portfolios 汇集多种在线投资组合选择(OLPS)算法。

Simulate an online minimum-variance portfolio on a weekly timeframe
data = vbt.YFData.pull(
    ["SPY", "TLT", "XLF", "XLE", "XLU", "XLK", "XLB", "XLP", "XLY", "XLI", "XLV"],
    start="2020",
    end="2023",
    missing_index="drop"
)
pfo = vbt.PFO.from_universal_algo(
    "MPT",
    data.resample("W").close,
    window=52,
    min_history=4,
    mu_estimator='historical',
    cov_estimator='empirical',
    method='mpt',
    q=0
)
pfo.plot().show()
2020 至 2022 年十一种资产的周度在线最小方差投资组合权重 图表数据(JSON)

教程

更多内容请参阅投资组合优化教程。

版权所有 © 2021–2026 Oleg Polakow。保留所有权利。

本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。