# Compute backends (/features/performance/compute-backends)

## Native Rust simulators \[#native-rust-simulators]

New in v2026.9.5

(Recently added)

✅ Take your strategy directly to Rust. VBT's native simulators let the same strategy process a full price
array or step through individual bars as they arrive. Strategy callbacks can inspect cash, positions,
and orders during execution, keeping portfolio state within reach of your trading logic.

!!! note "Note"
    The examples below share the strategy definition and require `vectorbtpro-rust` and `ndarray = "0.16"`.
    See the [Rust setup guide](/documentation/rust/) for installation.

```rust title="Define a strategy with price and position conditions"
use vectorbtpro_rust::error::VbtResult;
use vectorbtpro_rust::portfolio::enums::Order;
use vectorbtpro_rust::portfolio::simulator::{
    FnOrderStrategy, OrderContext, OrderStrategy,
};

fn buy_the_dip() -> impl OrderStrategy {
    FnOrderStrategy::new(|ctx: &OrderContext<'_, '_>| {
        let price = ctx.close(ctx.col());
        let position = ctx.position(ctx.col());
        let size = if price <= 100.0 && position == 0.0 { // (1)
            1.0
        } else if price >= 110.0 && position > 0.0 {
            -position
        } else {
            return Ok(None);
        };
        Ok(Some(Order::builder().size(size).build()))
    })
}
```

1.  Buy one share only when flat. The position check prevents another buy at $98.

=== "Batch"
    ```rust title="Run a batch simulation"
    use ndarray::array;
    use vectorbtpro_rust::portfolio::simulator::{OrderSimulator, SimulationConfig};

    fn main() -> VbtResult<()> {
        let close = array![[100.0], [98.0], [105.0], [112.0]];
        let groups = array![1];
        let config = SimulationConfig::builder()
            .target_shape(close.dim())
            .group_lens(groups.view())
            .close(close.view())
            .init_cash(1000.0)
            .build()?;
        let simulator = OrderSimulator::builder().config(config).build()?;
        let output = simulator.run_single(&mut buy_the_dip())?;
        for order in &output.order_records {
            println!("row {}: {:.0} share at ${:.0}", order.idx, order.size, order.price);
        }
        Ok(())
    }
    ```

    ```text title="Output"
    row 0: 1 share at $100
    row 3: 1 share at $112
    ```

=== "Streaming"
    ```rust title="Run a streaming simulation"
    use ndarray::array;
    use vectorbtpro_rust::portfolio::simulator::{OrderStepper, StreamConfig, StreamRow};

    fn main() -> VbtResult<()> {
        let close = array![[100.0], [98.0], [105.0], [112.0]];
        let groups = array![1];
        let config = StreamConfig::builder()
            .group_lens(groups.view())
            .init_cash(1000.0)
            .build()?;
        let mut live = OrderStepper::new_single(config, None, buy_the_dip())?;
        for row in close.rows() {
            let step = live.step(StreamRow::builder().close(row).build())?; // (1)
            for order in step.order_records() {
                println!("row {}: {:.0} share at ${:.0}", order.idx, order.size, order.price);
            }
        }
        Ok(())
    }
    ```

    1.  Preserve portfolio state between calls and return the current bar's fills. The stepper does not retain the complete record history by default.

    ```text title="Output"
    row 0: 1 share at $100
    row 3: 1 share at $112
    ```

!!! info "Tutorial"
    Learn more in the
    [From Python to Rust](/tutorials/from-python-to-rust/) tutorial.

## Rust backend \[#rust-backend]

New in v2026.6.27

✅ Install the optional `vectorbtpro-rust` extension and compatible jitted calls can take the Rust
fast lane automatically. VBT exposes the extension as `vbt.rs`, registers Rust kernels under `jitted="rs"`,
and falls back to the normal implementation, usually Numba, when Rust is unavailable or unsupported.

=== "Out of the box"
    ```python title="Keep your workflow, get the Rust lane"
    >>> data = vbt.YFData.pull("BTC-USD", start="2024")

    >>> fast_ma = data.close.vbt.rolling_mean(20)  # (1)
    >>> slow_ma = data.close.vbt.rolling_mean(50)
    >>> entries = fast_ma.vbt.crossed_above(slow_ma)  # (2)
    >>> exits = fast_ma.vbt.crossed_below(slow_ma)

    >>> pf = vbt.Portfolio.from_signals(  # (3)
    ...     data,
    ...     entries=entries,
    ...     exits=exits,
    ...     sl_stop=0.05,
    ...     tp_stop=0.15,
    ...     fees=0.001,
    ... )
    ```

    1.  `rolling_mean_nb` routes to `vbt.rs.generic.rolling.rolling_mean_rs`.
    2.  `crossed_above_nb` routes to `vbt.rs.generic.base.crossed_above_rs`.
    3.  `from_signals_nb` routes to `vbt.rs.portfolio.from_signals.from_signals_rs`.

=== "Speed check"
    ```python title="Compare Numba, parallel Numba, Rust, and parallel Rust"
    >>> np.random.seed(42)
    >>> returns = pd.DataFrame(np.random.normal(0, 0.01, size=(200_000, 64)))  # (1)

    >>> nb = lambda: returns.vbt.returns.rolling_profit_factor(
    ...     window=50,
    ...     jitted="nb"
    ... )
    >>> nb_parallel = lambda: returns.vbt.returns.rolling_profit_factor(
    ...     window=50,
    ...     jitted=dict(jitter="nb", parallel=True)
    ... )
    >>> rs = lambda: returns.vbt.returns.rolling_profit_factor(
    ...     window=50,
    ...     jitted="rs"
    ... )
    >>> rs_parallel = lambda: returns.vbt.returns.rolling_profit_factor(
    ...     window=50,
    ...     jitted=dict(jitter="rs", parallel=True)
    ... )

    >>> nb(); nb_parallel(); rs(); rs_parallel()  # (2)

    >>> %timeit nb()
    1.21 s ± 655 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)

    >>> %timeit nb_parallel()
    214 ms ± 15.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

    >>> %timeit rs()
    272 ms ± 736 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)

    >>> %timeit rs_parallel()
    54.6 ms ± 1.23 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    ```

    1.  Use a wide enough array to give parallel kernels room to stretch.
    2.  Warm up first so Numba compilation stays out of the race. These timings were measured on an
        Apple M3 and will vary by machine.

=== "AutoBench"
    ```bash title="Build the benchmark cache first"
    python -m vectorbtpro.benchmarks.bench_matrix_cli --full
    ```

    !!! info "Note"
        The full benchmark matrix can take several hours to finish, depending on your machine.

    ```python title="Let local timings pick the lane"
    >>> vbt.settings.jitting["backends"]["auto_bench_path"] = "benchmarks/cache.json"  # (1)
    >>> vbt.settings.jitting["backends"]["auto_mode"] = "bench"  # (2)

    >>> data = vbt.YFData.pull("BTC-USD", start="2024")
    >>> sharpe_ratio = data.returns.vbt.returns.sharpe_ratio()  # (3)
    ```

    1.  Run benchmarks before enabling AutoBench. The matrix command writes this cache by default;
        change the path if you pass a custom `--output-dir` or `--cache`.
    2.  Use `"bench"` for AutoBench or `"bench_mixed"` for AutoBenchMixed. AutoBenchMixed can pick
        across serial and parallel candidates automatically.
    3.  Leave `jitted` empty. Compatible calls consult your machine's benchmark cache automatically.

=== "Control panel"
    ```python title="Tune Rust preference"
    >>> vbt.settings.jitting["backends"]["auto_mode"] = False  # (1)
    >>> vbt.settings.jitting["resolve_overrides"]["returns_module"] = dict(  # (2)
    ...     match="vectorbtpro.returns",
    ...     resolve_kwargs=dict(auto_mode=True),
    ... )

    >>> data = vbt.YFData.pull("BTC-USD", start="2024")
    >>> sharpe_ratio = data.returns.vbt.returns.sharpe_ratio()
    ```

    1.  Disable Rust-first automatic dispatch globally.
    2.  Re-enable automatic backend dispatch only for returns-module calls.

=== "Raw Rust, fastest lane"
    ```python title="Resolve or call the Rust function directly"
    >>> total_return_rs = vbt.resolve_jitted(  # (1)
    ...     vbt.ret_nb.total_return_nb,
    ...     jitted="rs",
    ...     use_backend_wrapper=False,
    ... )

    >>> data = vbt.YFData.pull("BTC-USD", start="2024")
    >>> returns = data.returns.vbt.to_2d_array()  # (2)

    >>> total_return_rs(returns)  # (3)
    array([0.652341])

    >>> vbt.rs.returns.total_return_rs(returns)  # (4)
    array([0.652341])
    ```

    1.  Let VBT find the Rust backend registered for `total_return_nb`. With `use_backend_wrapper=False`,
        this returns the raw Rust function itself.
    2.  Prepare arguments. Raw kernels expect raw arrays, not wrapped Series/DataFrames.
    3.  Call the resolved Rust function with raw-array arguments.
    4.  Or import the same Rust function from its module path. The API page for each Numba function
        lists its Rust counterpart path when one exists.

    !!! info
        Raw Rust functions skip all VBT checks, conversions, and fallbacks. Use them only when you
        have prepared arguments in the exact format expected by the Rust function.

## Benchmarks \[#benchmarks]

New in v2026.6.27

✅ VBT now ships with correctness-aware backend benchmarks for almost the entire codebase. The
latest benchmark overview covers **748** functions, **768** variants, **1,976** kernels,
**15,709** cases, and **59,600** benchmark runs across Numba, Rust, raw backend calls, AutoBench,
and AutoBenchMixed. This makes performance inspectable: users can see which backend wins, at which
input size, and whether parallel execution is worth it for their workload.

=== "Targeted run"
    ```bash title="Benchmark one family from the terminal"
    python -m vectorbtpro.benchmarks.bench_engine_cli \
      --backend nb \
      --backend rs \
      --input-model 1d \
      --pattern returns
    ```

    ```python title="Run the same benchmark from Python"
    >>> results = vbt.run_benchmarks(
    ...     input_model="1d",
    ...     backend_ids=("nb", "rs"),
    ...     patterns=["returns"],
    ... )
    >>> print(results)  # (1)
    function,task_id,ndim,shape,nb_s,rs_s,speedup,auto_bench_backend,nb_elapsed_s,rs_elapsed_s
    ...
    ```

    1.  Results are emitted as CSV with per-backend runtimes, speedup, elapsed benchmark time, and the
        backend selected by AutoBench when applicable. Outputs are checked for parity before timing by default.

=== "Full matrix"
    ```bash title="Generate cache, reports, and SVG plots"
    python -m vectorbtpro.benchmarks.bench_matrix_cli --full --write-plots
    ```

    !!! info "Note"
        The full matrix can take a long time because it benchmarks thousands of cases across multiple
        backends, shapes, and execution modes. The full run writes Markdown reports, `cache.json`,
        `runtime.svg`, and `speedup.svg`. Writing plots also requires a Plotly static image export
        engine such as Kaleido.

Benchmark median runtime ranks by backend and total element count. [Figure data (JSON)](/assets/figures/features/productivity/benchmark-runtime.7d417eb67553.json)

## Parallel Numba \[#parallel-numba]

New in 1.0.0

✅ Most Numba-compiled functions have been rewritten to process columns in parallel using
[automatic parallelization with `@jit`](https://numba.readthedocs.io/en/stable/user/parallel.html).
You can enable this with a single command. This approach is best for lightweight functions that are
applied to wide arrays.

```python title="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()  # (1)
45.6 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> %timeit df.vbt.rolling_mean(10)  # (2)
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))  # (3)
1.82 ms ± 5.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```

1.  Using Pandas.
2.  Using Numba without parallelization.
3.  Using Numba with parallelization.

## Jitting \[#jitting]

New in 1.0.0

✅ 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](https://github.com/google/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.

```python title="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()  # (1)
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)  # (2)
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)  # (3)
... def nancumsum_np(arr):
...     return np.nancumsum(arr, axis=0)

>>> log_returns.vbt.cumsum(jitted="np")  # (4)
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
```

1.  Using the built-in Numba-compiled function.
2.  Using the built-in function but with Numba disabled → regular Python → slow!
3.  Register a NumPy version for the built-in Numba function.
4.  Using the NumPy version.

## Hyperfast rolling metrics \[#hyperfast-rolling-metrics]

New in 1.0.0

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

```python title="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)  # (1)
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)  # (2)
8.12 ms ± 199 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

1.  Using QuantStats.
2.  Using VBT.
