# 多维研究 (/zh/features/tooling/multidimensional-research)

VBT 如何一次测试数千个策略变体，又如何浏览结果？每个资产、参数值和策略都成为带标签的列。输入相互广播，在 NumPy 数组上计算，再以 pandas 对象返回，可像普通 DataFrame
一样选择、分组和堆叠。

```python title="Run 3 assets × 4 fast windows × 4 slow windows as 48 columns"
>>> from itertools import product

>>> data = vbt.YFData.pull(
...     ["BTC-USD", "ETH-USD", "SOL-USD"],
...     start="2022-01-01",
...     end="2025-01-01",
... )
>>> fast_windows, slow_windows = zip(*product([5, 10, 15, 20], [30, 50, 70, 90]))
>>> fast = vbt.MA.run(data.close, window=list(fast_windows), short_name="fast")
>>> slow = vbt.MA.run(data.close, window=list(slow_windows), short_name="slow")
>>> entries = fast.ma_crossed_above(slow)
>>> exits = fast.ma_crossed_below(slow)
>>> entries.shape
(1096, 48)
>>> entries.columns[:4]
MultiIndex([(5, 30, 'BTC-USD'),
            (5, 30, 'ETH-USD'),
            (5, 30, 'SOL-USD'),
            (5, 50, 'BTC-USD')],
           names=['fast_window', 'slow_window', 'symbol'])

>>> pf = vbt.PF.from_signals(data, entries, exits, fees=0.001)  # (1)
>>> pf.total_return.groupby("symbol").describe()[["mean", "min", "max"]].round(3)
          mean    min    max
symbol
BTC-USD  0.758  0.174  1.481
ETH-USD  0.051 -0.195  0.474
SOL-USD  1.394  0.104  4.285

>>> print(round(pf[(10, 50, "ETH-USD")].total_return, 4))  # (2)
0.1389
>>> pf.xs("ETH-USD", level="symbol", axis=1).wrapper.shape  # (3)
(1096, 16)
```

1.  三列价格广播到 48 列信号。每列价格供其对应的 16 组参数配对复用。
2.  按标签选中一个回测。
3.  某个资产的全部 16 组参数配对。

每列都是完整回测，拥有自己的订单、交易和度量；列标签携带全部参数，因此可用普通 pandas 汇总结果。

这样，同一实验可回答多个问题：哪些资产表现稳定、哪些参数范围经得起检验、不同成本假设会使结果变化多少。从输入到最终交易和绩效表，标签始终保留。

## 研究模型 \[#the-research-model]

VBT 对象由 NumPy 数组和封装器组成，封装器记录索引、列、频率与分组。编译代码在原始数组上计算，再按正确标签将结果封装回
pandas，无论结果是时间序列、每列一个值还是每组一个值。Series 和 DataFrame 使用相同代码，因此为单个资产编写的函数也适用于一千个资产。

市场数据的每个特征各有数组：收盘价一张表、最高价另一张表、入场信号又一张表。列标识交易品种和实验。VBT 数据对象可像开篇示例一样直接提供价格特征，也可从自己的数据流程传入独立数组。

## 广播 \[#broadcasting]

不同形状输入通过广播组合：标量应用于全部元素，Series 应用于每列，单行 DataFrame 应用于每根 K 线，完整 DataFrame
则逐元素应用。带标签列按层级匹配，因此上例三列价格可与共享 `symbol` 层级的 48 列信号对齐。小输入保持小规模：模拟内部将标量手续费作为标量读取，不会扩展成完整数组。

一维 NumPy 数组被视为时间序列，因此每根 K 线必须有一个值。逐列数值应放入 `np.array([[0.001, 0.002, 0.003]])`
这样的单行数组，或带标签对象。行按日期匹配：日期不同的输入按全部日期连接，并以 NaN
填充，因此不同时间周期的数据应先重采样到同一索引，参见[多时间周期分析](/zh/features/data/multi-timeframe-analysis/)。

普通 pandas 运算符严格匹配标签，因此不同参数层级的指标输出直接比较会失败。在左操作数前加 `.vbt`，改用 VBT
广播，详见[交易信号](/zh/features/indicators/trading-signals/#combining-conditions)。

匹配价格和信号列，即可每列运行一个回测；也可通过下方 DataFrame 笛卡尔积扩展实验，在比较组合时让每个品种始终对应自己的价格。

下方 [DataFrame 笛卡尔积](#dataframe-product)功能将一个 DataFrame
的每列与另一个的每列组合；[索引字典](#index-dictionaries)则无需先构建数组，即可在选定日期和列设置值。

### 将假设作为带标签情景比较 \[#compare-assumptions-as-labeled-scenarios]

参数不仅能描述指标，也能描述执行假设。这里用两条虚构价格序列，分别测试零手续费和 1% 手续费。各手续费成为一个额外列层级，因此无需从独立运行收集结果，就能将收益重塑为表格：

```python title="Compare two fee assumptions across two assets"
>>> close = pd.DataFrame(
...     {"A": [100.0, 105.0, 110.0], "B": [100.0, 98.0, 95.0]},
...     index=pd.date_range("2025-01-01", periods=3),
... ).rename_axis(columns="symbol")
>>> scenarios = vbt.PF.from_holding(close, fees=vbt.Param([0.0, 0.01], name="fee"))
>>> scenarios.total_return.unstack("symbol").round(4)
symbol       A       B
fee
0.00    0.1000 -0.0500
0.01    0.0891 -0.0594
```

这里是小数形式的总收益率，结束时持仓仍未平仓。手续费减少各回测入场时可以买入的数量。同一方法也能比较滑点或仓位假设。[参数优化](/zh/features/optimization/strategy-optimization/)介绍更大的搜索、条件组合和随机抽样。

## 分组 \[#grouping]

列可为报告或模拟而分组。`group_by` 按层级、标签列表，或用 `True` 将全部列组合成组；`cash_sharing=True`
将一个组变成一个账户。事后设置分组只影响结果报告方式，因此只要各组列相邻，同一批回测即可按资产、参数值或整体读取。

开篇示例中，pandas 汇总按品种将各自 16
个收益分组，以比较参数选择。这与对投资组合本身分组不同，后者可报告多列共同持有的表现。共享资金投资组合参见[投资组合记账](/zh/features/backtesting/portfolio-accounting/)，独立策略之间的配置参见[多策略投资组合](/zh/features/strategies/multi-strategy-portfolios/)。

## 堆叠运行结果 \[#stacking-runs]

独立运行可以合并为一个对象。按列堆叠将不同实验并排，例如在策略网格旁放上买入持有：

```python title="Put buy-and-hold next to the 48 crossover backtests"
>>> hold_pf = vbt.PF.from_holding(data)
>>> both = vbt.PF.column_stack(
...     pf,
...     hold_pf,
...     wrapper_kwargs=dict(keys=pd.Index(["crossover", "hold"], name="strategy")),
... )
>>> both.wrapper.shape
(1096, 51)
>>> both.total_return["hold"].round(3)
BTC-USD    0.959
ETH-USD   -0.116
SOL-USD    0.060
Name: total_return, dtype: float64
```

按行堆叠将连续时期，例如前向滚动的各测试窗口，连接为一段历史。堆叠后的投资组合保留记录，并据此重新计算现金和净值。独立模拟的各窗口都从新资金开始：首个窗口的初始现金成为堆叠组合的初始现金，后续窗口的初始现金则在各自起点作为入金加入，因此收益率保持正确，但净值会跳变。`combine_init_cash=True`
改为将全部资金视为初始现金，使后续窗口的资金在早期闲置，稀释收益。若要让同一账户贯穿全部窗口，应从上一窗口最终状态继续，并以 `chained=True`
堆叠，参见[实时模拟](/zh/features/backtesting/live-simulation/)。下方[按列堆叠](#column-stacking)和[按行堆叠](#row-stacking)功能展示完整投资组合的两种方式。

这也让您可分段处理长历史，或围绕多组入场和离场事件模拟数据窗口。边界处保留预期现金与持仓状态，再按行堆叠结果分析。大批独立实验如何分批，参见[并行执行与缓存](/zh/features/performance/parallel-execution-and-caching/)。

## 索引 \[#indexing]

每个 VBT 对象都可像 pandas 一样索引：按列标签、用 `.loc` 和 `.iloc` 按日期与位置、按参数层级，或用
`xs`。按日期切片投资组合会得到该时期的新投资组合，无须重跑模拟，记录和度量均按窗口调整。设置器则反向使用同一方式：借助数据封装器，按日期、日内时间、标签或掩码赋值。下方[切片](#slicing)与[索引对齐](#index-alignment)功能展示两者。

检查订单、绘制净值或比较回撤前，先选择交易品种或参数组合。标签让您能从汇总表回到产生特定结果的回测。实验维度较多时，也可重命名、重排或删除索引层级，保持报告易读。

## Pandas 访问器 \[#pandas-accessors]

导入 VBT 后，每个 Series、DataFrame 和 Index 都会增加 `.vbt` 访问器，以及 `.vbt.returns`、`.vbt.signals` 和 `.vbt.ohlcv`
等专门命名空间。数据仍是普通 pandas 对象，因此同一行中可混用 VBT 和 pandas 操作。访问器在运行时注册，部分编辑器无法自动补全，但 notebook
可以。`vbt.df_acc(df)` 和 `vbt.sr_acc(sr)` 以显式对象返回同一访问器，编辑器可以内省。

## DataFrame 笛卡尔积 \[#dataframe-product]

新增于 1.13.0

✅ 多个参数化指标可能产生形状与列不同的 DataFrame，而它们通常共享不应相互组合的列层级（如 "symbol"），使笛卡尔积构建变得复杂。现在可以通过新方法，分块交叉连接多个
DataFrame。

```python title="Enter when SMA goes above WMA, exit when EMA goes below WMA"
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
>>> sma = data.run("sma", timeperiod=[10, 20], unpack=True)
>>> ema = data.run("ema", timeperiod=[30, 40], unpack=True)
>>> wma = data.run("wma", timeperiod=[50, 60], unpack=True)
>>> sma, ema, wma = sma.vbt.x(ema, wma)  # (1)
>>> entries = sma.vbt.crossed_above(wma)
>>> exits = ema.vbt.crossed_below(wma)

>>> entries.columns
MultiIndex([(10, 30, 50, 'BTC-USD'),
            (10, 30, 50, 'ETH-USD'),
            (10, 30, 60, 'BTC-USD'),
            (10, 30, 60, 'ETH-USD'),
            (10, 40, 50, 'BTC-USD'),
            (10, 40, 50, 'ETH-USD'),
            (10, 40, 60, 'BTC-USD'),
            (10, 40, 60, 'ETH-USD'),
            (20, 30, 50, 'BTC-USD'),
            (20, 30, 50, 'ETH-USD'),
            (20, 30, 60, 'BTC-USD'),
            (20, 30, 60, 'ETH-USD'),
            (20, 40, 50, 'BTC-USD'),
            (20, 40, 50, 'ETH-USD'),
            (20, 40, 60, 'BTC-USD'),
            (20, 40, 60, 'ETH-USD')],
           names=['sma_timeperiod', 'ema_timeperiod', 'wma_timeperiod', 'symbol'])
```

1.  构建三个 DataFrame 的笛卡尔积，同时保持 "symbol" 列层级不变。也可使用 `vbt.pd_acc.cross(sma, ema, wma)`。

## 索引字典 \[#index-dictionaries]

新增于 1.3.0

手动创建数组并用 Pandas 设置数据往往很繁琐。现在有新功能来帮忙！任意可广播参数都可变成索引字典，其中包含在数组何处设置值的指令，并自动填入。它知道应更新哪个轴，非必要时不创建完整数组，节省
RAM ❤️

```python title="1) Accumulate daily and exit on Sunday vs 2) accumulate weekly and exit on month end"
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"])
>>> tile = pd.Index(["daily", "weekly"], name="strategy")  # (1)
>>> pf = vbt.PF.from_orders(
...     data.close,
...     size=vbt.index_dict({  # (2)
...         vbt.idx(
...             vbt.pointidx(every="day"),
...             vbt.colidx("daily", level="strategy")): 100,  # (3)
...         vbt.idx(
...             vbt.pointidx(every="sunday"),
...             vbt.colidx("daily", level="strategy")): -np.inf,  # (4)
...         vbt.idx(
...             vbt.pointidx(every="monday"),
...             vbt.colidx("weekly", level="strategy")): 100,
...         vbt.idx(
...             vbt.pointidx(every="monthend"),
...             vbt.colidx("weekly", level="strategy")): -np.inf,
...     }),
...     size_type="value",
...     direction="longonly",
...     init_cash="auto",
...     broadcast_kwargs=dict(tile=tile)
... )
>>> pf.sharpe_ratio
strategy  symbol
daily     BTC-USD    0.702259
          ETH-USD    0.782296
weekly    BTC-USD    0.838895
          ETH-USD    0.524215
Name: sharpe_ratio, dtype: float64
```

1.  为表示两个策略，需要将同一数据平铺两次。创建包含策略名称的参数，以 `tile` 传给广播器，将价格等各数组的列平铺两次。
2.  索引字典以索引指令为键、待设置数据为值。键可为行索引、标签，或 `PointIdxr` 等自定义索引器类。
3.  找到每天起始行的索引和 "daily" 列索引，将对应元素设为 100（即加仓）。
4.  找到星期日对应行的索引。若此前指令已设置这些位置的值，则会覆盖。

## 切片 \[#slicing]

新增于 1.3.0

✅ 与选列类似，每个 VBT 对象现在也能用与 Pandas 相同的机制切片行 🔪 无须重新模拟，即可轻松分析和绘制模拟数据的任意子集！

```python title="Analyze multiple date ranges of the same portfolio"
>>> data = vbt.YFData.pull("BTC-USD")
>>> pf = vbt.PF.from_holding(data, freq="d")

>>> pf.sharpe_ratio
1.116727709477293

>>> pf.loc[:"2020"].sharpe_ratio  # (1)
1.2699801554196481

>>> pf.loc["2021": "2021"].sharpe_ratio  # (2)
0.9825161170278687

>>> pf.loc["2022":].sharpe_ratio  # (3)
-1.0423271337174647
```

1.  获取 2020 年及之前的夏普比率。
2.  获取 2021 年的夏普比率。
3.  获取 2022 年及之后的夏普比率。

## 按列堆叠 \[#column-stacking]

新增于 1.3.0

✅ 同类型复杂 VBT 对象可以轻松按列堆叠。例如，将多个无关交易策略合并为一个投资组合分析。最终对象底层仍是单一的多维结构，甚至可以比多个独立合并对象处理得更快 🫁

```python title="Analyze two trading strategies separately and then jointly"
>>> def strategy1(data):
...     fast_ma = vbt.MA.run(data.close, 50, short_name="fast_ma")
...     slow_ma = vbt.MA.run(data.close, 200, short_name="slow_ma")
...     entries = fast_ma.ma_crossed_above(slow_ma)
...     exits = fast_ma.ma_crossed_below(slow_ma)
...     return vbt.PF.from_signals(
...         data.close,
...         entries,
...         exits,
...         size=100,
...         size_type="value",
...         init_cash="auto"
...     )

>>> def strategy2(data):
...     bbands = vbt.BBANDS.run(data.close, window=14)
...     entries = bbands.close_crossed_below(bbands.lower)
...     exits = bbands.close_crossed_above(bbands.upper)
...     return vbt.PF.from_signals(
...         data.close,
...         entries,
...         exits,
...         init_cash=200
...     )

>>> data1 = vbt.BinanceData.pull("BTCUSDT")
>>> pf1 = strategy1(data1)  # (1)
>>> pf1.sharpe_ratio
0.9100317671866922

>>> data2 = vbt.BinanceData.pull("ETHUSDT")
>>> pf2 = strategy2(data2)  # (2)
>>> pf2.sharpe_ratio
-0.11596286232734827

>>> pf_sep = vbt.PF.column_stack((pf1, pf2))  # (3)
>>> pf_sep.sharpe_ratio
0    0.910032
1   -0.115963
Name: sharpe_ratio, dtype: float64

>>> pf_join = vbt.PF.column_stack((pf1, pf2), group_by=True)  # (4)
>>> pf_join.sharpe_ratio
0.42820898354646514
```

1.  在独立投资组合中分析第一个策略。
2.  在独立投资组合中分析第二个策略。
3.  在同一投资组合中分别分析两个策略。
4.  在同一投资组合中联合分析两个策略。

## 按行堆叠 \[#row-stacking]

新增于 1.3.0

✅ 同类型复杂 VBT 对象可以轻松按行堆叠。例如，为已有投资组合追加新数据，或将样本内投资组合与对应样本外投资组合连接起来 🧬

```python title="Analyze two date ranges separately and then jointly"
>>> def strategy(data, start=None, end=None):
...     fast_ma = vbt.MA.run(data.close, 50, short_name="fast_ma")
...     slow_ma = vbt.MA.run(data.close, 200, short_name="slow_ma")
...     entries = fast_ma.ma_crossed_above(slow_ma)
...     exits = fast_ma.ma_crossed_below(slow_ma)
...     return vbt.PF.from_signals(
...         data.close[start:end],
...         entries[start:end],
...         exits[start:end],
...         size=100,
...         size_type="value",
...         init_cash="auto"
...     )

>>> data = vbt.BinanceData.pull("BTCUSDT")

>>> pf_whole = strategy(data)  # (1)
>>> pf_whole.sharpe_ratio
0.9100317671866922

>>> pf_sub1 = strategy(data, end="2019-12-31")  # (2)
>>> pf_sub1.sharpe_ratio
0.7810397448678937

>>> pf_sub2 = strategy(data, start="2020-01-01")  # (3)
>>> pf_sub2.sharpe_ratio
1.070339534746574

>>> pf_join = vbt.PF.row_stack((pf_sub1, pf_sub2))  # (4)
>>> pf_join.sharpe_ratio
0.9100317671866922
```

1.  分析完整区间。
2.  分析第一个日期区间。
3.  分析第二个日期区间。
4.  合并两个日期区间并一同分析。

## 索引对齐 \[#index-alignment]

新增于 1.3.0

✅ 不再要求每个 Pandas 数组具有相同索引。只要数据类型一致，所有需要相互广播的数组，其索引都会自动对齐。

```python title="Predict ETH price with BTC price using linear regression"
>>> btc_data = vbt.YFData.pull("BTC-USD")
>>> btc_data.wrapper.shape
(2817, 7)

>>> eth_data = vbt.YFData.pull("ETH-USD")  # (1)
>>> eth_data.wrapper.shape
(1668, 7)

>>> ols = vbt.OLS.run(  # (2)
...     btc_data.close,
...     eth_data.close
... )
>>> ols.pred
Date
2014-09-17 00:00:00+00:00            NaN
2014-09-18 00:00:00+00:00            NaN
2014-09-19 00:00:00+00:00            NaN
2014-09-20 00:00:00+00:00            NaN
2014-09-21 00:00:00+00:00            NaN
...                                  ...
2022-05-30 00:00:00+00:00    2109.769242
2022-05-31 00:00:00+00:00    2028.856767
2022-06-01 00:00:00+00:00    1911.555689
2022-06-02 00:00:00+00:00    1930.169725
2022-06-03 00:00:00+00:00    1882.573170
Freq: D, Name: Close, Length: 2817, dtype: float64
```

1.  ETH-USD 的历史短于 BTC-USD。
2.  现在可以运行！确保全部数组具有相同时间周期和时区。
