教程
从 Python 到 Rust
将同一个策略从向量化 Python 推进到有状态的 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
vectorbtpro.data.custom.tv.TVData class and its inherited vectorbtpro.data.base.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.
vectorbtpro.data.custom.tv.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.
Explicit persistence still has its place. Use the inherited
vectorbtpro.utils.pickling.Pickleable.save to preserve the complete object, or
vectorbtpro.data.base.Data.to_hdf when you need a named artifact to archive, inspect, or pass to
another Python process. For a regular data pull that should simply survive notebook restarts,
cache=True is the shorter path.
Rules
We keep the rules in one small configuration block. This block becomes the contract between all four implementations:
回测代码往往与策略一起成长。它可能最初只是 notebook 中的几行代码,最终却成为完全不含 Python 的服务。这套三部分教程将带领同一个波动率收缩突破策略走完整个过程。数据和规则始终不变,因此各版本之间唯一的区别在于实现方式。
✅ 学习用四种方式编写同一个策略,从简短的研究代码,一直到无需 Python 即可运行的独立程序。随后,逐笔核对四种实现产生的订单。
✅ 有些规则无法提前准备,因为它们取决于策略刚刚执行的操作。您将了解这类规则应该放在哪个环节,并看到它产生本教程中唯一一个在模拟运行之前无法预先算出的结果。
✅ 最后,学习在新 K 线到来时继续推进同一个投资组合,而不是每次重新运行完整回测;并了解 Rust 进程如何在重启后从上次的位置继续运行 🔄
会员主题(英文内容)
版权所有 © 2021–2026 Oleg Polakow。保留所有权利。
本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。