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.
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.0best_ms(lambda: df.vbt.rolling_zscore(20))12.0best_ms(lambda: df.vbt.rolling_zscore(20, jitted=dict(parallel=True))) 8.2The 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:
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: float64For 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:
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.0At 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 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.
@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()Tutorial
Learn more in the Signal development tutorial.
✅ 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.
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()Tutorial
Learn more in the Signal development tutorial.
✅ 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.
@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: float64data.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!) ⌨️
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: int64Related pages
- Documentation › FundamentalsUnderstand VBT's vectorized architecture and its core concepts
- Indicators and signals › Technical indicatorsRun TA-Lib, pandas-ta, ta, SMC, WorldQuant Alphas, and native indicators at scale
- Data › Data pipelinesAlign, update, transform, and analyze multi-symbol market data in one object
- Backtesting › Backtesting engineSimulate orders, signals, and callbacks across many assets and parameters at once
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.