The vectorized paradigm: Arrays over event loops

Traditional backtesting frameworks evaluate trading strategies sequentially. An event loop iterates bar by bar through historical timestamps (from bar 0 to bar N), maintaining stateful objects for current cash, pending orders, and open positions. While intuitive and structurally aligned with live execution, event loops in pure Python incur significant interpreter overhead on large datasets.

VectorBT approaches strategy simulation from a completely different direction: array manipulation. Instead of processing bars serially, VectorBT represents historical price series, technical indicators, and trade signals as contiguous multidimensional NumPy arrays managed through pandas interfaces.

Under the hood, VectorBT compiles heavy numerical operations using Numba just-in-time (JIT) compilation. Rather than looping over rows, Numba compiles array traversals directly into optimized machine code.

Code
Event-Driven Loop:
Bar 0 -> Evaluate Logic -> Generate Order -> Update State
Bar 1 -> Evaluate Logic -> Generate Order -> Update State
...
Bar N -> Evaluate Logic -> Generate Order -> Update State

Vectorized Array Broadcasting:
Price Array:     [100.2, 101.5, 100.8, 102.3, 103.1]
Fast MA (10):    [100.0, 100.4, 100.7, 101.1, 101.5]
Slow MA (30):    [ 99.2,  99.5,  99.7, 100.0, 100.3]
Signal Matrix:   [False, False,  True,  True, False] -> Compiled Portfolio Fill

The core advantage of vectorization lies in parameter broadcasting. In an event-driven engine, testing 50 moving average combinations requires running 50 independent passes through the dataset. In VectorBT, parameter combinations are broadcast across columns in a 2D or 3D matrix. A single function call computes thousands of indicator variants and portfolio trajectories simultaneously.

However, array broadcasting introduces architectural trade-offs:

  • Memory scaling: Storing multidimensional parameter grids in memory scales with the product of bar count and parameter combinations. Running multi-parameter optimizations across high-frequency tick or minute data can easily exhaust available RAM.
  • Path-dependent logic constraints: Complex execution models—such as trailing stops updated on tick highs, contingent One-Cancels-Other (OCO) brackets, or partial fills—do not map cleanly to pure array operations and require specialized iterative Numba kernels.

End-to-end VectorBT workflow

Constructing a reliable backtest in VectorBT involves five distinct stages, from data hygiene to performance attribution.

Step 1: Data preparation with OHLC prices

Vectorized calculations require clean, monotonically increasing time series. Incomplete bars, misaligned timezone offsets, or duplicate timestamps will corrupt vectorized shift operations.

Data preparation check
import pandas as pd

# Ensure DatetimeIndex is sorted and timezone-consistent
data = data.sort_index()
assert data.index.is_monotonic_increasing, "Index must be monotonically increasing"
assert not data.index.has_duplicates, "Timestamps must be unique"

Step 2: Generating moving average crossover indicators

VectorBT provides prebuilt indicator classes through vbt.MA. When computing multiple window sizes, VectorBT automatically constructs multi-index DataFrames where each column represents a specific parameter combination.

Compute indicators
import vectorbt as vbt

fast_ma = vbt.MA.run(data["Close"], window=10)
slow_ma = vbt.MA.run(data["Close"], window=30)

Step 3: Signal generation and the essential .shift(1) rule

A moving average crossover condition generates boolean masks identifying where the fast line crosses above or below the slow line:

Raw crossover signals
raw_entries = fast_ma.ma_crossed_above(slow_ma)
raw_exits = fast_ma.ma_crossed_below(slow_ma)

[!WARNING] The Lookahead Hazard: A crossover calculated using the Close of bar t cannot be executed at the Close of bar t. In real-world trading, bar t's close is unknown until the interval has fully expired. If your simulation buys at bar t's close based on a signal triggered by that same close, your strategy incorporates invisible lookahead bias.

To maintain temporal causality, the signal must be delayed by at least one period before execution. If you evaluate signals at the close of day t, your earliest feasible execution occurs at the Open of day t + 1:

Applying the causality shift
# Shift signals by 1 bar so trade executes on the next bar's price
entries = raw_entries.shift(1).fillna(False)
exits = raw_exits.shift(1).fillna(False)

Step 4: Running vbt.Portfolio.from_signals() with fees and slippage

The vbt.Portfolio.from_signals() constructor translates boolean arrays into a simulated equity curve. To produce realistic estimates, explicit friction parameters must be specified:

  • price: Specify the execution series explicitly (such as data["Open"]) to match your shifted signals.
  • fees: Proportional commission per trade (e.g., 0.001 for 10 basis points).
  • slippage: Execution slippage penalty (e.g., 0.0005 for 5 basis points).
  • freq: Bar frequency string ("1D") to annualize risk metrics accurately.
Portfolio simulation call
portfolio = vbt.Portfolio.from_signals(
    close=data["Open"],  # Execute at next-bar Open
    entries=entries,
    exits=exits,
    init_cash=10000.0,
    fees=0.001,          # 0.10% commission per trade
    slippage=0.0005,     # 0.05% slippage
    freq="1D",
)

Step 5: Analyzing returns, Sharpe ratio, and drawdowns

VectorBT computes industry-standard performance metrics directly on the compiled portfolio state:

Extracting core metrics
stats = portfolio.stats()
print(stats[["Total Return [%]", "Benchmark Return [%]", "Sharpe Ratio", "Max Drawdown [%]"]])

Complete runnable code example

Below is a self-contained, reproducible Python script demonstrating a complete moving average crossover backtest with realistic cost assumptions and causal signal shifting.

vectorbt_ma_crossover.py
import numpy as np
import pandas as pd
import vectorbt as vbt

# ---------------------------------------------------------
# 1. Generate Synthetic Daily OHLC Data
# ---------------------------------------------------------
np.random.seed(42)
n_bars = 500
dates = pd.date_range("2024-01-01", periods=n_bars, freq="B")

# Generate log returns via Geometric Brownian Motion
daily_drift = 0.0004
daily_vol = 0.015
shocks = np.random.normal(daily_drift, daily_vol, n_bars)
price_series = 100.0 * np.exp(np.cumsum(shocks))

close_price = pd.Series(price_series, index=dates, name="Close")
open_price = close_price.shift(1).fillna(close_price.iloc[0]) * (1 + np.random.normal(0, 0.001, n_bars))
high_price = np.maximum(open_price, close_price) * (1 + np.abs(np.random.normal(0, 0.004, n_bars)))
low_price = np.minimum(open_price, close_price) * (1 - np.abs(np.random.normal(0, 0.004, n_bars)))

ohlc = pd.DataFrame({
    "Open": open_price,
    "High": high_price,
    "Low": low_price,
    "Close": close_price,
})

# ---------------------------------------------------------
# 2. Compute Moving Averages
# ---------------------------------------------------------
fast_window = 10
slow_window = 30

fast_ma = vbt.MA.run(ohlc["Close"], window=fast_window)
slow_ma = vbt.MA.run(ohlc["Close"], window=slow_window)

# ---------------------------------------------------------
# 3. Generate Signals with Causal Execution Shift
# ---------------------------------------------------------
# Fast MA crosses above Slow MA -> Buy
raw_entries = fast_ma.ma_crossed_above(slow_ma)
# Fast MA crosses below Slow MA -> Sell / Exit
raw_exits = fast_ma.ma_crossed_below(slow_ma)

# Essential: Shift by 1 bar to trade at the Open of t + 1
entries = raw_entries.shift(1).fillna(False)
exits = raw_exits.shift(1).fillna(False)

# ---------------------------------------------------------
# 4. Execute Portfolio Simulation
# ---------------------------------------------------------
portfolio = vbt.Portfolio.from_signals(
    close=ohlc["Open"],   # Order fills at bar t+1 Open
    entries=entries,
    exits=exits,
    init_cash=10_000.0,   # $10,000 initial capital
    fees=0.001,           # 0.1% transaction fee per fill
    slippage=0.0005,      # 0.05% (5 bps) slippage penalty
    freq="1D",
)

# ---------------------------------------------------------
# 5. Inspect Strategy Performance
# ---------------------------------------------------------
stats = portfolio.stats()
print("=== VectorBT Strategy Summary ===")
print(f"Total Return:       {stats['Total Return [%]']:.2f}%")
print(f"Benchmark Return:   {stats['Benchmark Return [%]']:.2f}%")
print(f"Sharpe Ratio:       {stats['Sharpe Ratio']:.2f}")
print(f"Max Drawdown:       {stats['Max Drawdown [%]']:.2f}%")
print(f"Total Trades:       {stats['Total Trades']}")
print(f"Win Rate:           {stats['Win Rate [%]']:.2f}%")

Common pitfalls in vectorized backtesting

While vectorized frameworks provide remarkable computational speed for parameter screening, their structural abstraction can mask critical execution flaws.

Failure ModeMechanism in Vectorized EnginesReal-World ImpactVerification Checklist
Lookahead LeakageSignal at bar t fills at bar t's Close or uses full-series normalization.Dramatically inflated Sharpe ratios; strategy fails immediately in live validation.Always apply .shift(1) to signals and calculate rolling Z-scores strictly with past windows.
Intrabar AmbiguityBoth profit target and stop-loss levels are crossed within the same daily High-Low span.Simulator assumes the favorable exit was hit first, understating drawdowns.Use sub-bar resolution (e.g. 5-minute bars) or strict conservative fill assumptions.
Order State OmissionTrailing stops or OCO orders are simplified into static boolean matrix masks.Inability to track dynamic high-water marks or contingent cancel-replace states.Cross-check complex orders in a dedicated bar-by-bar state machine.

1. Lookahead leakage during signal generation

In an array-based calculation, it is easy to accidentally reference future values. Common culprits include:

  • Calculating indicators using centered rolling windows (center=True).
  • Normalizing inputs using global dataset statistics (e.g., (df - df.mean()) / df.std()) rather than expanding or rolling historical statistics.
  • Omitting the temporal shift between signal confirmation and execution price.

2. Intrabar price path ambiguity

Vectorized engines evaluate bars as atomic summary rectangles: Open, High, Low, and Close. If a strategy sets a stop-loss at -2% and a take-profit at +3%, and a single daily bar exhibits a +4% High and a -3% Low, a 2D matrix calculation cannot determine which boundary was crossed first. In reality, hitting the stop-loss first terminates the position at a loss; an overly optimistic vectorized engine may credit the trade with a profit.

3. Path-dependent order lifecycles

In real-world trading, orders undergo complex state transitions: Submitted → Accepted → Partially Filled → Cancelled or Filled. Trailing stop orders require continuous price tracking against an evolving high-water mark. Because array operations apply uniform formulas across static axes, modeling dynamic, path-dependent position rules requires writing custom Numba loops, sacrificing the clean syntax of vectorized expressions.


Vectorized screening vs. Bar-by-bar execution: When to use which

Rather than viewing vectorized backtesting and bar-by-bar simulation as competing methodologies, quantitative research desks treat them as complementary phases of an integrated research pipeline.

Evaluation DimensionVectorized Screening (VectorBT)Bar-by-Bar Event Engine (OpenBacktesting)
Primary Use CaseHypothesis screening, parameter space exploration, factor rankingProduction validation, execution auditability, order lifecycle simulation
Execution MechanicsArray broadcasting via NumPy and NumbaExplicit bar-by-bar state machine with order queues
Memory FootprintScales with parameter combinations; can be RAM-intensiveMinimal; streams one bar at a time through the engine
Supported Order TypesSignal-based target allocations, basic stopsMarket, Limit, Stop, Stop-Limit, Trailing Stop, OCO brackets
Causality EnforcementManual responsibility (requires explicit .shift(1))Enforced by design; orders submitted on bar t evaluate on bar t + 1
Execution FrictionPercentage fees and proportional slippageCustomizable commission tiers and price slippage models
Result VerificationAggregate metrics, summary DataFramesInspectable trade logs, SQLite trial storage, interactive Studio
Code
Quantitative Strategy Development Funnel:

┌────────────────────────────────────────────────────────┐
│  Stage 1: Vectorized Screening (VectorBT)              │
│  - Sweep 10,000+ parameter combinations                │
│  - Filter out unviable parameter spaces                │
│  - Fast exploration of signal hypotheses               │
└──────────────────────────┬─────────────────────────────┘
                           │ (Top candidate regimes)
                           ▼
┌────────────────────────────────────────────────────────┐
│  Stage 2: Bar-by-Bar Deterministic Audit (OpenBacktesting)│
│  - Enforce explicit order state machines (Limit, OCO)  │
│  - Validate exact cash balances and commission models  │
│  - Inspect trade logs and drawdown paths in Studio    │
└────────────────────────────────────────────────────────┘

When screening thousands of moving average windows or factor weightings, VectorBT's multi-column broadcasting provides rapid feedback. However, once a strategy hypothesis shows statistical promise, validating it inside a causal bar-by-bar engine like OpenBacktesting protects against hidden lookahead leakage, unmodeled order rejections, and execution ambiguities.


Conclusion and further reading

VectorBT represents a significant evolution in quantitative research velocity, enabling practitioners to evaluate array-based strategies at compile-time speeds. However, raw computational speed must be matched with strict methodological discipline:

  1. Always delay signals: Enforce a mandatory .shift(1) rule to ensure decisions made at bar t's close execute no earlier than bar t + 1's open.
  2. Account for transaction friction: Always specify realistic fees and slippage parameters; zero-cost backtests produce deceptive equity curves.
  3. Cross-validate candidate strategies: Transition your top-performing candidate parameters from vectorized screening into a deterministic bar-by-bar framework to audit trade logs and order lifecycles before allocating capital.

For deeper dives into reproducible backtesting methodology and strategy inspection: