Features

Time-series operations

Run compiled rolling windows, reductions, and date logic on labeled pandas data

How do you run rolling windows, reductions, and date logic over hundreds of columns quickly without losing pandas labels? VBT adds compiled time-series operations to every Series and DataFrame through the .vbt accessor. Run calculations in Numba and get labeled pandas objects back, ready to use in indicators, signals, and performance analysis.

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):  
    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)))  
8.2

The output is identical to pandas. On the Apple M3 that built this page, the compiled version took 12 ms instead of 67 ms, and 8 ms across eight cores. Timings depend on the machine and the data, so measure on yours.

Use these operations to normalize a price series, estimate rolling volatility, compare assets' correlations, or build a signal from several input arrays. They work on your existing pandas data, so you can add a VBT calculation to a notebook without moving the whole workflow.

Windows

Rolling and expanding versions of mean, sum, standard deviation, min, max, the position of the min and max, rank, z-score, covariance, correlation, and regression are compiled, as are exponentially weighted mean and standard deviation and a linearly weighted mean. Median and quantiles are not, so they run through rolling_apply with your own function or through pandas. Every window function takes a minimum number of observations, works on Series and DataFrames alike, and broadcasts a second input where one is needed:

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

For anything else, rolling_apply, expanding_apply, and groupby_apply run your own compiled function over each window or group. Use a fixed window for recent behavior, an expanding window for a statistic since the start, or exponential weights when newer observations should count more.

Use minp to require enough recent observations before an exponential calculation returns a value, giving you control over how gaps in the data affect your signals.

Dates and times

Data instances keep timezone-aware timestamps. By default, timestamps without a timezone are read as UTC, and everything is stored in UTC. tz_localize sets the timezone that naive timestamps were recorded in, and tz_convert sets the one to keep, such as the exchange's local time. One data instance holds one timezone, so symbols from different exchanges share it. Date strings, relative dates such as "3 months ago", and timedeltas are parsed wherever dates are accepted. Inside compiled code, datetimes are integers, and helper functions extract the hour, weekday, or month and find the next session boundary, as the Numba datetime highlight below shows. .vbt.xloc["09:30":"16:00"] selects a time of day across all dates. vbt.wait and index masks for calendar intervals help with schedules, and .vbt.ago reads values a fixed time back rather than a fixed number of bars, as shown in Periods ago.

Daylight saving time

Time-of-day rules in local time move by an hour twice a year relative to UTC. Keep the index in the exchange's timezone when a rule refers to its session times.

Look back by bars or elapsed time

A previous bar can be more than an hour old when observations are missing. Choose the lookback that matches the rule you want to test:

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

At noon, the previous bar is from 10:00, while the exact one-hour lookup has no observation to return. any_ago and all_ago extend the idea to recent events, such as whether a condition was true at any point during the last hour or throughout the last five bars.

Calendar rules can also select recurring points and intervals, such as a weekday entry time or a trading-session window. Combine these masks with price conditions for Trading signals, or use them to compare behavior by session on the Intraday backtesting page.

Cleaning and precision

fillna, ffill, bfill, and shifts work column by column in compiled code. Shifting with fshift and bshift fills the new gaps with a value of your choice, so an integer or boolean array keeps its type. Crossover checks can skip missing values with skipna=True instead of treating each gap as a break.

For custom compiled rules, numerical helpers such as is_close_nb compare values with explicit absolute and relative tolerances. This is useful when rounding noise would otherwise turn an intended equality into a different decision.

Apply, map, and reduce

map transforms values one by one, apply_along_axis runs a function per column or row, and reduce turns each column into one value or one array, all with your own compiled functions. Meta methods go a step further and run one function over several aligned arrays at once, which is how custom indicators and metrics are built without writing loops in Python. The Meta methods highlight below shows the pattern, and Array expressions evaluate a formula string over named arrays.

Group operations let you summarize observations by a calendar period or another label, while transformations can demean or normalize values for a signal. A custom rolling calculation can combine price, volume, and a benchmark through the same meta-method pattern. When the calculation becomes a reusable indicator, Indicator development adds named inputs, parameters, and outputs around it.

Other utilities

rebase restarts a series at a chosen value, proximity_apply reduces windows around each element, shuffle reorders values within columns for randomized tests, and squeeze_grouped and flatten_grouped combine the columns of each group into one. Lower-level kernels also provide causal FIR filtering with your own coefficients and polynomial fitting for custom numerical work.

Numba datetime

✅ Numba functions cannot take a pandas datetime index directly. So, how do you connect data to time? VBT provides a collection of functions that work on timestamp integers to extract information from each timestamp, such as the current time and day of the week, to determine whether the bar is during trading hours.

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()  
)
pct_change = data.symbol_wrapper.wrap(pct_change)
pct_change.vbt.plot().show()
Bitcoin and Ethereum percentage change from the start of each month in 2022 Figure data (JSON)

Tutorial

Learn more in the Signal development tutorial.

Periods ago

✅ Instead of writing Numba functions, comparing values at different bars can also be done in a vectorized way with Pandas. VBT adds convenient accessor methods for looking back by timedeltas and checking whether events happened during a past period.

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()
Bitcoin price with highlighted ranges where price dropped for five consecutive bars Figure data (JSON)

Tutorial

Learn more in the Signal development tutorial.

Meta methods

✅ Many methods, such as rolling apply, now come in two versions: regular (instance methods) and meta (class methods). Regular methods are bound to a single array and do not need metadata, while meta methods are not tied to any array. Their broadcasting and templating logic lets a custom function work with several named arrays at once, using the window boundaries and column index supplied by VBT.

Compute the rolling z-score on one array and the rolling correlation coefficient on two arrays
@njit
def zscore_nb(x):  
    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)  
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)  
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):  
    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(  
    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]

✅ When combining multiple arrays, they often need to be aligned and broadcast before the operation itself. Pandas alone often falls short because it can be too strict. Fortunately, VBT includes an accessor class method that can take a regular Python expression, identify all variable names, extract the arrays from the current context, broadcast them, and then evaluate the expression (with support for NumExpr!) ⌨️

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

Copyright © 2021–2026 Oleg Polakow. All rights reserved.

Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.