High-performance vectorized backtesting with parameter optimization, portfolio simulation, and rich performance metrics
vectorbt is a Python library for vectorized backtesting — running strategy simulations using NumPy/pandas array operations instead of bar-by-bar loops. This makes it 100–1000x faster than event-driven frameworks (backtrader, zipline), enabling parameter optimization across thousands of combinations in seconds.
Key strengths:
uv pip install vectorbt pandas numpy
vectorbt pulls in pandas, NumPy, and Plotly automatically. For technical indicators, also install pandas-ta:
uv pip install vectorbt pandas-ta
Strategies in vectorbt are expressed as boolean pandas Series (or arrays) indicating where to enter and exit positions:
import vectorbt as vbt
import pandas as pd
# Entry: buy when fast EMA crosses above slow EMA
entries = fast_ema > slow_ema
# Exit: sell when fast EMA crosses below slow EMA
exits = fast_ema < slow_ema
vectorbt resolves conflicting signals automatically (you can't enter while already in a position).
vbt.Portfolio.from_signals() is the primary backtesting function. It takes price data and entry/exit signals, simulates trades, and computes performance:
pf = vbt.Portfolio.from_signals(
close=close_prices,
entries=entries,
exits=exits,
init_cash=10_000,
fees=0.003, # 0.3% per trade
slippage=0.005, # 0.5% slippage
freq="1h", # hourly data
)
# Full stats summary
print(pf.stats())
# Individual metrics
print(f"Total Return: {pf.total_return():.2%}")
print(f"Sharpe Ratio: {pf.sharpe_ratio():.3f}")
print(f"Max Drawdown: {pf.max_drawdown():.2%}")
print(f"Win Rate: {pf.trades.win_rate():.2%}")
Pass arrays instead of scalars to test many parameter combos simultaneously:
import numpy as np
fast_periods = np.arange(5, 25, 2) # 10 values
slow_periods = np.arange(20, 60, 5) # 8 values
fast_ma = vbt.MA.run(close, fast_periods, short_name="fast")
slow_ma = vbt.MA.run(close, slow_periods, short_name="slow")
# This creates 80 parameter combinations automatically
entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)
import pandas as pd
# From CSV
df = pd.read_csv("ohlcv.csv", parse_dates=["timestamp"], index_col="timestamp")
close = df["close"]
# From Yahoo Finance (traditional markets)
btc = vbt.YFData.download("BTC-USD", start="2023-01-01", end="2025-01-01")
close = btc.get("Close")
For Solana tokens, fetch data via the birdeye-api skill and load into a DataFrame.
import pandas_ta as ta
# Using pandas-ta (see pandas-ta skill)
df.ta.ema(length=12, append=True)
df.ta.ema(length=26, append=True)
df.ta.rsi(length=14, append=True)
df.ta.bbands(length=20, std=2, append=True)
# Or using vectorbt built-ins
rsi = vbt.RSI.run(close, window=14)
bbands = vbt.BBANDS.run(close, window=20, alpha=2)
# EMA crossover
entries = df["EMA_12"] > df["EMA_26"]
exits = df["EMA_12"] < df["EMA_26"]
# RSI mean reversion
entries = rsi.rsi_below(30)
exits = rsi.rsi_above(70)
pf = vbt.Portfolio.from_signals(
close=close,
entries=entries,
exits=exits,
init_cash=10_000,
fees=0.003,
slippage=0.005,
size=0.95, # use 95% of available cash
size_type="percent",
freq="1h",
)
# Summary statistics
print(pf.stats())
# Trade-level analysis
trades = pf.trades.records_readable
print(f"\nTrade count: {len(trades)}")
print(f"Avg holding period: {trades['Duration'].mean()}")
# Equity curve
pf.plot().show()
# Drawdown chart
pf.drawdowns.plot().show()
| Parameter | Description | Example |
|---|---|---|
close | Price series (pd.Series or DataFrame) | df["close"] |
entries | Boolean entry signals | fast > slow |
exits | Boolean exit signals | fast < slow |
init_cash | Starting capital | 10_000 |
fees | Fee per trade (fraction) | 0.003 (0.3%) |
slippage | Slippage per trade (fraction) | 0.005 (0.5%) |
size | Position size | 0.95 |
size_type | How to interpret size | "percent", "amount", "value" |
freq | Data frequency | "1h", "4h", "1d" |
direction | Trade direction | "both", "longonly", "shortonly" |
accumulate | Allow adding to positions | False |
sl_stop | Stop-loss level (fraction) | 0.05 (5%) |
tp_stop | Take-profit level (fraction) | 0.10 (10%) |
total_return() — cumulative return over the periodannualized_return() — annualized compound returndaily_returns() — Series of daily returnsmax_drawdown() — maximum peak-to-trough declineannualized_volatility() — annualized standard deviation of returnsvalue_at_risk() — VaR at specified confidence levelsharpe_ratio() — excess return per unit volatilitysortino_ratio() — excess return per unit downside deviationcalmar_ratio() — annualized return / max drawdownomega_ratio() — probability-weighted gain/loss ratiotrades.win_rate() — fraction of profitable tradestrades.profit_factor() — gross profit / gross losstrades.expectancy() — average P&L per tradetrades.avg_winning_trade() — mean profit on winnerstrades.avg_losing_trade() — mean loss on loserstrades.count() — total number of completed tradesfast_windows = [5, 8, 12, 15, 20]
slow_windows = [20, 26, 30, 40, 50]
# Run all 25 combos at once
fast_ma = vbt.MA.run(close, fast_windows, short_name="fast")
slow_ma = vbt.MA.run(close, slow_windows, short_name="slow")
entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)
pf = vbt.Portfolio.from_signals(close, entries, exits, fees=0.003)
# Find best params by Sharpe
sharpe = pf.sharpe_ratio()
best_idx = sharpe.idxmax()
print(f"Best params: {best_idx}, Sharpe: {sharpe[best_idx]:.3f}")
Always validate optimized parameters on out-of-sample data:
# Split: 70% train, 30% test
split_idx = int(len(close) * 0.7)
train_close = close.iloc[:split_idx]
test_close = close.iloc[split_idx:]
# Optimize on training data
# ... (run grid search on train_close)
# Validate best params on test data
# ... (run single backtest on test_close with best params)
See references/optimization_guide.md for detailed walk-forward methodology and overfitting prevention.
Crypto markets never close. Use hourly or minute-based frequencies, not business-day frequencies:
# Correct for crypto
pf = vbt.Portfolio.from_signals(close, entries, exits, freq="1h")
# Wrong — business days assume market closures
# pf = vbt.Portfolio.from_signals(close, entries, exits, freq="1B")
DEX swaps on Solana typically cost 0.25–1% including AMM fees. CEX spot fees are 0.05–0.1%.
# Solana DEX (conservative)
pf = vbt.Portfolio.from_signals(close, entries, exits, fees=0.005)
# CEX spot
pf = vbt.Portfolio.from_signals(close, entries, exits, fees=0.001)
Low-liquidity tokens can have 1–5% slippage. Always model this:
# High-liquidity (SOL, ETH): 0.1–0.5%
pf = vbt.Portfolio.from_signals(close, entries, exits, slippage=0.003)
# Low-liquidity memecoins: 1–3%
pf = vbt.Portfolio.from_signals(close, entries, exits, slippage=0.02)
Many tokens have less than 1 year of data. Be cautious about annualizing metrics from short samples.
fast = vbt.MA.run(close, 12, short_name="fast")
slow = vbt.MA.run(close, 26, short_name="slow")
entries = fast.ma_crossed_above(slow)
exits = fast.ma_crossed_below(slow)
rsi = vbt.RSI.run(close, 14)
entries = rsi.rsi_crossed_below(30)
exits = rsi.rsi_crossed_above(70)
bb = vbt.BBANDS.run(close, window=20, alpha=2)
entries = close > bb.upper
exits = close < bb.lower
pf = vbt.Portfolio.from_signals(
close, entries, exits,
sl_stop=0.05, # 5% stop-loss
tp_stop=0.10, # 10% take-profit
)
references/api_guide.md — Complete vectorbt API reference for Portfolio, indicators, plotting, and data loadingreferences/optimization_guide.md — Grid search, walk-forward validation, overfitting prevention, and optimization best practicesscripts/backtest_example.py — Three-strategy backtest comparison using synthetic data (EMA crossover, RSI mean reversion, Bollinger breakout)scripts/parameter_sweep.py — EMA crossover parameter grid search with walk-forward validation