# 基础概念 (/zh/documentation/fundamentals)

VBT 旨在解决许多回测库中常见的性能问题。它的核心理念是：每个交易策略实例都可以用向量化形式表示。这样，多个策略实例便可合并为一个多维数组，从而实现高效处理和直观分析。

## 技术栈 \[#stack]

交易数据以时间序列为基础，因此回测的大多数环节都可以表示为数组。VBT 尤其依赖
[NumPy 数组](https://numpy.org/doc/stable/user/quickstart.html)，它们得益于经过优化、预先编译的 C 代码，***速度极快***。在活跃的
Python 生态中，许多科学计算包都支持 NumPy 数组，包括 Pandas、NumPy 和 Numba。您很可能已经使用过其中一些包！

虽然 NumPy 性能出色，但它并不总是时间序列分析中最直观的工具。看看下面这个使用 NumPy 计算移动平均的例子：

```python
>>> from vectorbtpro import *

>>> def rolling_window(a, window):
...     shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
...     strides = a.strides + (a.strides[-1],)
...     return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)

>>> np.mean(rolling_window(np.arange(10), 3), axis=1)
array([1., 2., 3., 4., 5., 6., 7., 8.])
```

这种方法虽然很快，但理解其原理需要一些时间，正确编写这类向量化代码也需要经验。那么，更高级指标中使用的其他滚动函数呢？重采样、分组以及其他涉及日期和时间的操作呢？

这时就该 [Pandas](https://pandas.pydata.org/docs/getting_started/overview.html) 出场了！Pandas
提供丰富的时间序列功能、数据对齐、兼容缺失值的统计运算、groupby、merge 和 join
方法，以及其他许多实用工具。它有两种主要数据结构：[Series](https://pandas.pydata.org/docs/reference/api/pandas.Series.html)（一维）和
[DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html)（二维）。您可以将它们视为附带时间戳、列名等有用信息的
NumPy 数组。前面的移动平均只需一行就能写出：

```python
>>> index = vbt.date_range("2020-01-01", periods=10)
>>> sr = pd.Series(range(len(index)), index=index)
>>> sr.rolling(3).mean()
2020-01-01    NaN
2020-01-02    NaN
2020-01-03    1.0
2020-01-04    2.0
2020-01-05    3.0
2020-01-06    4.0
2020-01-07    5.0
2020-01-08    6.0
2020-01-09    7.0
2020-01-10    8.0
Freq: D, dtype: float64
```

VBT 非常依赖 Pandas，但使用方式可能与您想象的不同。对我们而言，Pandas 有一个主要局限：处理大型数据集和用户自定义函数时，速度会变慢。滚动平均等许多内置函数底层使用
[Cython](https://cython.org/)，速度足够快。但当您尝试实现更复杂的函数，例如涉及多条时间序列的滚动排名指标时，情况就会变得复杂且缓慢。另外，无法向量化的函数怎么办？例如，包含资金管理的投资组合策略无法直接通过向量计算模拟。在这些情况下，我们需要编写快速的迭代代码，逐元素处理数据。

如果有一个 Python 包，让您能以机器码速度运行 for 循环呢？而且它与 NumPy 无缝配合，无需大幅修改 Python 代码？这将解决许多问题：代码会变得非常快，同时依然易读。这个包就是
[Numba](https://numba.pydata.org/numba-doc/latest/user/5minguide.html)。Numba 将一部分 Python 和 NumPy
代码转换为高效的机器码。

```python
>>> @njit
... def moving_average_nb(a, window_len):
...     b = np.empty_like(a, dtype=float_)
...     for i in range(len(a)):
...         window_start = max(0, i + 1 - window_len)
...         window_end = i + 1
...         if window_end - window_start < window_len:
...             b[i] = np.nan
...         else:
...             b[i] = np.mean(a[window_start:window_end])
...     return b

>>> moving_average_nb(np.arange(10), 3)
array([nan, nan, 1., 2., 3., 4., 5., 6., 7., 8.])
```

现在，我们可以清楚地看到程序在做什么：逐个时间戳遍历时间序列，检查窗口内的数据是否足够，如果足够，就计算均值。Numba 不仅能帮助我们编写更易读、更不易出错的代码，速度还可媲美
[C](https://en.wikipedia.org/wiki/C_\(programming_language\))！

```python
>>> big_a = np.arange(1000000)
>>> %timeit moving_average_nb.py_func(big_a, 10)  # (1)!
6.54 s ± 142 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

>>> %timeit np.mean(rolling_window(big_a, 10), axis=1)  # (2)!
24.7 ms ± 173 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> %timeit pd.Series(big_a).rolling(10).mean()  # (3)!
10.2 ms ± 309 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

>>> %timeit moving_average_nb(big_a, 10)  # (4)!
5.12 ms ± 7.21 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
```

1.  Python。
2.  NumPy。
3.  Pandas。
4.  Numba！

!!! tip
    如果您好奇 VBT 如何使用 Numba，可以查找名为 `nb`
    的目录或文件。[这个子包](https://github.com/polakowo/vectorbt.pro/blob/main/vectorbtpro/generic/nb/)包含所有基础函数，而[这个模块](https://github.com/polakowo/vectorbt.pro/blob/main/vectorbtpro/portfolio/nb/from_order_func.py)则处理一些高级主题（⚠️
    仅限高手）。

那么，问题在哪里？遗憾的是，Numba 只理解 NumPy，不理解 Pandas。这意味着我们会失去日期时间索引和时间序列分析必需的其他功能。这正是 VBT 的用武之地：它用 Numba
重新实现了许多 Pandas 函数，甚至还添加了新功能。结果是，Pandas 的一部分功能不仅更快，也变得更强大！

其工作方式如下：

1.  从 Pandas 对象中提取 NumPy 数组。
2.  在该数组上运行由 Numba 编译的函数。
3.  将结果重新封装为 Pandas 对象。

```python
>>> arr = sr.values
>>> result = moving_average_nb(arr, 3)
>>> new_sr = pd.Series(result, index=sr.index, name=sr.name)
>>> new_sr
2020-01-01    NaN
2020-01-02    NaN
2020-01-03    1.0
2020-01-04    2.0
2020-01-05    3.0
2020-01-06    4.0
2020-01-07    5.0
2020-01-08    6.0
2020-01-09    7.0
2020-01-10    8.0
Freq: D, dtype: float64
```

或者，使用 VBT：

```python
>>> sr.vbt.rolling_mean(3)
2020-01-01    NaN
2020-01-02    NaN
2020-01-03    1.0
2020-01-04    2.0
2020-01-05    3.0
2020-01-06    4.0
2020-01-07    5.0
2020-01-08    6.0
2020-01-09    7.0
2020-01-10    8.0
Freq: D, dtype: float64
```

## 访问器 \[#accessors]

注意到 `vbt` 是如何直接附加在 Series
对象上的吗？这称为[访问器](https://pandas.pydata.org/docs/development/extending.html#registering-custom-accessors)，是一种无需创建子类即可扩展
Pandas 对象的便捷方式。通过访问器，您可以轻松切换原生 Pandas 和 VBT 功能。此外，每个 VBT 方法都能灵活接受输入，同时适用于 Series 和 DataFrame。

```python
>>> df = pd.DataFrame({'a': range(10), 'b': range(9, -1, -1)})
>>> df.vbt.rolling_mean(3)
     a    b
0  NaN  NaN
1  NaN  NaN
2  1.0  8.0
3  2.0  7.0
4  3.0  6.0
5  4.0  5.0
6  5.0  4.0
7  6.0  3.0
8  7.0  2.0
9  8.0  1.0
```

您可以在 API 文档中进一步了解 VBT 的访问器。例如，`rolling_mean` 属于 `GenericAccessor` 访问器，可直接通过 `vbt` 访问。另一个常用访问器
`ReturnsAccessor` 用于处理收益率。它是 `GenericAccessor` 的子类，可通过 `vbt.returns` 访问。

```python
>>> ret = pd.Series([0.1, 0.2, -0.1])
>>> ret.vbt.returns.total()
0.18800000000000017
```

!!! warning
    每个访问器都要求数据已准备为可直接使用的格式。例如，处理收益率的访问器要求输入收益率，而不是价格！

## 多维性 \[#multidimensionality]

请记住，VBT 与传统回测工具的区别，在于将交易数据视为多维数组。具体来说，VBT 将每一列视为独立的回测实例，而不是一个特征。来看一个简单的 OHLC DataFrame：

```python
>>> p1 = pd.DataFrame({
...     'open': [1, 2, 3, 4, 5],
...     'high': [2.5, 3.5, 4.5, 5.5, 6.5],
...     'low': [0.5, 1.5, 2.5, 3.5, 4.5],
...     'close': [2, 3, 4, 5, 6]
... }, index=vbt.date_range("2020-01-01", periods=5))
>>> p1
            open  high  low  close
2020-01-01     1   2.5  0.5      2
2020-01-02     2   3.5  1.5      3
2020-01-03     3   4.5  2.5      4
2020-01-04     4   5.5  3.5      5
2020-01-05     5   6.5  4.5      6
```

这里的各列是不同的特征，描述同一个抽象对象：价格。将这个 DataFrame 直接传给 VBT 似乎很自然（类似于
[scikit-learn](https://scikit-learn.org/stable/) 和其他要求以列表示特征的机器学习工具），但这种方式在回测中存在一些缺点：

1.  不同长度或类型的特征难以用 NumPy 拼接。
2.  可选特征无法恰当地表示为数组。
3.  多个回测可以堆叠为三维数组（立方体），但 Pandas 无法正确处理。
4.  不变的特征和常数需要转换为数组，并在所有回测中复制，造成内存浪费。

VBT 通过将各特征作为独立数组处理，来应对这种差异。因此，您无需传入一个大型 DataFrame，而是分别提供每个特征：

```python
>>> single_pf = vbt.Portfolio.from_holding(
...     open=p1['open'],
...     high=p1['high'],
...     low=p1['low'],
...     close=p1['close']
... )
>>> single_pf.value
2020-01-01    100.0
2020-01-02    150.0
2020-01-03    200.0
2020-01-04    250.0
2020-01-05    300.0
Freq: D, dtype: float64
```

现在，如果您想处理多个抽象对象，例如多个交易标的，只需将 Series 换为 DataFrame：

```python
>>> p2 = pd.DataFrame({
...     'open': [6, 5, 4, 3, 2],
...     'high': [6.5, 5.5, 4.5, 3.5, 2.5],
...     'low': [4.5, 3.5, 2.5, 1.5, 0.5],
...     'close': [5, 4, 3, 2, 1]
... }, index=vbt.date_range("2020-01-01", periods=5))
>>> p2
            open  high  low  close
2020-01-01     6   6.5  4.5      5
2020-01-02     5   5.5  3.5      4
2020-01-03     4   4.5  2.5      3
2020-01-04     3   3.5  1.5      2
2020-01-05     2   2.5  0.5      1

>>> multi_open = pd.DataFrame({
...     'p1': p1['open'],
...     'p2': p2['open']
... })
>>> multi_high = pd.DataFrame({
...     'p1': p1['high'],
...     'p2': p2['high']
... })
>>> multi_low = pd.DataFrame({
...     'p1': p1['low'],
...     'p2': p2['low']
... })
>>> multi_close = pd.DataFrame({
...     'p1': p1['close'],
...     'p2': p2['close']
... })

>>> multi_pf = vbt.Portfolio.from_holding(
...     open=multi_open,
...     high=multi_high,
...     low=multi_low,
...     close=multi_close
... )
>>> multi_pf.value
               p1     p2
2020-01-01  100.0  100.0
2020-01-02  150.0   80.0
2020-01-03  200.0   60.0
2020-01-04  250.0   40.0
2020-01-05  300.0   20.0
```

这里，每个特征 DataFrame 中的每一列（VBT 中有时称为一条“线”）都表示独立的回测实例，并生成独立的权益曲线。因此，要增加一个回测，只需为各特征增加一列即可 ✨

将特征分开还有一个主要优势：便于组合。更进一步，我们可以通过向量化，一次性组合所有回测实例。例如，下面在前一根 K 线为上涨 K 线时设置入场信号，在前一根 K 线为下跌 K
线时设置离场信号（这是一个用于说明的基础示例）：

```python
>>> candle_green = multi_close > multi_open
>>> prev_candle_green = candle_green.vbt.signals.fshift(1)
>>> prev_candle_green
               p1     p2
2020-01-01  False  False
2020-01-02   True  False
2020-01-03   True  False
2020-01-04   True  False
2020-01-05   True  False

>>> candle_red = multi_close < multi_open
>>> prev_candle_red = candle_red.vbt.signals.fshift(1)
>>> prev_candle_red
               p1     p2
2020-01-01  False  False
2020-01-02  False   True
2020-01-03  False   True
2020-01-04  False   True
2020-01-05  False   True
```

Pandas 对象 `multi_close` 和 `multi_open` 可以是任意形状的 Series 或 DataFrame，我们的这个小型流水线仍会按预期运行。

## 标签 \[#labels]

在上面的例子中，我们创建多组 OHLC DataFrame 时使用了 `p1` 和 `p2` 两列，便于之后在分析阶段识别。为此，VBT 确保这些列会在整个回测流水线中保留，从信号生成一直到绩效建模。

但如果单个列表示更复杂的配置，例如涉及多种超参数组合，该怎么办？这时，将复杂对象直接存为列标签并不合适。幸运的是，Pandas
提供了[分层列](https://pandas.pydata.org/pandas-docs/stable/user_guide/advanced.html)：它们类似普通列，但由多个层级叠加而成。层级中的每一层，都可以用于识别某个特定输入或参数。

以简单的均线交叉策略为例，它取决于快、慢窗口的长度。每个超参数都成为操作数据时的一个额外维度，并存为独立的列层级。下面是一个更复杂的例子，展示 MACD 指标的列层级结构：

```python
>>> macd = vbt.MACD.run(
...     multi_close,
...     fast_window=2,
...     slow_window=(3, 4),
...     signal_window=2,
...     macd_wtype="simple",
...     signal_wtype="weighted"
... )
>>> macd.signal
macd_fast_window               2             2  << fast window for MACD line
macd_slow_window               3             4  << slow window for MACD line
macd_signal_window             2             2  << window for signal line
macd_macd_wtype           simple        simple  << window type for MACD line
macd_signal_wtype       weighted      weighted  << window type for signal line
                         p1   p2       p1   p2  << price
2020-01-01              NaN  NaN      NaN  NaN
2020-01-02              NaN  NaN      NaN  NaN
2020-01-03              NaN  NaN      NaN  NaN
2020-01-04              0.5 -0.5      NaN  NaN
2020-01-05              0.5 -0.5      1.0 -1.0
```

上面的列表示两种不同的回测配置，现在可以使用 Pandas 轻松分析和比较。这是一种强大的数据分析方式。例如，您可以按 `macd_fast_window`
对绩效分组，观察快窗口大小如何影响策略盈利能力。是不是很神奇？

## 广播 \[#broadcasting]

广播是 VBT 中最重要的概念之一。由于 VBT 函数将时间序列作为独立数组接受，它们需要知道如何连接不同数组中的元素，以便 1）信息完整，2）涵盖所有数组，且 3）覆盖每个时间步。

如果所有数组大小相同，VBT 很容易逐元素执行运算。如果某个数组更小，VBT 会尝试将其“拉伸”，以匹配其他数组的长度。这种方法深受
[NumPy 广播](https://numpy.org/doc/stable/user/basics.broadcasting.html)的启发，内部也以此为基础。与 NumPy
的主要区别是，一维数组始终按行指定，因为我们主要处理时间序列数据。

为什么广播重要？因为它让您可以向 VBT 中几乎每个函数传入任意形状的类数组对象，无论是常数还是完整的 DataFrame，VBT 都会自动判断每个元素应处的位置。

```python
>>> part_arrays = dict(
...     close=pd.DataFrame({  # (1)!
...         'a': [1, 2, 3, 4],
...         'b': [4, 3, 2, 1]
...     }),
...     size=pd.Series([1, -1, 1, -1]),  # (2)!
...     direction=[['longonly', 'shortonly']],  # (3)!
...     fees=0.01  # (4)!
... )
>>> full_arrays = vbt.broadcast(part_arrays)

>>> full_arrays['close']
   a  b
0  1  4
1  2  3
2  3  2
3  4  1

>>> full_arrays['size']
   a  b
0  1  1
1 -1 -1
2  1  1
3 -1 -1

>>> full_arrays['direction']
          a          b
0  longonly  shortonly
1  longonly  shortonly
2  longonly  shortonly
3  longonly  shortonly

>>> full_arrays['fees']
      a     b
0  0.01  0.01
1  0.01  0.01
2  0.01  0.01
3  0.01  0.01
```

1.  按元素指定。无需广播。
2.  按行指定。沿列方向广播。
3.  按列指定。沿行方向广播。
4.  为整个数组指定。沿行和列两个方向广播。

!!! tip
    可以遵循以下经验法则：

    1.  只要有一个数组是 Pandas 对象，输出就始终是 Pandas 对象。
    2.  只要有一个数组是二维的，输出就始终是二维数组。
    3.  如果所有数组都是常数或一维数组，输出就始终是一维数组。
    4.  如果数组是一维 NumPy 数组或 Series，就始终按行指定。
    5.  列表和其他序列会在广播之前转换为 NumPy 数组。

与 NumPy 和 Pandas 不同，VBT 知道如何广播标签：如果两个对象中的列或某个列层级不同，就会将它们堆叠在一起。例如，您可以使用 (2, 3) 和 (3, 4)
这两组窗口组合，检查快均线何时高于慢均线。

```python
>>> fast_ma = vbt.MA.run(multi_close, window=[2, 3], short_name='fast')
>>> slow_ma = vbt.MA.run(multi_close, window=[3, 4], short_name='slow')

>>> fast_ma.ma
fast_window    2    2    3    3
              p1   p2   p1   p2
2020-01-01   NaN  NaN  NaN  NaN
2020-01-02   2.5  4.5  NaN  NaN
2020-01-03   3.5  3.5  3.0  4.0
2020-01-04   4.5  2.5  4.0  3.0
2020-01-05   5.5  1.5  5.0  2.0

>>> slow_ma.ma
slow_window    3    3    4    4
              p1   p2   p1   p2
2020-01-01   NaN  NaN  NaN  NaN
2020-01-02   NaN  NaN  NaN  NaN
2020-01-03   3.0  4.0  NaN  NaN
2020-01-04   4.0  3.0  3.5  3.5
2020-01-05   5.0  2.0  4.5  2.5

>>> fast_ma.ma > slow_ma.ma  # (1)!
ValueError: Can only compare identically-labeled DataFrame objects

>>> fast_ma.ma.values > slow_ma.ma.values  # (2)!
array([[False, False, False, False],
       [False, False, False, False],
       [ True, False, False, False],
       [ True, False,  True, False],
       [ True, False,  True, False]])

>>> fast_ma.ma.vbt > slow_ma.ma  # (3)!
fast_window      2      2      3      3
slow_window      3      3      4      4
                p1     p2     p1     p2
2020-01-01   False  False  False  False
2020-01-02   False  False  False  False
2020-01-03    True  False  False  False
2020-01-04    True  False   True  False
2020-01-05    True  False   True  False
```

1.  使用 Pandas 比较失败。
2.  使用 NumPy 比较成功。
3.  使用 VBT 比较成功。

!!! tip
    在左侧的 Pandas 对象后加上 `.vbt`，即可通过 VBT 广播两个操作数，并通过 NumPy/Numba 执行运算。这样就能兼得强大功能与便捷体验 🧨

与 Pandas 不同，VBT 按行和列的绝对位置广播，而不是按标签。这种广播方式与 NumPy 非常相似：

```python
>>> df1 = pd.DataFrame({'a': [0], 'b': [1]})
>>> df2 = pd.DataFrame({'b': [0], 'a': [1]})
>>> df1 + df2  # (1)!
   a  b
0  1  1

>>> df1.values + df2.values
array([[0, 2]])

>>> df1.vbt + df2  # (2)!
   a  b
   b  a
0  0  2
```

1.  Pandas 将 `df1` 中的列 `a` 与 `df2` 中的列 `a` 对齐。
2.  VBT 将 `df1` 中的第一列与 `df2` 中的第一列对应起来，而不考虑标签。

!!! warning
    向 VBT 传入多个数据数组时，请确保它们的列在位置上一一对应！

    如果列的顺序不正确，您会发现结果有多个列层级，标签相同但顺序不同。

VBT 的另一个特性，是能够广播形状不兼容、但多级索引层级存在重叠的对象，也就是共享相同名称或值的对象。接着上面的例子，我们来检查快均线何时高于价格：

```python
>>> fast_ma.ma > multi_close  # (1)!
ValueError: Can only compare identically-labeled DataFrame objects

>>> fast_ma.ma.values > multi_close.values  # (2)!
ValueError: operands could not be broadcast together with shapes (5,4) (5,2)

>>> fast_ma.ma.vbt > multi_close  # (3)!
fast_window      2      2      3      3
                p1     p2     p1     p2
2020-01-01   False  False  False  False
2020-01-02   False   True  False  False
2020-01-03   False   True  False   True
2020-01-04   False   True  False   True
2020-01-05   False   True  False   True
```

1.  使用 Pandas 比较失败。
2.  使用 NumPy 比较失败。
3.  使用 VBT 比较成功。

还有更多（请继续看）：您可以将多个类标量超参数作为 Pandas Index 传入，轻松进行测试。让我们看看价格是否位于某些阈值之间：

```python
>>> above_lower = multi_close.vbt > vbt.Param([1, 2], name='lower')
>>> below_upper = multi_close.vbt < vbt.Param([3, 4], name='upper')
>>> above_lower.vbt & below_upper
lower           1      1      2      2
upper           3      3      4      4
               p1     p2     p1     p2
2020-01-01   True  False  False  False
2020-01-02  False  False   True  False
2020-01-03  False  False  False   True
2020-01-04  False   True  False  False
2020-01-05  False  False  False  False
```

可以看到，在合并信息方面，智能广播堪称 💎。请参阅 `broadcast`，进一步了解广播原理和组合数组的新方法。

## 灵活索引 \[#flexible-indexing]

广播多个大型数组可能占用大量内存，最终拖慢处理速度。因此，VBT 引入了“灵活索引”：从任意形状的一维或二维数组中选取一个元素。例如，如果一维数组只有一个元素，却需要广播到 1000
行，无论查询哪一行，VBT 都会返回这一个元素，因为该数组可以广播到任意形状：

```python
>>> a = np.array([1])

>>> vbt.flex_select_1d_nb(a, 0)
1

>>> vbt.flex_select_1d_nb(a, 1)
1

>>> vbt.flex_select_1d_nb(a, 2)
1
```

这等价于：

```python
>>> full_a = np.broadcast_to(a, (1000,))

>>> full_a[2]
1
```

二维数组提供更大的灵活性。假设您需要处理 1000
列，并有几个参数应用于每个元素。有些参数可能是所有元素共享的标量，有些可能是在每列重复的一维数组，还有些可能在每行相同。无需完整广播这些数组，您只需保留原有元素数量，并按需扩展为二维，使其能够通过
NumPy 正确广播：

```python
>>> a = np.array([[0]])  # (1)!
>>> b = np.array([[1, 2, 3]])  # (2)!
>>> c = np.array([[4], [5], [6]])  # (3)!

>>> vbt.flex_select_nb(a, 2, 1)  # (4)!
0

>>> vbt.flex_select_nb(b, 2, 1)
2

>>> vbt.flex_select_nb(c, 2, 1)
6
```

1.  每个数组一个值。每个元素都使用相同值。
2.  每列一个值。每行都使用相同的一组值。
3.  每行一个值。每列都使用相同的一组值。
4.  查询第三行、第二列的元素。

这种方法的一个优点是，几乎不会增加额外的内存开销，而且无论目标形状多大，都可以沿任意方向广播。这也是 `Portfolio.from_signals` 能够广播 50
多个参数，同时保持内存效率和性能的关键之一 😉
