# 时间序列操作 (/zh/features/tooling/time-series-operations)

如何在数百列上快速运行滚动窗口、归约和日期逻辑，同时保留 pandas 标签？VBT 通过 `.vbt` 访问器，为每个 Series 和 DataFrame 添加编译后的时间序列操作。在 Numba
中计算，再获得带标签的 pandas 对象，可直接用于指标、信号与绩效分析。

```python title="Compute a rolling z-score on 500 columns with pandas and with VBT"
>>> import timeit

>>> np.random.seed(42)
>>> df = pd.DataFrame(
...     np.random.normal(size=(5000, 500)).cumsum(axis=0),
...     index=pd.date_range("2020-01-01", periods=5000, freq="h"),
... )
>>> pd_z = (df - df.rolling(20).mean()) / df.rolling(20).std()
>>> vbt_z = df.vbt.rolling_zscore(20)
>>> np.allclose(pd_z, vbt_z, equal_nan=True), vbt_z.shape
(True, (5000, 500))

>>> def best_ms(func):  # (1)
...     return min(timeit.repeat(func, number=1, repeat=7)) * 1000

>>> best_ms(lambda: (df - df.rolling(20).mean()) / df.rolling(20).std())
67.0
>>> best_ms(lambda: df.vbt.rolling_zscore(20))
12.0
>>> best_ms(lambda: df.vbt.rolling_zscore(20, jitted=dict(parallel=True)))  # (2)
8.2
```

1.  函数已编译一次后，取七次运行中的最短耗时。
2.  用并行线程运行 500 列。

输出与 pandas 完全一致。在构建本页的 Apple M3 上，编译版本耗时 12 毫秒，而非 67 毫秒；使用八核时为 8 毫秒。耗时取决于机器和数据，请在自己的环境中测量。

这些操作可用于标准化价格序列、估计滚动波动率、比较资产相关性，或从多个输入数组构建信号。它们直接处理已有 pandas 数据，因此可以在 notebook 中加入 VBT 计算，无须迁移整个工作流。

## 窗口 \[#windows]

均值、和、标准差、最小值、最大值、最值位置、排名、z 分数、协方差、相关性和回归的滚动与扩展版本均已编译，指数加权均值、标准差和线性加权均值也是如此。中位数和分位数未编译，可用
`rolling_apply` 配合自己的函数，或通过 pandas 计算。所有窗口函数都接受最少观测数，适用于 Series 和 DataFrame，并在需要第二个输入时对其广播：

```python title="Compute a 30-day rolling correlation between Bitcoin and Ether"
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], start="2024-01-01", end="2025-01-01")
>>> rets = data.close.pct_change()
>>> rets["BTC-USD"].vbt.rolling_corr(rets["ETH-USD"], 30).describe().round(3)
count    336.000
mean       0.796
std        0.094
min        0.443
25%        0.749
50%        0.818
75%        0.861
max        0.948
dtype: float64
```

其他计算可用 `rolling_apply`、`expanding_apply` 和
`groupby_apply`，在各窗口或组中运行自己的编译函数。近期行为适合固定窗口，自起点以来的统计适合扩展窗口，希望新观测权重更高时则可用指数权重。

使用 `minp`，可要求指数计算具备足够多的近期观测才返回值，控制数据缺口对信号的影响。

## 日期与时间 \[#dates-and-times]

数据实例保留带时区时间戳。默认将无时区时间戳视为 UTC，所有数据均存为 UTC。`tz_localize` 指定无时区时间戳原本记录的时区，`tz_convert`
指定最终保留的时区，例如交易所当地时间。一个数据实例只保留一个时区，因此不同交易所的品种共用该时区。所有接受日期的位置均可解析日期字符串、`"3 months ago"`
等相对日期及时间差。编译代码内部使用整数表示日期时间，辅助函数可提取小时、星期或月份，并寻找下一个交易时段边界，详见下方
[Numba 日期时间](#numba-datetime)。`.vbt.xloc["09:30":"16:00"]` 跨所有日期选择指定日内时间。`vbt.wait`
和日历区间索引掩码有助于制定计划；`.vbt.ago` 按固定时间距离而非固定 K 线数量取值，参见[过去时段](#periods-ago)。

!!! warning "夏令时"
    当地时间的日内规则相对 UTC 每年会发生两次一小时偏移。规则涉及交易时段时，应将索引保留在交易所时区。

### 按 K 线数量或经过时间回看 \[#look-back-by-bars-or-elapsed-time]

存在观测缺失时，上一根 K 线可能已经超过一小时前。选择与待测规则匹配的回看方式：

```python title="Compare the previous bar with exactly one hour ago"
>>> price = pd.Series(
...     [100.0, 101.0, 104.0, 102.0],
...     index=pd.to_datetime([
...         "2025-01-01 09:00", "2025-01-01 10:00",
...         "2025-01-01 12:00", "2025-01-01 13:00",
...     ]),
... )
>>> lookbacks = pd.DataFrame({
...     "price": price,
...     "previous_bar": price.vbt.ago(1),
...     "one_hour_ago": price.vbt.ago("1h"),
... })
>>> lookbacks
                     price  previous_bar  one_hour_ago
2025-01-01 09:00:00  100.0           NaN           NaN
2025-01-01 10:00:00  101.0         100.0         100.0
2025-01-01 12:00:00  104.0         101.0           NaN
2025-01-01 13:00:00  102.0         104.0         104.0
```

中午时，上一根 K 线来自 10:00，而精确回看一小时没有观测可返回。`any_ago` 和 `all_ago` 将此思路扩展到近期事件，例如过去一小时内是否曾满足条件，或过去五根 K
线是否始终满足条件。

日历规则也可选择周期性时点和区间，例如某个工作日的入场时间或交易时段窗口。将这些掩码与价格条件组合成[交易信号](/zh/features/indicators/trading-signals/)，或按[日内回测](/zh/features/backtesting/intraday-and-tick-backtesting/)页的方法比较不同时段表现。

## 清理与精度 \[#cleaning-and-precision]

`fillna`、`ffill`、`bfill` 和位移逐列在编译代码中执行。用 `fshift` 和 `bshift` 位移时，可选择新缺口的填充值，因此整数或布尔数组能保持原有类型。交叉检查可用
`skipna=True` 跳过缺失值，而非将每个缺口视为中断。

自定义编译规则中，`is_close_nb` 等数值辅助函数可按显式绝对与相对容差比较值。当舍入噪声可能将原本相等的值变成不同决策时，这很有用。

## 应用、映射与归约 \[#apply-map-and-reduce]

`map` 逐值转换，`apply_along_axis` 逐列或逐行运行函数，`reduce`
将各列归约为一个值或数组，均可使用自己的编译函数。元方法更进一步，可对多个已对齐数组一次运行一个函数，自定义指标和度量正是借此无须编写 Python
循环。下方[元方法](#meta-methods)功能介绍展示此模式，[数组表达式](#array-expressions)则在命名数组上求公式字符串的值。

分组操作可按日历周期或其他标签汇总观测，转换操作则可为信号去均值或标准化。自定义滚动计算可用同一元方法模式结合价格、成交量与基准。当计算成为可复用指标时，[指标开发](/zh/features/indicators/indicator-development/)可为其添加命名输入、参数和输出。

## 其他工具 \[#other-utilities]

`rebase` 将序列起点重设为指定值，`proximity_apply` 归约各元素周围的窗口，`shuffle` 在列内打乱数值以进行随机检验，`squeeze_grouped` 和
`flatten_grouped` 将各组的多列合并为一列。底层内核还提供使用自定义系数的因果 FIR 滤波，以及用于自定义数值计算的多项式拟合。

## Numba 日期时间 \[#numba-datetime]

新增于 1.2.3

✅ Numba 函数无法直接接收 pandas 日期时间索引。那么如何将数据与时间关联？VBT 提供一组处理整数时间戳的函数，可从每个时间戳提取当前时间、星期等信息，以判断 K 线是否位于交易时段。

```python title="Plot the percentage change from the start of the month to now"
>>> @njit
... def month_start_pct_change_nb(arr, index):
...     out = np.full(arr.shape, np.nan)
...     for col in range(arr.shape[1]):
...         for i in range(arr.shape[0]):
...             if i == 0 or vbt.dt_nb.month_nb(index[i - 1]) != vbt.dt_nb.month_nb(index[i]):
...                 month_start_value = arr[i, col]
...             else:
...                 out[i, col] = (arr[i, col] - month_start_value) / month_start_value
...     return out

>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], start="2022", end="2023")
>>> pct_change = month_start_pct_change_nb(
...     vbt.to_2d_array(data.close),
...     data.index.vbt.to_ns()  # (1)
... )
>>> pct_change = data.symbol_wrapper.wrap(pct_change)
>>> pct_change.vbt.plot().show()
```

1.  将日期时间索引转换为纳秒格式。

2022 年比特币和以太坊相对每月月初的百分比变化. [Figure data (JSON)](/assets/figures/features/productivity/numba-datetime.b4837f52d042.json)

!!! info "教程"
    更多内容参见[信号开发](/zh/tutorials/signal-development)教程。

## 过去时段 \[#periods-ago]

新增于 1.2.3

✅ 比较不同 K 线上的值，也可使用 Pandas 的向量化方式，无须编写 Numba 函数。VBT 添加便捷的访问器方法，用于按时间差回看，并检查过去一段时间内是否发生过事件。

```python title="Check whether the price dropped for 5 consecutive bars"
>>> data = vbt.YFData.pull("BTC-USD", start="2022-05", end="2022-08")
>>> mask = (data.close < data.close.vbt.ago(1)).vbt.all_ago(5)
>>> fig = data.plot(plot_volume=False)
>>> mask.vbt.signals.ranges.plot_shapes(
...     plot_close=False,
...     fig=fig,
...     add_shape_kwargs=dict(fillcolor="orangered")
... )
>>> fig.show()
```

比特币价格，突出显示连续五根 K 线下跌的区间. [Figure data (JSON)](/assets/figures/features/productivity/periods-ago.ce2f9924664f.json)

!!! info "教程"
    更多内容参见[信号开发](/zh/tutorials/signal-development)教程。

## 元方法 \[#meta-methods]

新增于 1.0.0

✅ 滚动应用等许多方法现在有两种版本：普通方法（实例方法）和元方法（类方法）。普通方法绑定单个数组，不需要元数据；元方法不绑定任何数组。借助广播与模板逻辑，自定义函数可以使用 VBT
提供的窗口边界和列索引，同时处理多个命名数组。

```python title="Compute the rolling z-score on one array and the rolling correlation coefficient on two arrays"
>>> @njit
... def zscore_nb(x):  # (1)
...     return (x[-1] - np.mean(x)) / np.std(x)

>>> data = vbt.YFData.pull("BTC-USD", start="2020", end="2021")
>>> data.close.rolling(14).apply(zscore_nb, raw=True)  # (2)
Date
2020-01-01 00:00:00+00:00         NaN
                                  ...
2020-12-27 00:00:00+00:00    1.543527
2020-12-28 00:00:00+00:00    1.734715
2020-12-29 00:00:00+00:00    1.755125
2020-12-30 00:00:00+00:00    2.107147
2020-12-31 00:00:00+00:00    1.781800
Freq: D, Name: Close, Length: 366, dtype: float64

>>> data.close.vbt.rolling_apply(14, zscore_nb)  # (3)
2020-01-01 00:00:00+00:00         NaN
                                  ...
2020-12-27 00:00:00+00:00    1.543527
2020-12-28 00:00:00+00:00    1.734715
2020-12-29 00:00:00+00:00    1.755125
2020-12-30 00:00:00+00:00    2.107147
2020-12-31 00:00:00+00:00    1.781800
Freq: D, Name: Close, Length: 366, dtype: float64

>>> @njit
... def corr_meta_nb(from_i, to_i, col, a, b):  # (4)
...     a_window = a[from_i:to_i, col]
...     b_window = b[from_i:to_i, col]
...     return np.corrcoef(a_window, b_window)[1, 0]

>>> data2 = vbt.YFData.pull(["ETH-USD", "XRP-USD"], start="2020", end="2021")
>>> vbt.pd_acc.rolling_apply(  # (5)
...     14,
...     corr_meta_nb,
...     vbt.Rep("a"),
...     vbt.Rep("b"),
...     broadcast_named_args=dict(a=data.close, b=data2.close)
... )
symbol                      ETH-USD   XRP-USD
Date
2020-01-01 00:00:00+00:00       NaN       NaN
...                             ...       ...
2020-12-27 00:00:00+00:00  0.636862 -0.511303
2020-12-28 00:00:00+00:00  0.674514 -0.622894
2020-12-29 00:00:00+00:00  0.712531 -0.773791
2020-12-30 00:00:00+00:00  0.839355 -0.772295
2020-12-31 00:00:00+00:00  0.878897 -0.764446

[366 rows x 2 columns]
```

1.  只能访问窗口本身。
2.  使用 Pandas。
3.  使用普通方法，接受与 Pandas 相同的函数。
4.  可以访问一个或多个完整数组。
5.  使用元方法，接受元数据和可变参数。

## 数组表达式 \[#array-expressions]

新增于 1.0.0

✅ 组合多个数组时，通常需先对齐并广播，再执行运算。单用 Pandas 往往过于严格，难以满足需求。VBT 提供访问器类方法，可接收普通 Python
表达式、识别全部变量名、从当前上下文提取数组、广播它们，再求表达式值，还支持 [NumExpr](https://github.com/pydata/numexpr)！⌨️

```python title="Evaluate a multiline array expression based on a Bollinger Bands indicator"
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"])

>>> low = data.low
>>> high = data.high
>>> bb = vbt.talib("BBANDS").run(data.close)
>>> upperband = bb.upperband
>>> lowerband = bb.lowerband
>>> bandwidth = (bb.upperband - bb.lowerband) / bb.middleband
>>> up_th = vbt.Param([0.3, 0.4])
>>> low_th = vbt.Param([0.1, 0.2])

>>> expr = """
... narrow_bands = bandwidth < low_th
... above_upperband = high > upperband
... wide_bands = bandwidth > up_th
... below_lowerband = low < lowerband
... (narrow_bands & above_upperband) | (wide_bands & below_lowerband)
... """
>>> mask = vbt.pd_acc.eval(expr)
>>> mask.sum()
low_th  up_th  symbol
0.1     0.3    BTC-USD    344
               ETH-USD    171
        0.4    BTC-USD    334
               ETH-USD    158
0.2     0.3    BTC-USD    444
               ETH-USD    253
        0.4    BTC-USD    434
               ETH-USD    240
dtype: int64
```
