Features

Rust engine

Run simulations on the native Rust engine and write strategy callbacks in Rust

Bring Rust speed to your Python research, or write the entire strategy in Rust. VBT PRO ships a native Rust backtesting engine for both workflows. Python users get precompiled calculations through the optional extension. Rust users can also write stateful strategy callbacks and run independent simulations across CPU cores.

Choose your route to Rust

What you want to doWhere to start
Speed up supported calculations in an existing Python notebook or scriptInstall the optional extension and keep using the Python API.
Keep custom trading rules written in PythonUse Numba callbacks, with Rust available for supported calculations elsewhere in the pipeline.
Write a Rust strategy that reacts to fills, cash, or positionsUse the native simulators and give the strategy its own state.
Process new bars as they arrive in a Rust programUse a streaming stepper to carry the portfolio and strategy state forward.

The Python extension and native Rust library share calculation code. You can adopt the extension without learning Rust, and use the native library to build standalone Rust strategies. Native Rust programs can use the library without a Python runtime.

A stateful strategy in Python and Rust

The same strategy below runs twice: once as Numba callbacks through the Python API, once as a Rust strategy. It trades 100 synthetic assets over 50,000 hourly bars, enters on a moving average crossover, exits on the opposite cross or a 3% stop loss or 6% take profit, and waits 24 bars after any stop before entering that asset again. Whether that wait applies depends on fills the simulation itself produces, so it cannot be precomputed as an array.

Write the cooldown as Numba callbacks and run it through the Python API
from collections import namedtuple

COOLDOWN_BARS = 24
np.random.seed(42)
returns = np.random.normal(0, 0.01, size=(50_000, 100))
close = pd.DataFrame(100 * np.exp(returns.cumsum(axis=0)))
fast = close.rolling(20).mean()
slow = close.rolling(100).mean()
entries = fast.vbt.crossed_above(slow)
exits = fast.vbt.crossed_below(slow)
Memory = namedtuple("Memory", ["blocked_until"])

@njit
def cooldown_signal_func_nb(c, entries, exits, memory):
    cooling = c.i < memory.blocked_until[c.col]
    entry = vbt.nb.flex_select_nb(entries, c.i, c.col) and not cooling
    exit = vbt.nb.flex_select_nb(exits, c.i, c.col)
    return entry, exit, False, False

@njit
def cooldown_post_order_func_nb(c, memory):
    if vbt.pf_nb.order_closed_position_nb(c):  
        if vbt.pf_nb.get_last_order_nb(c)["stop_type"] >= 0:
            memory.blocked_until[c.col] = c.i + 1 + COOLDOWN_BARS

memory = Memory(blocked_until=np.zeros(close.shape[1], dtype=int_))
pf = vbt.PF.from_signals(
    close,
    signal_func_nb=cooldown_signal_func_nb,
    signal_args=(vbt.Rep("entries"), vbt.Rep("exits"), memory),
    broadcast_named_args=dict(entries=entries, exits=exits),
    post_order_func_nb=cooldown_post_order_func_nb,
    post_order_args=(memory,),
    size=1.0,
    fees=0.001,
    sl_stop=0.03,
    tp_stop=0.06,
)
Write the same cooldown as a Rust strategy
const COOLDOWN_BARS: usize = 24;

struct Cooldown<'a> {
    entries: ArrayView2<'a, bool>,
    exits: ArrayView2<'a, bool>,
    blocked_until: Vec<usize>, 
}

impl SignalStrategy for Cooldown<'_> {
    fn signals(&mut self, ctx: &mut SignalContext<'_, '_>) -> VbtResult<Signals> {
        let cell = [ctx.i(), ctx.col()];
        let cooling = ctx.i() < self.blocked_until[ctx.local_col()];
        Ok(Signals {
            long_entry: self.entries[cell] && !cooling,
            long_exit: self.exits[cell],
            ..Signals::default()
        })
    }

    fn on_order_end(
        &mut self,
        ctx: &mut OrderEndContext<'_, '_, SignalOrderRecord>,
        report: &ExecutionReport,
    ) -> VbtResult<()> {
        if report.before.position != 0.0 && report.after.position == 0.0 {
            let record = ctx.order_record_now().expect("a close emits a record");
            if record.stop_type >= 0 {
                self.blocked_until[ctx.local_col()] = ctx.i() + 1 + COOLDOWN_BARS;
            }
        }
        Ok(())
    }
}
Run it on one core and on all cores
let simulator = SignalSimulator::builder()
    .config(
        SimulationConfig::builder()
            .target_shape(close.dim())
            .group_lens(group_lens.view())
            .close(close.view())
            .build()?,
    )
    .signal_config(
        SignalConfig::builder().size(1.0).fees(0.001).sl_stop(0.03).tp_stop(0.06).build(),
    )
    .build()?;
let new_cooldown = |info: GroupInfo| {
    Ok(Cooldown {
        entries: entries.view(),
        exits: exits.view(),
        blocked_until: vec![0; info.to_col - info.from_col],
    })
};
let serial = simulator.run(new_cooldown)?; 
let parallel = simulator.run_parallel(new_cooldown)?;

Both versions produced the same 65,278 orders, matching on every column, bar, size, and price. Measured on an Apple M3 with 8 cores, best of five runs:

RunMilliseconds
Numba callbacks through vbt.PF.from_signals1,129
Rust strategy, one core538
Rust strategy, all cores201

The Python time includes argument preparation and portfolio construction, and the Rust times cover the simulation alone. The table therefore measures the two workflows at different levels. The research script that produced it builds the Rust program, runs both versions, and compares their orders.

Drop-in Rust kernels

Python users get the Rust engine without writing Rust. With the optional vectorbtpro-rust package installed, supported functions such as rolling statistics, signal helpers, return metrics, and the from_signals and from_orders simulators run as precompiled Rust kernels, and fall back to Numba for anything Rust does not cover. Install it from the platform wheels of the matching release before installing vectorbtpro, as described in the installation guide. VBT uses it only when its version matches exactly. jitted=dict(jitter="rs", parallel=True) spreads supported calculations across cores, over columns or independent portfolio groups.

Because the kernels are compiled ahead of time, the first call in a session does not wait for Numba. On the machine that built this page, the first call of a rolling standard deviation in a fresh process took about 0.08 seconds with Rust. Numba took 1.3 seconds the first time it saw those inputs, since it compiled the function, and about 0.17 seconds in later processes, which load the compiled version from disk. Later calls in the same process took under a millisecond either way, so short scripts, scheduled jobs, and new worker processes gain the most. The Compute backends page shows when Rust wins and how to choose a backend per call, and the Rust backend highlight below shows the controls.

Native simulators

The same simulators are a Rust library. A Rust program can run a whole history at once with run_single, run, or run_parallel, or feed bars one at a time to a stepper such as SignalStepper and receive each bar's fills as they happen, with checkpoints to save and restore its state. You can replay a history and then continue as new bars arrive, keeping cash, positions, and strategy memory such as the cooldown above. See live simulation for this workflow.

Batch and streaming runs can be compared with ensure_simulation_eq, which checks records and terminal portfolio state and reports the first mismatch. This gives you a way to check that moving a strategy from a full-history run to a bar-by-bar feed preserves its simulated trades. The crate's test suite compares its simulators with the Numba ones, as the example above did with orders.

Rust callbacks

Strategies in Rust are types that implement a trait, such as SignalStrategy for signals, OrderStrategy for one order per element, and FlexOrderStrategy for any number of orders per bar. Their methods receive a context with the current bar, column, cash, and positions, and an execution report after each order. Other native engines take callbacks for signal generation, apply and reduce operations, and portfolio allocation.

Each parallel simulation group gets its own strategy instance. A group can contain several assets that share cash, so you can keep portfolio-level decisions together while running independent portfolios in parallel. Rows and callbacks within each group still run in order.

Rust callbacks run inside Rust programs. From Python, strategy logic is written as Numba callbacks like the ones above, which run on the Numba simulators. Errors in Rust carry a category, such as an invalid value or a rejected order, and surface in Python as the matching exception type.

Bring Rust results back to Python

A native Rust simulation can save its output to a NumPy-compatible archive with save_npz. Load it with vbt.load_simulation_npz and build a Portfolio using the same prices, labels, initial cash, and grouping as the simulation. You can then use VBT's performance analysis and trade analytics on the Rust results. This lets you run the simulation in Rust and keep your Python analysis and reporting workflow.

Tutorial

The members-only From Python to Rust tutorial builds one strategy with the high-level API, Numba kernels, Python bindings, and native Rust, then moves it to streaming.

Native Rust simulators

✅ 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

The examples below share the strategy definition and require vectorbtpro-rust and ndarray = "0.16". See the Rust setup guide for installation.

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.0
        } else if price >= 110.0 && position > 0.0 {
            -position
        } else {
            return Ok(None);
        };
        Ok(Some(Order::builder().size(size).build()))
    })
}
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(())
}
row 0: 1 share at $100
row 3: 1 share at $112

Tutorial

Learn more in the From Python to Rust tutorial.

Rust backend

✅ 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.

Keep your workflow, get the Rust lane
data = vbt.YFData.pull("BTC-USD", start="2024")

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

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

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.