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:
| Interface | Best suited for | Result |
|---|---|---|
| High-level VBT | Research and analysis | Feature-rich portfolio |
| Numba kernels | Optimizing and customizing Python code | Raw simulation output |
| Rust from Python (PyO3) | Accelerating Python code | Raw simulation output |
| Native Rust | Services, CLIs, and embedded systems | Raw 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:
- Pull hourly OHLCV data for
NVDA,TSLA,AAPL, andAMZNfrom TradingView. - Measure compression with Bollinger Band width.
- Require trend activity with ADX.
- Require participation with volume above its rolling mean.
- Enter when price breaks out of the Bollinger envelope after a squeeze.
- Exit on a middle-band cross or on ATR-based stop-loss / take-profit levels.
- Simulate all symbols in one cash-sharing group.
- 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.closesymbol 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]- Cache the result in VBT's local LMDB cache, here placed in the
tempdirectory. 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
Static simulation
Learn how to backtest the same strategy with high-level VBT, Numba, PyO3 Rust, and native Rust
Dynamic simulation
Learn how to implement stateful portfolio callbacks and stream the same strategy in native Rust
Live simulation
Learn how to continue a path-dependent simulation as new bars arrive and preserve it across restarts
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.