P
PipsGrowth
← Back to Libraries

bt for Backtesting

A flexible backtesting framework for Python with a focus on portfolio-based strategies. Features an intuitive algorithm-tree structure for building and comparing trading strategies.

Difficulty: Intermediate
Category: Backtesting
🌳 Tree-Based

Installation

Install bt with pip. It depends on ffn (financial functions) which will be installed automatically.

# Install bt
$ pip install bt
# Install with data sources
$ pip install bt yfinance pandas

Key Features

Tree-Based Strategies

Build complex strategies using composable algorithm trees with parent-child relationships.

Built-in Rebalancing

Multiple rebalancing options: monthly, weekly, daily, or custom trigger-based rebalancing.

Portfolio Weighting

Equal weight, inverse volatility, mean-variance, risk parity, and custom weighting schemes.

Rich Analytics

Comprehensive performance metrics, drawdown analysis, and visualization tools built-in.

How bt Works

bt uses an algorithm tree where each node is an algorithm that processes in sequence:

1.RunMonthly()→ When to run
2.SelectAll()→ What to consider
3.WeighEqually()→ How to weight
4.Rebalance()→ Execute trades

Code Examples

Simple SMA Crossover Strategy

Create a basic moving average crossover strategy

Python
import bt
import yfinance as yf
# Download data
data = yf.download(['EURUSD=X', 'GBPUSD=X'], start='2020-01-01', end='2024-01-01')['Close']
# Create a simple SMA crossover strategy
def sma_crossover(data, short_period=20, long_period=50):
"""SMA crossover signal generator"""
sma_short = data.rolling(short_period).mean()
sma_long = data.rolling(long_period).mean()
return sma_short > sma_long
# Generate signals
signal = sma_crossover(data, 20, 50)
# Create strategy
strategy = bt.Strategy('SMA Crossover', [
bt.algos.SelectWhere(signal),
bt.algos.WeighEqually(),
bt.algos.Rebalance()
])
# Create backtest
backtest = bt.Backtest(strategy, data)
# Run backtest
result = bt.run(backtest)
# Display results
result.display()
result.plot()

Compare Multiple Strategies

Run and compare different strategies simultaneously

Python
import bt
import yfinance as yf
data = yf.download('EURUSD=X', start='2020-01-01', end='2024-01-01')['Close'].to_frame('EURUSD')
# Strategy 1: Buy and Hold
buy_hold = bt.Strategy('Buy and Hold', [
bt.algos.RunOnce(),
bt.algos.SelectAll(),
bt.algos.WeighEqually(),
bt.algos.Rebalance()
])
# Strategy 2: SMA 50
sma_signal = data > data.rolling(50).mean()
sma_strategy = bt.Strategy('SMA 50 Filter', [
bt.algos.SelectWhere(sma_signal),
bt.algos.WeighEqually(),
bt.algos.Rebalance()
])
# Strategy 3: Monthly Rebalance
monthly = bt.Strategy('Monthly Rebalance', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.WeighEqually(),
bt.algos.Rebalance()
])
# Create backtests
backtests = [
bt.Backtest(buy_hold, data),
bt.Backtest(sma_strategy, data),
bt.Backtest(monthly, data)
]
# Run all backtests
results = bt.run(*backtests)
# Compare results
results.display()
results.plot()

Custom Portfolio Weighting

Implement various weighting schemes

Python
import bt
import numpy as np
# Download multi-asset data
symbols = ['EURUSD=X', 'GBPUSD=X', 'USDJPY=X', 'AUDUSD=X']
data = yf.download(symbols, start='2020-01-01', end='2024-01-01')['Close']
data.columns = ['EURUSD', 'GBPUSD', 'USDJPY', 'AUDUSD']
# Equal Weight Strategy
equal_weight = bt.Strategy('Equal Weight', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.WeighEqually(),
bt.algos.Rebalance()
])
# Inverse Volatility Weighting
invvol = bt.Strategy('Inverse Volatility', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.WeighInvVol(lookback=pd.DateOffset(months=3)),
bt.algos.Rebalance()
])
# Mean-Variance Optimization
mvo = bt.Strategy('Mean-Variance', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.WeighMeanVar(lookback=pd.DateOffset(months=6)),
bt.algos.Rebalance()
])
# Custom Fixed Weights
def fixed_weights():
"""Return fixed weights"""
return {'EURUSD': 0.4, 'GBPUSD': 0.3, 'USDJPY': 0.2, 'AUDUSD': 0.1}
fixed = bt.Strategy('Fixed Weights', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.WeighSpecified(**fixed_weights()),
bt.algos.Rebalance()
])
# Run comparison
results = bt.run(
bt.Backtest(equal_weight, data),
bt.Backtest(invvol, data),
bt.Backtest(mvo, data),
bt.Backtest(fixed, data)
)
results.plot_weights()

Momentum Strategy

Select assets based on momentum

Python
import bt
import pandas as pd
# Download data
symbols = ['EURUSD=X', 'GBPUSD=X', 'USDJPY=X', 'AUDUSD=X', 'USDCHF=X']
data = yf.download(symbols, start='2018-01-01', end='2024-01-01')['Close']
data.columns = [s.replace('=X', '') for s in symbols]
# Calculate momentum (12-month return)
momentum = data.pct_change(252)
# Momentum strategy - Select top 2 performers
momentum_strategy = bt.Strategy('Momentum Top 2', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.SelectMomentum(n=2, lookback=pd.DateOffset(months=12)),
bt.algos.WeighEqually(),
bt.algos.Rebalance()
])
# Dual Momentum - Only buy if positive momentum
class SelectPositiveMomentum(bt.Algo):
def __init__(self, lookback=252):
self.lookback = lookback
def __call__(self, target):
selected = target.temp.get('selected', target.universe.columns)
returns = target.universe[selected].pct_change(self.lookback).iloc[-1]
target.temp['selected'] = returns[returns > 0].index.tolist()
return len(target.temp['selected']) > 0
dual_momentum = bt.Strategy('Dual Momentum', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
SelectPositiveMomentum(lookback=252),
bt.algos.SelectMomentum(n=2, lookback=pd.DateOffset(months=12)),
bt.algos.WeighEqually(),
bt.algos.Rebalance()
])
results = bt.run(
bt.Backtest(momentum_strategy, data),
bt.Backtest(dual_momentum, data)
)
results.display()

Risk Parity Strategy

Allocate based on risk contribution

Python
import bt
import pandas as pd
# Risk Parity - Equal risk contribution from each asset
risk_parity = bt.Strategy('Risk Parity', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.WeighERC(lookback=pd.DateOffset(months=6)), # Equal Risk Contribution
bt.algos.Rebalance()
])
# Target Volatility - Scale exposure to target vol
class TargetVolatility(bt.Algo):
def __init__(self, target_vol=0.1, lookback=60):
self.target_vol = target_vol
self.lookback = lookback
def __call__(self, target):
if target.positions.shape[0] < self.lookback:
return True
# Calculate realized volatility
returns = target.universe.pct_change().iloc[-self.lookback:]
weights = target.temp.get('weights', {})
if not weights:
return True
# Scale weights to target volatility
portfolio_vol = returns.std().mean() * np.sqrt(252)
scale = self.target_vol / portfolio_vol if portfolio_vol > 0 else 1
scale = min(scale, 2) # Cap leverage at 2x
target.temp['weights'] = {k: v * scale for k, v in weights.items()}
return True
target_vol = bt.Strategy('Target Volatility 10%', [
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.WeighEqually(),
TargetVolatility(target_vol=0.10),
bt.algos.Rebalance()
])
results = bt.run(
bt.Backtest(risk_parity, data),
bt.Backtest(target_vol, data)
)
results.plot_security_weights()

Performance Analysis

Analyze detailed backtest metrics

Python
import bt
# After running backtest
result = bt.run(backtest)
# Get performance statistics
stats = result.stats
print(stats)
# Key metrics
print(f"Total Return: {result.stats.loc['total_return'].values[0]:.2%}")
print(f"CAGR: {result.stats.loc['cagr'].values[0]:.2%}")
print(f"Max Drawdown: {result.stats.loc['max_drawdown'].values[0]:.2%}")
print(f"Sharpe Ratio: {result.stats.loc['daily_sharpe'].values[0]:.2f}")
print(f"Calmar Ratio: {result.stats.loc['calmar'].values[0]:.2f}")
# Monthly returns heatmap
result.plot_histogram()
# Drawdown plot
result.plot_drawdown()
# Get transaction log
transactions = result.get_transactions()
print(f"\nTotal trades: {len(transactions)}")
print(transactions.tail())
# Get weights over time
weights = result.get_security_weights()
weights.plot(title='Portfolio Weights Over Time')

Create Custom Algorithm

Build your own selection/weighting algorithm

Python
import bt
import pandas as pd
import numpy as np
class SelectByRSI(bt.Algo):
"""Select assets where RSI is oversold"""
def __init__(self, period=14, oversold=30):
self.period = period
self.oversold = oversold
def __call__(self, target):
# Calculate RSI for all assets
prices = target.universe
delta = prices.diff()
gain = delta.where(delta > 0, 0).rolling(self.period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(self.period).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
# Select oversold assets
latest_rsi = rsi.iloc[-1]
selected = latest_rsi[latest_rsi < self.oversold].index.tolist()
target.temp['selected'] = selected if selected else target.universe.columns.tolist()
return True
class LimitPositionSize(bt.Algo):
"""Limit maximum position size"""
def __init__(self, max_weight=0.25):
self.max_weight = max_weight
def __call__(self, target):
weights = target.temp.get('weights', {})
if not weights:
return True
# Cap weights
capped = {k: min(v, self.max_weight) for k, v in weights.items()}
# Renormalize
total = sum(capped.values())
if total > 0:
target.temp['weights'] = {k: v/total for k, v in capped.items()}
return True
# Use custom algorithms
custom_strategy = bt.Strategy('RSI Oversold', [
bt.algos.RunWeekly(),
SelectByRSI(period=14, oversold=30),
bt.algos.WeighEqually(),
LimitPositionSize(max_weight=0.30),
bt.algos.Rebalance()
])
result = bt.run(bt.Backtest(custom_strategy, data))
result.display()

Common Use Cases

Portfolio backtesting
Asset allocation strategies
Momentum investing
Risk parity portfolios
Mean reversion strategies
Multi-asset comparison
Rebalancing optimization
Factor-based investing
Custom algorithm development
Strategy visualization

Best Practices & Common Pitfalls

Use Algorithm Composition

Build complex strategies by chaining simple algorithms together for maintainable code.

Test Multiple Strategies

Always compare against benchmarks like buy-and-hold to validate strategy performance.

Monitor Turnover

Use result.get_transactions() to analyze trading frequency and potential transaction costs.

Data Alignment

Ensure all price series have the same date index. Use dropna() to handle missing values.

Column Names Matter

bt uses column names as asset identifiers. Keep them consistent across data and signal DataFrames.

Forward-Looking Bias

Custom algorithms must only use historical data available at each point in time.

Additional Resources

bt vs Other Frameworks

  • bt: Portfolio-focused, tree structure
  • Backtrader: Event-driven, feature-rich
  • vectorbt: Fastest, vectorized

Next Steps

Explore other backtesting frameworks or add live trading capabilities: