How to Backtest a Momentum Strategy Using vectorbt: A Step-by-Step Guide
You can backtest a momentum strategy using vectorbt by calculating 12-month returns (skipping the most recent month), ranking assets to generate long/short signals, and passing these signals to vbt.Portfolio.from_signals() for vectorized backtesting.
The repository paperswithbacktest/awesome-systematic-trading contains canonical momentum implementations originally written for QuantConnect that translate directly into vectorbt workflows. By leveraging vectorbt's Numba-accelerated engine, you can replicate institutional-grade momentum factor backtesting on plain pandas DataFrames without complex event-driven architecture.
Understanding the Momentum Logic
The reference implementation in static/strategies/momentum-factor-effect-in-stocks.py defines momentum as the past 12-month return excluding the most recent month—a standard academic definition used to avoid short-term reversal effects. The script's CoarseSelectionFunction and FineSelectionFunction methods filter and rank securities by this metric, then allocate equal weights to top and bottom performers.
This same logic maps cleanly to vectorbt's vectorized paradigm. Instead of iterating through bars, you operate on entire price matrices at once, computing signals for all assets simultaneously.
Step-by-Step Implementation
Load Price Data
vectorbt accepts any pandas DataFrame with shape (dates × assets). Use yfinance or your preferred data provider to fetch adjusted close prices.
import yfinance as yf
import vectorbt as vbt
# Fetch S&P 500 constituents or your universe
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'JPM', 'BAC', 'XOM']
prices = yf.download(tickers, start='2000-01-01', end='2024-01-01')['Adj Close']
Calculate Momentum Signals
Compute the 12-month (252 trading days) return and skip the most recent month (21 days) to match the logic in the QuantConnect algorithm.
# 12-month momentum, skipping the most recent month
momentum = prices.pct_change(252).shift(21)
This mirrors the comment in momentum-factor-effect-in-stocks.py lines 3-5, which explicitly excludes the most recent month's returns to avoid reversal bias.
Generate Long/Short Signals
Rank assets monthly and select extremes. The QuantConnect version uses sorted_by_perf to identify winners and losers, assigning equal weights in lines 79-94.
# Define portfolio size (e.g., top/bottom 10%)
n_assets = prices.shape[1]
n_long = n_short = int(0.10 * n_assets)
# Rank assets by momentum (1 = highest)
rank = momentum.rank(axis=1, ascending=False, method='first')
# Generate signals: 1 for long, -1 for short, 0 otherwise
long_signal = (rank <= n_long).astype(int)
short_signal = (rank > n_assets - n_short).astype(int) * -1
signals = long_signal + short_signal
Build the Portfolio with vectorbt
Pass the boolean signals to Portfolio.from_signals(), specifying bidirectional trading to allow both long and short positions.
# Create boolean masks for entries and exits
entries = signals > 0
exits = signals < 0
pf = vbt.Portfolio.from_signals(
prices,
entries=entries,
exits=exits,
direction='both', # Enable long and short positions
freq='1M' # Monthly rebalancing frequency
)
Analyze Performance Metrics
vectorbt replaces the ad-hoc logging in QuantConnect's OnData method with comprehensive built-in analytics.
# Key performance statistics
print(pf.stats())
# Specific metrics
print(f"Total Return: {pf.total_return():.2%}")
print(f"Sharpe Ratio: {pf.sharpe():.2f}")
print(f"Max Drawdown: {pf.max_drawdown():.2%}")
Complete Working Example
The following script consolidates the entire workflow, replicating the momentum factor effect from the repository's QuantConnect implementation using pure vectorbt:
import yfinance as yf
import pandas as pd
import vectorbt as vbt
# 1. Load data
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META',
'JPM', 'BAC', 'XOM', 'JNJ', 'WMT']
prices = yf.download(tickers, start='2015-01-01', end='2024-01-01')['Adj Close']
# 2. Calculate momentum (12-month, skip 1 month)
lookback = 252 # trading days
skip = 21 # 1 month
momentum = prices.pct_change(lookback).shift(skip)
# 3. Generate signals monthly
# Resample to month-end to match QuantConnect's monthly rebalancing
month_end_prices = prices.resample('M').last()
month_end_momentum = momentum.resample('M').last()
n_long = n_short = 2 # Top/bottom 2 assets
# Rank and select
rank = month_end_momentum.rank(axis=1, ascending=False)
long_entries = rank <= n_long
short_entries = rank > (prices.shape[1] - n_short)
# Align signals with daily prices for vectorbt
long_entries = long_entries.reindex(prices.index, method='ffill')
short_entries = short_entries.reindex(prices.index, method='ffill')
# 4. Build portfolio
pf = vbt.Portfolio.from_signals(
prices,
entries=long_entries,
exits=short_entries,
direction='both',
freq='1D'
)
# 5. Results
print(pf.stats())
pf.total_return().vbt.plot()
Key Reference Files in the Repository
static/strategies/momentum-factor-effect-in-stocks.py– Contains the classic 12-month momentum strategy logic, including theInitialize,CoarseSelectionFunction, andFineSelectionFunctionmethods that define the ranking and selection criteria.static/strategies/time-series-momentum-effect.py– Demonstrates time-series momentum (trend following) that can be implemented using vectorbt's rolling window indicators.static/strategies/sector-momentum-rotational-system.py– Shows sector-level rotation logic useful for testing rotational momentum systems with ETF universes.README.md– Lists vectorbt as a recommended fast backtesting alternative and provides links to the library documentation.
Summary
- vectorbt enables vectorized momentum backtesting directly on pandas DataFrames, eliminating the need for event-driven loops.
- Calculate momentum using
pct_change(252).shift(21)to match the academic definition found in the repository's QuantConnect scripts. - Use
rank(axis=1)to replicate the sorting logic inmomentum-factor-effect-in-stocks.pylines 79-94. - Feed boolean signals to
vbt.Portfolio.from_signals()withdirection='both'to implement long/short equity strategies. - Access institutional-grade analytics via
pf.stats(), including Sharpe ratio, drawdown, and return attribution.
Frequently Asked Questions
What is the optimal lookback period for momentum strategies?
Most implementations in paperswithbacktest/awesome-systematic-trading use a 12-month lookback period (252 trading days) excluding the most recent month. This 12-1 month formation period is the academic standard found in the momentum-factor-effect-in-stocks.py file, designed to capture persistent trends while avoiding short-term reversal effects.
How does vectorbt handle transaction costs and slippage?
When calling vbt.Portfolio.from_signals(), you can specify fees (as a percentage of traded value), slippage (as a percentage of price), and fixed_fees (per-trade costs). For realistic momentum strategy backtesting, set fees=0.001 for 10 basis points commission and slippage=0.0005 to simulate market impact during monthly rebalancing.
Can I implement time-series momentum instead of cross-sectional momentum?
Yes. While the repository's time-series-momentum-effect.py demonstrates cross-sectional ranking (comparing assets to each other), you can adapt the code to time-series momentum by comparing each asset's return to a threshold (e.g., risk-free rate or zero): long_signal = momentum > 0. This creates individual trend-following signals without ranking against other assets.
How do I optimize the number of portfolio positions (N) in vectorbt?
Use vbt.Portfolio.from_signals() inside an optimization loop or with vbt.Param objects. Create a parameter grid for n_long and n_short, then run pf.from_signals() for each combination. Compare results using pf.sharpe() or pf.calmar_ratio() to identify the optimal portfolio concentration that maximizes risk-adjusted returns for your specific universe.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →