Features

Compute backends

Run kernels with Numba, parallel Numba, Rust, or NumPy and choose the backend per call

Why is VBT fast, and how do you control what runs where? VBT PRO moves heavy backtesting and analysis operations into Numba-compiled functions. Supported functions can also run in parallel, use the optional Rust extension, or use a NumPy implementation you register. Choose a backend per call or globally while keeping the same Python research workflow.

Faster calculations from pandas

You can start with a pandas Series or DataFrame and call VBT's rolling statistics, indicators, and portfolio simulators without writing a compiled function yourself. VBT passes numerical arrays to the calculation and wraps results with their labels. Your dates, symbols, and parameter combinations remain available for analysis.

For custom logic, Numba lets you write numerical loops in Python and compile them. This is useful for a signal that depends on an open position or an indicator that updates its state at each bar. Your compiled functions work with NumPy arrays, while data preparation and reporting can stay in pandas. See custom indicators and event-driven backtesting for examples.

Compare NumPy, Numba, and Rust

Time one rolling mean on 50 million values across backends and memory layouts
import timeit

@vbt.register_jitted(task_id_or_func=vbt.nb.rolling_mean_nb)  
def rolling_mean_np(arr, window, minp=None):
    csum = np.cumsum(arr, axis=0)
    out = np.full(arr.shape, np.nan)
    out[window - 1] = csum[window - 1]
    out[window:] = csum[window:] - csum[:-window]
    return out / window

np.random.seed(42)
values = 100 + np.random.normal(0, 0.01, size=(100_000, 500)).cumsum(axis=0)
layouts = {
    "row-major": pd.DataFrame(np.ascontiguousarray(values)),  
    "column-major": pd.DataFrame(np.asfortranarray(values)),
}
backends = {
    "pandas": None,
    "NumPy": "np",
    "Numba": "nb",
    "Numba parallel": dict(jitter="nb", parallel=True),
    "Rust": "rs",
    "Rust parallel": dict(jitter="rs", parallel=True),
}
def ms(func):
    return min(timeit.repeat(func, number=3, repeat=5)) / 3 * 1000

timings = {}
for layout, prices in layouts.items():
    for backend, jitted in backends.items():
        if jitted is None:
            func = lambda: prices.rolling(50).mean()
        else:
            func = lambda: prices.vbt.rolling_mean(50, jitted=jitted)
        timings[(backend, layout)] = ms(func)
pd.Series(timings).unstack().loc[list(backends), list(layouts)].round(0).astype(int)
                row-major  column-major
pandas                693           360
NumPy                 489           204
Numba                 561           163
Numba parallel        379            32
Rust                   87           145
Rust parallel          71            67

The table shows milliseconds per call on an Apple M3, and every backend returned the same values. The fastest run took 32 ms against 360 ms for pandas on the same array, but the winner changed with the layout: parallel Numba on column-major data and parallel Rust on row-major data. Rust's rolling mean walks the array row by row and first copies column-major input into row order, while Numba's runs down each column and gains the most when columns are contiguous. That is why VBT lets you choose a backend per call, set one globally, or let measured timings choose.

Which backend fits your workload?

Your workloadWhat to try
Backtests with your own Python trading rulesNumba compiles numerical callbacks and simulation loops.
Rolling calculations across many symbols or parameter combinationsParallel Numba or Rust can spread supported calculations across CPU cores.
Repeated calls to supported built-in functionsRust runs native code without waiting for Numba to compile that function.
An existing NumPy implementation you want to reuseRegister it alongside the built-in implementation and select it with jitted.
A research pipeline with different calculation shapesMeasure the important calls and use per-call choices or AutoBench.

Columns can represent both instruments and strategy configurations. Simulations parallelize independent columns or groups, while processing each strategy's bars in order. For distributing whole backtests across workers or splitting large arrays into chunks, see parallel execution and caching.

Numba compilation

Most numerical functions in VBT are compiled with Numba, from rolling statistics and signal generators to the simulators behind vbt.PF.from_signals. The simulators are compiled loops over bars rather than whole-array operations, which is why path-dependent rules such as stops and position sizing from the current balance run at compiled speed. Compiled functions release the GIL, so threads can run them side by side, and cache-enabled built-in functions store compiled code on disk for reuse in later Python sessions. Changing data or parameter values does not itself require recompilation when the argument types stay the same, making repeated research runs a good fit.

The first call in a fresh environment still compiles, which can take seconds. Your own callbacks, such as a signal function for from_signals, are compiled too, and the simulator is compiled again for each new callback, which took about 20 seconds on the machine that built this page. Redefining the callback, for example by rerunning the notebook cell that defines it, counts as new, so keep callbacks in their own cell or module. The staticized option turns a simulation that calls them into a function that can be cached on disk as well, so it survives restarts and new worker processes. This caches the compiled simulator, so you can still run it on new market data and parameters.

For debugging, jitted=False runs the underlying Python function, as the cumulative-sum example below shows. You can also disable Numba before importing VBT through the numba.disable configuration setting. This controls Numba compilation, while backend selection controls whether a call uses Rust.

Backends and jitters

A jitted function in VBT is a registry entry with one or more implementations: nb for Numba, rs for the optional Rust extension, np for vectorized NumPy, or any jitter you register. The jitted argument picks one per call and passes options such as parallel=True, as the Jitting and Parallel Numba highlights below show. Parallel versions process columns on several cores, which pays off on wide arrays and costs a little on narrow ones. Rolling return metrics, such as the rolling Sharpe and Sortino ratios, update running sums in one pass instead of recomputing each window, as the Hyperfast rolling metrics highlight below shows.

Backend selection

With the Rust extension installed, compatible calls go to Rust by default and fall back to Numba when Rust cannot handle the inputs. In the example above, the default call took 86 ms on row-major data and 147 ms on column-major data, the Rust timings. Three settings change that:

  • Per call: jitted="nb", jitted="rs", or a dictionary with options.
  • By module: resolution overrides set a backend for every function whose path matches, such as everything under vectorbtpro.returns.
  • By measurement: AutoBench reads a benchmark cache built on your machine and sends each call to an eligible backend using those measurements and, where needed, the input shape.

The Rust backend highlight on the Rust engine page shows each option. Backend selection happens inside the calculation, so you can keep the same pandas inputs and result handling when switching supported implementations.

Find your fastest backend

Compare backends on your own arrays with VBT's benchmark tools, then use the timings to select a backend for each calculation. Keep the same Python inputs and labeled results.

Parallel Numba

✅ Most Numba-compiled functions have been rewritten to process columns in parallel using automatic parallelization with @jit. You can enable this with a single command. This approach is best for lightweight functions that are applied to wide arrays.

Benchmark the rolling mean without and with parallelization
np.random.seed(42)
df = pd.DataFrame(np.random.uniform(size=(1000, 1000)))

%timeit df.rolling(10).mean()  
45.6 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit df.vbt.rolling_mean(10)  
5.33 ms ± 302 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit df.vbt.rolling_mean(10, jitted=dict(parallel=True))  
1.82 ms ± 5.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Jitting

✅ Jitting stands for just-in-time compiling. In the VBT universe, however, jitting simply means accelerating. While Numba remains the primary jitter, VBT now allows you to implement custom jitter classes, such as those for vectorized NumPy or even JAX with GPU support. Every jitted function is registered globally, so you can switch between different implementations or even disable jitting entirely with a single command.

Run different implementations of the cumulative sum
data = vbt.YFData.pull("BTC-USD", start="7 days ago")
log_returns = np.log1p(data.close.pct_change())
log_returns.vbt.cumsum()  
Date
2023-01-31 00:00:00+00:00    0.000000
2023-02-01 00:00:00+00:00    0.024946
2023-02-02 00:00:00+00:00    0.014271
2023-02-03 00:00:00+00:00    0.013310
2023-02-04 00:00:00+00:00    0.008288
2023-02-05 00:00:00+00:00   -0.007967
2023-02-06 00:00:00+00:00   -0.010087
Freq: D, Name: Close, dtype: float64
log_returns.vbt.cumsum(jitted=False)  
Date
2023-01-31 00:00:00+00:00    0.000000
2023-02-01 00:00:00+00:00    0.024946
2023-02-02 00:00:00+00:00    0.014271
2023-02-03 00:00:00+00:00    0.013310
2023-02-04 00:00:00+00:00    0.008288
2023-02-05 00:00:00+00:00   -0.007967
2023-02-06 00:00:00+00:00   -0.010087
Freq: D, Name: Close, dtype: float64
@vbt.register_jitted(task_id_or_func=vbt.nb.nancumsum_nb)  
def nancumsum_np(arr):
    return np.nancumsum(arr, axis=0)

log_returns.vbt.cumsum(jitted="np")  
Date
2023-01-31 00:00:00+00:00    0.000000
2023-02-01 00:00:00+00:00    0.024946
2023-02-02 00:00:00+00:00    0.014271
2023-02-03 00:00:00+00:00    0.013310
2023-02-04 00:00:00+00:00    0.008288
2023-02-05 00:00:00+00:00   -0.007967
2023-02-06 00:00:00+00:00   -0.010087
Freq: D, Name: Close, dtype: float64

✅ Rolling metrics based on returns have been optimized for maximum performance, with up to 1000x speedup!

Benchmark the rolling Sortino ratio
import quantstats as qs

index = vbt.date_range("2020", periods=100000, freq="1min")
np.random.seed(42)
returns = pd.Series(np.random.normal(0, 0.001, size=len(index)), index=index)

%timeit qs.stats.rolling_sortino(returns, rolling_period=10)  
2.79 s ± 24.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit returns.vbt.returns.rolling_sortino_ratio(window=10)  
8.12 ms ± 199 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

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.