Tutorials

From Python to Rust

Take one strategy from vectorized Python to live, stateful Rust

A backtest often grows up with its strategy. It starts as a few expressive lines in a notebook, turns into a collection of typed arrays once the rules settle, and may eventually end up in a service where Python is nowhere to be seen. VBT supports each stage. Think of its abstractions as nesting dolls: open one layer and the same array contract appears inside the next one 🪆

In this tutorial, we will backtest one strategy through four interfaces:

InterfaceBest suited forResult
High-level VBTResearch and analysisFeature-rich portfolio
Numba kernelsOptimizing and customizing Python codeRaw simulation output
Rust from Python (PyO3)Accelerating Python codeRaw simulation output
Native RustServices, CLIs, and embedded systemsRaw simulation output

We will keep the data, rules, execution assumptions, and metrics fixed. Changing the strategy and the interface at the same time would tell us very little. We want to see where each layer begins, what it adds, and what it costs.

Idea

Most introductory backtests use a moving-average crossover. It is a fine first example, but it hides many details that matter in practice: several indicators must agree, signals become known only after a bar has closed, orders compete for shared cash, and stops must make assumptions about the unknown path inside an OHLC candle.

Here we will build a volatility-squeeze breakout strategy over liquid NASDAQ stocks:

  1. Pull hourly OHLCV data for NVDA, TSLA, AAPL, and AMZN from TradingView.
  2. Measure compression with Bollinger Band width.
  3. Require trend activity with ADX.
  4. Require participation with volume above its rolling mean.
  5. Enter when price breaks out of the Bollinger envelope after a squeeze.
  6. Exit on a middle-band cross or on ATR-based stop-loss / take-profit levels.
  7. Simulate all symbols in one cash-sharing group.
  8. Compare return, drawdown, risk-adjusted return, trade quality, and order counts.

This is still an educational strategy, but it has enough moving parts to expose meaningful differences among the interfaces.

TradingView candles are only a source of historical prices here. Short signals in a backtest do not mean the stock was actually available to short. A live implementation would need a margin account, borrow availability, and the associated costs, none of which are modeled here.

Data

Let's pull hourly candles for four NASDAQ heavyweights using VBT's TVData class and its inherited Data.pull method. TradingView serves the most recent history for each symbol and timeframe, so we cannot pin an exact date range. Instead, we let VBT cache the first pull: every later run reads the same candles from disk, which is exactly the frozen input a cross-implementation comparison needs.

from vectorbtpro import *

SYMBOLS = [
    "NASDAQ:NVDA",
    "NASDAQ:TSLA",
    "NASDAQ:AAPL",
    "NASDAQ:AMZN"
]
TIMEFRAME = "1h"

data = vbt.TVData.pull(
    SYMBOLS,
    timeframe=TIMEFRAME,
    silence_warnings=True,
    cache=True,  # (1)!
    cache_kwargs=dict(cache_dir="temp")
)

data.close
symbol                     NASDAQ:NVDA  NASDAQ:TSLA  NASDAQ:AAPL  NASDAQ:AMZN
datetime
2020-01-02 14:30:00+00:00      5.94375    28.264638      74.3675      93.4400
2020-01-02 15:30:00+00:00      5.94375    28.417305      74.5275      93.4960
2020-01-02 16:30:00+00:00      5.95675    28.431305      74.5775      93.7385
...                                ...          ...          ...          ...
2026-07-14 17:30:00+00:00    211.75000   396.385000     315.2100     247.4000
2026-07-14 18:30:00+00:00    211.26000   396.610000     315.7100     247.8300
2026-07-14 19:30:00+00:00    211.82000   396.080000     314.9900     247.5000

[11452 rows x 4 columns]
Symbol 4/4
  1. Cache the result in VBT's local LMDB cache, here placed in the temp directory. The same symbols requested again with the same pull arguments are loaded from disk instead of TradingView.

TradingView returned roughly six and a half years of hourly candles per symbol. VBT aligned them on a common timezone-aware index of 11452 rows, which is plenty of history for a strategy that trades only a few times per month and symbol.

TVData serves a moving window, so a pull made today will not match the one cached for this tutorial (11452 rows ending 2026-07-14 19:30 UTC). Every number printed from here on comes from that one snapshot, and yours will differ. What should reproduce on your machine is the agreement the Comparison section asserts, not the values the four implementations agree on.

Data caching has its own small lifecycle. Pass refresh_cache=True to fetch and replace a cached result, clear_cache=True to remove it, or use cache_kwargs to choose the cache directory, database name, compression, and other settings. See the data caching recipes for the full set of controls.

Backtesting code often grows up with its strategy. It starts as a few lines in a notebook, and it can end up as a service with no Python in it at all. This three-part tutorial takes one volatility-squeeze breakout along that path. The data and the rules never change, so the only difference between the versions is how they are written.

✅ Learn how to write the same strategy four times, from short research code down to a standalone program that runs without Python. All four are then checked against each other, order for order.

✅ Some rules cannot be prepared in advance, because they depend on what the strategy just did. Learn where a rule like that belongs, and see it produce the one result in this tutorial that could not have been worked out before the simulation ran.

✅ Finally, learn how to carry one portfolio forward as new bars arrive, instead of rerunning the whole backtest each time, and how a Rust process picks up where it left off after a restart 🔄

Topics

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.