# 投资组合优化 (/zh/features/optimization/portfolio-optimization)

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

```python title="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")  # (1)
>>> 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":]  # (2)
...     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
```

1.  每月各优化器查看此前 365 天。第一个窗口在 2017 年 1 月结束，因此等权重组合也从该时点开始。
2.  新权重在每个窗口后第一根 K 线收盘交易，避免使用当时无法看到的价格。从 2017 年开始切片，使三者在相同区间比较。

2017 至 2024 年十只 ETF 的等权重、波动率倒数和最大夏普配置的投资组合价值. [Figure data (JSON)](/assets/figures/features/optimization/allocation-comparison.7195e06f1cfb.json)

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

## 回测自己的权重 \[#backtest-your-own-weights]

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

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

```python title="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` 和再平衡计划。

## 配置与优化函数 \[#allocation-and-optimization-functions]

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` 切片。

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

```python title="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)  # (1)
...     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(),  # (2)
...         "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
```

1.  等权重篮子过去 365 天的年化波动率。
2.  每根 K 线上资产持有占组合价值的比例，再对整个区间取平均。

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

## 优化器库 \[#optimizer-libraries]

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

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

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

## 再平衡计划 \[#rebalancing-schedules]

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

```python title="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`
将再平衡逻辑写为模拟回调，详见会员专属的[动态投资组合优化（英文）](https://members.vectorbt.pro/tutorials/portfolio-optimization/dynamic/)教程。

## 从配置到投资组合 \[#from-allocations-to-a-portfolio]

`pfo.simulate(data)` 将配置历史转为使用共享现金的目标百分比订单，默认使用 `vbt.PF.from_orders`。传入 `pf_method="from_signals"`
可添加[止损与止盈](/zh/features/backtesting/stop-loss-and-take-profit/)页面中的止损及其他退出。模拟前也可检查优化器本身：

*   `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` 将在不同列上分别构建的优化器（例如每策略一个）组合为一次模拟。

### 比较与组合配置方法 \[#compare-and-combine-allocation-methods]

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

比较回看期时，请像开头示例一样，在所有方法都有足够历史后，使用共同区间评估。检查[订单](/zh/features/backtesting/orders-and-execution/)，了解目标实际交易频率，并在收益之外比较[回撤](/zh/features/analytics/drawdown-analysis/)和手续费。[前向滚动与交叉验证](/zh/features/optimization/time-series-cross-validation/)页面展示如何跨时间重复选择与评估。

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

!!! info "教程"
    会员专属的[投资组合优化（英文）](https://members.vectorbt.pro/tutorials/portfolio-optimization/)教程介绍配置与优化函数、索引点和区间、库集成以及动态再平衡。

## Riskfolio-Lib \[#riskfolio-lib]

新增于 1.7.0

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

```python title="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 层次风险平价的月度资产权重. [Figure data (JSON)](/assets/figures/features/optimization/riskfolio-lib.c10fa4f9b800.json)

!!! info "教程"
    更多内容请参阅[投资组合优化](/zh/tutorials/portfolio-optimization)教程。

## 投资组合优化 \[#portfolio-optimization]

新增于 1.2.0

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

```python title="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 年十一种资产的月度市场状态切换投资组合配置. [Figure data (JSON)](/assets/figures/features/optimization/portfolio-optimization.f2d874e1afd4.json)

!!! info "教程"
    更多内容请参阅[投资组合优化](/zh/tutorials/portfolio-optimization)教程。

## PyPortfolioOpt \[#pyportfolioopt]

新增于 1.2.0

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

```python title="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 层次风险平价的月度资产权重. [Figure data (JSON)](/assets/figures/features/optimization/pyportfolioopt.71817c556f5b.json)

!!! info "教程"
    更多内容请参阅[投资组合优化](/zh/tutorials/portfolio-optimization)教程。

## Universal Portfolios \[#universal-portfolios]

新增于 1.2.0

✅ [Universal Portfolios](https://github.com/Marigold/universal-portfolios) 汇集多种在线投资组合选择（OLPS）算法。

```python title="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 年十一种资产的周度在线最小方差投资组合权重. [Figure data (JSON)](/assets/figures/features/optimization/universal-portfolios.2016998532d6.json)

!!! info "教程"
    更多内容请参阅[投资组合优化](/zh/tutorials/portfolio-optimization)教程。
