教程

从 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:

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

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 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。