Skip to main content

backtesting framework - vectorbt for fast parameter testing at scale

vectorbt = game changer for parameter testing.

10x faster than backtrader.

vectorized operations instead of event-driven.

the speed problem
#

traditional backtesting:

backtrader loops through each bar.

processes events sequentially.

slow for parameter optimization.

my experience:

256 parameter combinations × 10 windows = 2,560 backtests.

backtrader: 45 minutes.

too slow for rapid iteration.

learned about vectorbt from NexusFi discussions on high-performance backtesting frameworks for algorithmic trading.

vectorbt approach
#

vectorized operations:

processes entire dataset at once using numpy.

parallel parameter testing.

same 2,560 backtests:

vectorbt: 3.5 minutes.

13x faster than backtrader.

installation and setup
#

# install vectorbt
pip install vectorbt

# core imports
import vectorbt as vbt
import pandas as pd
import numpy as np
from datetime import datetime

# data handling
import yfinance as yf  # or use your data provider

basic strategy implementation
#

simple moving average crossover example:

import vectorbt as vbt
import pandas as pd
import numpy as np

class VectorBTStrategy:
    def __init__(self, data, fast_window=20, slow_window=50):
        """
        Initialize strategy with price data and parameters

        Args:
            data: DataFrame with OHLCV data
            fast_window: Fast MA period
            slow_window: Slow MA period
        """
        self.data = data
        self.fast_window = fast_window
        self.slow_window = slow_window
        self.close = data['Close']

    def generate_signals(self):
        """Generate entry/exit signals using MA crossover"""
        # Calculate moving averages (vectorized)
        fast_ma = vbt.MA.run(self.close, window=self.fast_window, short_name='fast')
        slow_ma = vbt.MA.run(self.close, window=self.slow_window, short_name='slow')

        # Generate crossover signals
        entries = fast_ma.ma_above(slow_ma, crossed=True)
        exits = fast_ma.ma_below(slow_ma, crossed=True)

        return entries, exits

    def backtest(self, initial_cash=100000, commission=0.001):
        """Run backtest with generated signals"""
        entries, exits = self.generate_signals()

        # Create portfolio
        portfolio = vbt.Portfolio.from_signals(
            self.close,
            entries=entries,
            exits=exits,
            init_cash=initial_cash,
            fees=commission,
            freq='1D'
        )

        return portfolio

    def get_metrics(self, portfolio):
        """Extract key performance metrics"""
        return {
            'total_return': portfolio.total_return(),
            'sharpe_ratio': portfolio.sharpe_ratio(),
            'max_drawdown': portfolio.max_drawdown(),
            'win_rate': portfolio.trades.win_rate(),
            'total_trades': portfolio.trades.count(),
            'avg_trade': portfolio.trades.pnl.mean(),
            'profit_factor': portfolio.trades.profit_factor()
        }

# Example usage
if __name__ == "__main__":
    # Load data
    data = pd.read_csv('es_futures_daily.csv', index_col='Date', parse_dates=True)

    # Run single backtest
    strategy = VectorBTStrategy(data, fast_window=20, slow_window=50)
    portfolio = strategy.backtest()
    metrics = strategy.get_metrics(portfolio)

    print(f"Sharpe Ratio: {metrics['sharpe_ratio']:.2f}")
    print(f"Total Return: {metrics['total_return']:.2%}")
    print(f"Max Drawdown: {metrics['max_drawdown']:.2%}")

parameter optimization at scale
#

this is where vectorbt shines:

import vectorbt as vbt
import pandas as pd
import numpy as np
import itertools

class ParameterOptimizer:
    def __init__(self, data):
        """Initialize optimizer with price data"""
        self.data = data
        self.close = data['Close']
        self.results = []

    def optimize_ma_crossover(
        self,
        fast_windows=[10, 20, 30, 50],
        slow_windows=[50, 100, 150, 200],
        initial_cash=100000,
        commission=0.001
    ):
        """
        Optimize MA crossover strategy across parameter grid

        Uses vectorbt's built-in optimization for speed
        """
        # Create parameter combinations
        fast_windows = np.array(fast_windows)
        slow_windows = np.array(slow_windows)

        # Run optimization using vectorbt's native optimization
        # This executes all combinations in parallel (vectorized)
        fast_ma = vbt.MA.run(
            self.close,
            window=fast_windows,
            short_name='fast'
        )

        slow_ma = vbt.MA.run(
            self.close,
            window=slow_windows,
            short_name='slow'
        )

        # Generate entry/exit for all parameter combinations
        entries = fast_ma.ma_above(slow_ma, crossed=True)
        exits = fast_ma.ma_below(slow_ma, crossed=True)

        # Backtest all combinations at once (THIS IS THE MAGIC)
        portfolio = vbt.Portfolio.from_signals(
            self.close,
            entries=entries,
            exits=exits,
            init_cash=initial_cash,
            fees=commission,
            freq='1D'
        )

        return portfolio

    def extract_best_params(self, portfolio):
        """Find best performing parameter combination"""
        # Get Sharpe ratio for all combinations
        sharpe_ratios = portfolio.sharpe_ratio()

        # Find maximum Sharpe
        best_idx = sharpe_ratios.idxmax()

        # Extract metrics for best combination
        best_metrics = {
            'fast_window': best_idx[0] if isinstance(best_idx, tuple) else best_idx,
            'slow_window': best_idx[1] if isinstance(best_idx, tuple) else None,
            'sharpe_ratio': sharpe_ratios.max(),
            'total_return': portfolio.total_return().loc[best_idx],
            'max_drawdown': portfolio.max_drawdown().loc[best_idx],
            'win_rate': portfolio.trades.win_rate().loc[best_idx],
            'total_trades': portfolio.trades.count().loc[best_idx]
        }

        return best_metrics

    def walk_forward_optimization(
        self,
        in_sample_days=365,
        out_sample_days=90,
        step_days=90,
        fast_windows=[10, 20, 30],
        slow_windows=[50, 100, 150]
    ):
        """
        Walk-forward optimization using vectorbt

        Combines walk-forward validation with vectorized backtesting
        """
        results = []

        # Generate date windows
        start_date = self.data.index[0]
        end_date = self.data.index[-1]

        current_start = start_date
        window_num = 0

        while current_start + pd.Timedelta(days=in_sample_days + out_sample_days) <= end_date:
            in_sample_end = current_start + pd.Timedelta(days=in_sample_days)
            out_sample_end = in_sample_end + pd.Timedelta(days=out_sample_days)

            # Split data
            in_sample_data = self.data.loc[current_start:in_sample_end]
            out_sample_data = self.data.loc[in_sample_end:out_sample_end]

            # Optimize on in-sample (vectorized)
            in_optimizer = ParameterOptimizer(in_sample_data)
            in_portfolio = in_optimizer.optimize_ma_crossover(
                fast_windows=fast_windows,
                slow_windows=slow_windows
            )
            best_params = in_optimizer.extract_best_params(in_portfolio)

            # Test on out-of-sample with best params
            out_strategy = VectorBTStrategy(
                out_sample_data,
                fast_window=best_params['fast_window'],
                slow_window=best_params['slow_window']
            )
            out_portfolio = out_strategy.backtest()
            out_metrics = out_strategy.get_metrics(out_portfolio)

            # Store results
            results.append({
                'window': window_num,
                'in_sample_period': (current_start, in_sample_end),
                'out_sample_period': (in_sample_end, out_sample_end),
                'best_fast': best_params['fast_window'],
                'best_slow': best_params['slow_window'],
                'in_sample_sharpe': best_params['sharpe_ratio'],
                'out_sample_sharpe': out_metrics['sharpe_ratio'],
                'out_sample_return': out_metrics['total_return'],
                'degradation': (best_params['sharpe_ratio'] - out_metrics['sharpe_ratio']) / best_params['sharpe_ratio']
            })

            window_num += 1
            current_start += pd.Timedelta(days=step_days)

        return pd.DataFrame(results)

# Example: Run walk-forward optimization
if __name__ == "__main__":
    # Load your data
    data = pd.read_csv('es_futures_daily.csv', index_col='Date', parse_dates=True)

    # Initialize optimizer
    optimizer = ParameterOptimizer(data)

    # Run walk-forward
    results_df = optimizer.walk_forward_optimization(
        in_sample_days=365,
        out_sample_days=90,
        step_days=90,
        fast_windows=[10, 20, 30, 50],
        slow_windows=[50, 100, 150, 200]
    )

    # Analyze results
    print(f"Average out-of-sample Sharpe: {results_df['out_sample_sharpe'].mean():.2f}")
    print(f"Average degradation: {results_df['degradation'].mean():.1%}")
    print(f"Windows tested: {len(results_df)}")

performance comparison
#

backtrader (event-driven):

4 fast × 4 slow = 16 combinations.

10 walk-forward windows.

total: 160 backtests.

time: 7 minutes.

vectorbt (vectorized):

same 160 backtests.

time: 32 seconds.

13x speedup.

my current workflow
#

monthly strategy review:

load 2 years ES futures data.

run walk-forward optimization (4×4×10 = 160 backtests).

vectorbt: <1 minute.

backtrader would take 10+ minutes.

parameter stability check:

test if current parameters still in top quartile.

if degrading: research why, adjust or pause.

rapid iteration:

test new parameter ranges quickly.

experiment with different entry/exit logic.

this is how i stay profitable.

advanced features
#

vectorbt has more power i’m exploring:

# Multi-asset portfolio optimization
symbols = ['ES', 'NQ', 'YM']
data = vbt.YFData.download(symbols, start='2023-01-01')

# Run strategy across all symbols simultaneously
portfolio = vbt.Portfolio.from_signals(
    data.get('Close'),
    entries=entries,  # Same signals, applied to all symbols
    exits=exits,
    init_cash=100000,
    fees=0.001,
    group_by=True  # Treat as single portfolio
)

# Position sizing with Kelly criterion
kelly_sizes = portfolio.get_kelly_sizes()

# Risk metrics
portfolio.get_risk_metrics()

gotchas and limitations
#

memory usage:

vectorized = loads everything into RAM.

2 years daily data across 16 params = ~500MB.

not a problem on my server (128GB RAM).

complexity:

event-driven logic (backtrader) easier to understand.

vectorized requires thinking in arrays.

debugging:

harder to debug vectorized code.

trade-off for speed.

when to use vectorbt vs backtrader
#

use vectorbt when:

parameter optimization (speed critical).

testing simple signal-based strategies.

walk-forward validation at scale.

use backtrader when:

complex event-driven logic (stop losses, position sizing).

strategy needs order management.

debugging new strategy logic.

i use both:

vectorbt: parameter optimization, validation.

backtrader: strategy development, complex logic.

resources
#

vectorbt docs:

https://vectorbt.dev/

thorough examples, API reference.

my learning path:

started with backtrader (2023).

discovered vectorbt (2024).

now use vectorbt for 80% of backtesting.

NexusFi discussions:

learned optimization tricks from quant traders.

tonight (february 5, 2:51am)
#

vectorbt = 13x faster than backtrader.

vectorized parameter testing.

same 160 backtests:

backtrader: 7 minutes.

vectorbt: 32 seconds.

rapid iteration enabled.

this is how i stay ahead.


2:51am wednesday. vectorbt backtesting framework. 13x faster than backtrader for parameter optimization (160 backtests: 32 seconds vs 7 minutes). vectorized operations process entire dataset at once using numpy. walk-forward optimization example: 4×4×10 = 160 backtests <1 minute. use vectorbt for parameter optimization, backtrader for complex event-driven logic. learned from NexusFi quant discussions. enables rapid monthly strategy review. this is sustainable algo trading edge.

-AK

Related

parameter tuning - avoiding overfitting with walk-forward validation
parameter optimization = dangerous. easy to overfit historical data. walk-forward validation = solution. the overfitting problem # traditional optimization:
walk-forward optimization - how i avoid overfitting my strategies
overfitting = #1 way algos fail in production. backtest looks amazing. live trading implodes. walk-forward optimization prevents this. been discussing validation techniques on NexusFi algo trading threads and walk-forward is the gold standard.
rebuilt backtesting pipeline - 10x faster parameter optimization
spent last 3 days rebuilding backtest optimization pipeline. went from 6 hours to 35 minutes for full parameter sweep. the problem # old approach: sequential parameter testing.
regime detection - walk-forward validation improving accuracy
regime detection upgraded. walk-forward validation running. accuracy improving. the problem # static regime parameters: optimized on historical data.
regime detection improvements - faster market adaptation working
week 3 april going strong. adaptive strategy crushing it. been refining regime detection logic. current performance (apr 1-17) # trades: 29
strategy overhaul - adapting algos to new market regime
february crushed my strategies. mean reversion dropped from 81% to 57% win rate. market regime changed. strategies need to adapt. been discussing regime adaptation on r/algotrading. other algo traders dealing with same shit.