# How to Backtest a Momentum Strategy Using vectorbt: A Step-by-Step Guide

> Learn to backtest a momentum strategy with vectorbt. This guide covers calculating returns, ranking assets for signals, and vectorized backtesting for profitable trading.

- Repository: [Papers With Backtest/awesome-systematic-trading](https://github.com/paperswithbacktest/awesome-systematic-trading)
- Tags: how-to-guide
- Published: 2026-08-08

---

**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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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.

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

```python

# 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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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.

```python

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

```python

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

```python

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

```python
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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/static/strategies/momentum-factor-effect-in-stocks.py)** – Contains the classic 12-month momentum strategy logic, including the `Initialize`, `CoarseSelectionFunction`, and `FineSelectionFunction` methods that define the ranking and selection criteria.
- **[`static/strategies/time-series-momentum-effect.py`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/static/strategies/sector-momentum-rotational-system.py)** – Shows sector-level rotation logic useful for testing rotational momentum systems with ETF universes.
- **[`README.md`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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 in [`momentum-factor-effect-in-stocks.py`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/momentum-factor-effect-in-stocks.py) lines 79-94.
- Feed boolean signals to `vbt.Portfolio.from_signals()` with `direction='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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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.