title: Multi-Strategy Backtesting description: Guide for running and managing multiple strategies in backtrader


Multi-Strategy Backtesting

Running multiple strategies simultaneously allows you to diversify your approach, compare performance, and build robust trading systems. This guide covers techniques for multi-strategy portfolio management in backtrader.

Quick Start

Basic Multi-Strategy Setup

import backtrader as bt

cerebro = bt.Cerebro()

# Add multiple strategies

cerebro.addstrategy(MomentumStrategy, period=20)
cerebro.addstrategy(MeanReversionStrategy, period=10)
cerebro.addstrategy(BreakoutStrategy, period=50)

# Run - all strategies trade with shared broker

results = cerebro.run()

# Each strategy result is returned separately

for i, strat in enumerate(results):
    print(f"Strategy {i}: Final Value {strat.broker.getvalue()}")

```bash

## Strategy Portfolio Management

### Equal Weight Allocation

```python
class EqualWeightStrategy(bt.Strategy):
    """Base class for equal-weight multi-strategy portfolio."""

    params = (
        ('weight', 0.33),  # Equal allocation for 3 strategies
        ('max_position', 0.95),
    )

    def __init__(self):
        self.order = None
        self.target_value = self.broker.getvalue() *self.p.weight

    def next(self):
        current_value = self.broker.getvalue()*self.p.weight

        if self.signal() and not self.position:

# Buy with allocated capital
            size = int(current_value / self.data.close[0])
            self.buy(size=size)
        elif not self.signal() and self.position:
            self.close()

    def signal(self):

# Override in subclass
        return False

```bash

### Risk Parity Allocation

```python
class RiskParityStrategy(bt.Strategy):
    """Allocate capital based on strategy volatility."""

    params = (
        ('lookback', 20),
        ('target_risk', 0.02),  # 2% daily risk
    )

    def __init__(self):
        self.atr = bt.indicators.ATR(self.data, period=self.p.lookback)
        self.volatility = self.atr / self.data.close

    def get_position_size(self):
        """Calculate position size based on volatility."""
        risk_per_share = self.atr[0]
        account_risk = self.broker.getvalue()*self.p.target_risk
        return int(account_risk / risk_per_share) if risk_per_share > 0 else 0

```bash

## Resource Allocation

### Capital Allocation Strategies

```python
class CapitalAllocator(bt.Strategy):
    """Dynamically allocate capital between strategies."""

    params = (
        ('rebalance_freq', 20),  # Rebalance every 20 bars
        ('min_allocation', 0.1),  # Minimum 10% allocation
    )

    def __init__(self):
        self.strategies = []
        self.allocations = []
        self.last_rebalance = 0

    def add_strategy(self, strategy, allocation):
        """Add a strategy with its target allocation."""
        self.strategies.append(strategy)
        self.allocations.append(allocation)

    def next(self):
        if len(self.data) - self.last_rebalance >= self.p.rebalance_freq:
            self.rebalance()
            self.last_rebalance = len(self.data)

    def rebalance(self):
        """Rebalance capital based on performance."""

# Implementation depends on allocation method
        pass

```bash

### Commission Splitting

```python
class CommissionSplitter(bt.CommissionInfo):
    """Split commissions proportionally among strategies."""

    params = (('strategies', []),)

    def getcommission(self, size, price):
        comm = super().getcommission(size, price)

# Split commission if multiple strategies involved
        return comm / len(self.p.strategies) if self.p.strategies else comm

```bash

## Results Aggregation

### Portfolio-Level Analysis

```python
class PortfolioAnalyzer(bt.Analyzer):
    """Analyze combined performance of all strategies."""

    def __init__(self):
        self.returns = []
        self.drawdowns = []

    def next(self):
        total_value = self.strategy.broker.getvalue()
        self.returns.append(total_value)

    def get_analysis(self):
        import numpy as np

        returns_array = np.array(self.returns)
        cumulative_returns = (returns_array / returns_array[0]) - 1

# Calculate running maximum
        running_max = np.maximum.accumulate(returns_array)
        drawdowns = (returns_array - running_max) / running_max

        return {
            'total_return': cumulative_returns[-1],
            'max_drawdown': drawdowns.min(),
            'final_value': returns_array[-1],
            'returns_series': self.returns,
        }

# Usage

cerebro.addanalyzer(PortfolioAnalyzer, _name='portfolio')
results = cerebro.run()
portfolio_analysis = results[0].analyzers.portfolio.get_analysis()

```bash

### Multi-Strategy Comparison

```python
def compare_strategies(strategies, data_path):
    """Run and compare multiple strategies."""
    results_summary = []

    for strat_class in strategies:
        cerebro = bt.Cerebro()
        cerebro.adddata(bt.feeds.CSVData(dataname=data_path))
        cerebro.addstrategy(strat_class)
        cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
        cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
        cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')

        result = cerebro.run()[0]

        summary = {
            'strategy': strat_class.__name__,
            'sharpe': result.analyzers.sharpe.get_analysis().get('sharperatio'),
            'max_dd': result.analyzers.drawdown.get_analysis()['max']['drawdown'],
            'return': result.analyzers.returns.get_analysis()['rnorm'],
        }
        results_summary.append(summary)

# Print comparison table
    print(f"{'Strategy':<20} {'Sharpe':>10} {'Max DD':>10} {'Return':>10}")
    print("-"* 52)
    for s in results_summary:
        print(f"{s['strategy']:<20} {s['sharpe']:>10.2f} {s['max_dd']:>10.2f} {s['return']:>10.2%}")

    return results_summary

```bash

## Strategy Correlation Analysis

### Calculate Correlations

```python
def calculate_strategy_correlations(strategies, data_path):
    """Calculate return correlations between strategies."""
    from scipy.stats import pearsonr
    import pandas as pd

# Collect returns from each strategy
    all_returns = {}

    for strat_class in strategies:
        cerebro = bt.Cerebro()
        cerebro.adddata(bt.feeds.CSVData(dataname=data_path))
        cerebro.addstrategy(strat_class)
        cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='returns')

        result = cerebro.run()[0]
        returns_dict = result.analyzers.returns.get_analysis()
        all_returns[strat_class.__name__] = pd.Series(returns_dict)

# Calculate correlation matrix
    returns_df = pd.DataFrame(all_returns)
    correlation_matrix = returns_df.corr()

    return correlation_matrix

# Usage

strategies = [MomentumStrategy, MeanReversionStrategy, BreakoutStrategy]
corr_matrix = calculate_strategy_correlations(strategies, 'data.csv')
print(corr_matrix)

```bash

### Low-Correlation Portfolio

```python
class LowCorrelationSelector(bt.Strategy):
    """Select strategies with low correlation to each other."""

    params = (
        ('max_correlation', 0.7),
        ('min_strategies', 2),
    )

    def __init__(self):
        self.selected_strategies = []
        self.returns_history = {s: [] for s in self.p.strategies}

    def calculate_correlation(self, returns1, returns2):
        """Calculate correlation between two return series."""
        import numpy as np
        return np.corrcoef(returns1, returns2)[0, 1]

    def select_strategies(self):
        """Select strategies with correlations below threshold."""
        selected = [self.p.strategies[0]]  # Start with first strategy

        for candidate in self.p.strategies[1:]:

# Check correlation with all selected strategies
            correlations = [
                self.calculate_correlation(
                    self.returns_history[candidate],
                    self.returns_history[selected_strat]
                )
                for selected_strat in selected
            ]

            if all(c < self.p.max_correlation for c in correlations):
                selected.append(candidate)

        return selected[:self.p.max_strategies]

```bash

## Parallel Execution

### Multi-Process Optimization

```python
from multiprocessing import Pool
import itertools

def run_strategy_backtest(params):
    """Run a single backtest with given parameters."""
    strat_class, data_path, strat_params = params

    cerebro = bt.Cerebro(stdstats=False)
    cerebro.adddata(bt.feeds.CSVData(dataname=data_path))
    cerebro.addstrategy(strat_class, **strat_params)

    result = cerebro.run()[0]
    return {
        'params': strat_params,
        'final_value': cerebro.broker.getvalue(),
        'sharpe': result.analyzers.sharpe.get_analysis().get('sharperatio', 0),
    }

def parallel_optimize(strat_class, data_path, param_grid, n_workers=4):
    """Optimize strategy parameters in parallel."""

# Generate all parameter combinations
    param_combinations = list(itertools.product(*param_grid.values()))
    param_dicts = [dict(zip(param_grid.keys(), combo)) for combo in param_combinations]

# Create parameter tuples for each worker
    params_list = [(strat_class, data_path, p) for p in param_dicts]

# Run in parallel
    with Pool(n_workers) as pool:
        results = pool.map(run_strategy_backtest, params_list)

# Sort by Sharpe ratio
    results.sort(key=lambda x: x['sharpe'], reverse=True)
    return results

```bash

### Independent Strategy Execution

```python
def run_strategies_independent(strategies_config):
    """Run strategies independently and combine results."""
    import concurrent.futures

    def run_single(config):
        cerebro = bt.Cerebro()
        cerebro.adddata(bt.feeds.CSVData(dataname=config['data']))
        cerebro.addstrategy(config['strategy'], **config.get('params', {}))
        cerebro.broker.setcash(config.get('cash', 100000))

        result = cerebro.run()[0]
        return {
            'name': config['name'],
            'return': cerebro.broker.getvalue() / config.get('cash', 100000) - 1,
        }

    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        futures = [executor.submit(run_single, config) for config in strategies_config]
        results = [f.result() for f in concurrent.futures.as_completed(futures)]

    return results

```bash

## Risk Management Across Strategies

### Portfolio-Level Stop Loss

```python
class PortfolioStopLoss(bt.Strategy):
    """Implement portfolio-level stop loss across all strategies."""

    params = (
        ('max_drawdown', 0.15),  # 15% max drawdown
        ('stop_trading', False),
    )

    def __init__(self):
        self.peak_value = self.broker.getvalue()
        self.trading_stopped = False

    def next(self):
        current_value = self.broker.getvalue()

# Update peak
        if current_value > self.peak_value:
            self.peak_value = current_value

# Calculate drawdown
        drawdown = (self.peak_value - current_value) / self.peak_value

# Stop trading if max drawdown exceeded
        if drawdown >= self.p.max_drawdown and not self.trading_stopped:
            self.trading_stopped = True
            self.close()  # Close all positions

        if self.trading_stopped:
            return  # Skip all trading logic

# Normal strategy logic here
        self.execute_strategy()

    def execute_strategy(self):
        """Override in subclass."""
        pass

```bash

### Position-Level Risk Controls

```python
class MultiStrategyPositionSizer(bt.Sizer):
    """Size positions considering all strategy positions."""

    params = (
        ('max_total_exposure', 0.95),  # Max 95% of portfolio
        ('max_single_position', 0.20),  # Max 20% per position
    )

    def _getsizing(self, comminfo, cash, data, isbuy):
        total_value = self.strategy.broker.getvalue()
        current_exposure = abs(self.strategy.broker.getvalue() -
                               self.strategy.broker.get_cash()) / total_value

# Calculate available capacity
        available = self.p.max_total_exposure - current_exposure
        if available <= 0:
            return 0  # No capacity for new position

# Calculate position size
        max_size = (total_value * min(available, self.p.max_single_position))
        price = data.close[0]
        return int(max_size / price) if price > 0 else 0

```bash

## Complete Example

### Multi-Strategy Portfolio System

```python
import backtrader as bt
import pandas as pd
from datetime import datetime

# Strategy 1: Momentum

class MomentumStrategy(bt.Strategy):
    """Momentum-based strategy using RSI."""

    params = (('rsi_period', 14), ('oversold', 30), ('overbought', 70))

    def __init__(self):
        self.rsi = bt.indicators.RSI(self.data.close, period=self.p.rsi_period)
        self.signal = 0  # 1=buy, -1=sell, 0=hold

    def next(self):
        if self.rsi[0] < self.p.oversold and not self.position:
            self.buy(size=self.sizer.get_size(self))
            self.signal = 1
        elif self.rsi[0] > self.p.overbought and self.position:
            self.close()
            self.signal = -1
        else:
            self.signal = 0


# Strategy 2: Mean Reversion

class MeanReversionStrategy(bt.Strategy):
    """Mean reversion using Bollinger Bands."""

    params = (('period', 20), ('devfactor', 2.0))

    def __init__(self):
        self.boll = bt.indicators.BollingerBands(
            self.data.close,
            period=self.p.period,
            devfactor=self.p.devfactor
        )
        self.signal = 0

    def next(self):
        if self.data.close[0] < self.boll.lines.bot[0] and not self.position:
            self.buy(size=self.sizer.get_size(self))
            self.signal = 1
        elif self.data.close[0] > self.boll.lines.top[0] and self.position:
            self.close()
            self.signal = -1
        else:
            self.signal = 0


# Strategy 3: Trend Following

class TrendFollowingStrategy(bt.Strategy):
    """Trend following using moving average crossover."""

    params = (('fast_period', 10), ('slow_period', 30))

    def __init__(self):
        self.fast_ma = bt.indicators.SMA(self.data.close, period=self.p.fast_period)
        self.slow_ma = bt.indicators.SMA(self.data.close, period=self.p.slow_period)
        self.crossover = bt.ind.CrossOver(self.fast_ma, self.slow_ma)
        self.signal = 0

    def next(self):
        if self.crossover[0] > 0 and not self.position:
            self.buy(size=self.sizer.get_size(self))
            self.signal = 1
        elif self.crossover[0] < 0 and self.position:
            self.close()
            self.signal = -1
        else:
            self.signal = 0


# Portfolio Manager

class MultiStrategyPortfolio(bt.Strategy):
    """Portfolio manager combining multiple strategies."""

    params = (
        ('strategies', []),
        ('weights', None),  # None = equal weight
        ('rebalance_freq', 5),
    )

    def __init__(self):

# Store strategy instances
        self.strategy_instances = []
        for strat_params in self.p.strategies:
            strat_class = strat_params['class']
            strat_instance = strat_class(**strat_params.get('params', {}))
            self.strategy_instances.append(strat_instance)

# Set weights
        if self.p.weights is None:
            self.weights = [1.0 / len(self.strategy_instances)] *len(self.strategy_instances)
        else:
            self.weights = self.p.weights

# Track allocations
        self.allocations = [0.0]*len(self.strategy_instances)
        self.last_rebalance = 0

    def next(self):

# Get signals from all strategies
        signals = []
        for i, strat in enumerate(self.strategy_instances):

# Execute strategy logic
            strat.next()
            signals.append(strat.signal)

# Rebalance if needed
        if len(self.data) - self.last_rebalance >= self.p.rebalance_freq:
            self.rebalance()
            self.last_rebalance = len(self.data)

    def rebalance(self):
        """Rebalance portfolio based on target weights."""
        total_value = self.broker.getvalue()

        for i, weight in enumerate(self.weights):
            target_value = total_value*weight
            current_value = self.get_strategy_value(i)

            if current_value < target_value*0.95:  # Underweight

# Buy to reach target
                pass
            elif current_value > target_value*1.05:  # Overweight

# Sell to reach target
                pass

    def get_strategy_value(self, index):
        """Get current value of strategy at index."""

# Implementation depends on tracking method
        return self.broker.getvalue() / len(self.strategy_instances)


# Custom Position Sizer

class EqualWeightSizer(bt.Sizer):
    """Equal weight position sizer for multi-strategy portfolio."""

    params = (('num_strategies', 3), ('target_weight', 0.33))

    def _getsizing(self, comminfo, cash, data, isbuy):
        total_value = self.strategy.broker.getvalue()
        target_value = total_value*self.p.target_weight
        return int(target_value / data.close[0]) if data.close[0] > 0 else 0


# Run the portfolio

def run_multi_strategy_portfolio(data_path):
    """Run multi-strategy portfolio backtest."""

    cerebro = bt.Cerebro()

# Add data
    data = bt.feeds.CSVData(dataname=data_path)
    cerebro.adddata(data)

# Add portfolio strategy
    strategies_config = [
        {'class': MomentumStrategy, 'params': {'rsi_period': 14}},
        {'class': MeanReversionStrategy, 'params': {'period': 20}},
        {'class': TrendFollowingStrategy, 'params': {'fast_period': 10, 'slow_period': 30}},
    ]

    cerebro.addstrategy(
        MultiStrategyPortfolio,
        strategies=strategies_config,
        weights=[0.3, 0.3, 0.4],  # Custom weights
    )

# Set broker
    cerebro.broker.setcash(100000)
    cerebro.broker.setcommission(commission=0.001)

# Add sizer
    cerebro.addsizer(EqualWeightSizer, num_strategies=3, target_weight=0.33)

# Add analyzers
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')

# Run
    results = cerebro.run()
    strat = results[0]

# Print results
    print("\n" + "="*50)
    print("Multi-Strategy Portfolio Results")
    print("="*50)
    print(f"Final Value: {cerebro.broker.getvalue():.2f}")
    print(f"Sharpe Ratio: {strat.analyzers.sharpe.get_analysis().get('sharperatio', 'N/A')}")
    print(f"Max Drawdown: {strat.analyzers.drawdown.get_analysis()['max']['drawdown']:.2f}%")
    print(f"Annual Return: {strat.analyzers.returns.get_analysis().get('rnorm', 0):.2%}")
    print("="* 50)

    return results


if __name__ == '__main__':

# Run the portfolio
    results = run_multi_strategy_portfolio('data.csv')

```bash

## Best Practices

### Strategy Selection

1. **Diversification**: Combine strategies with different market conditions
2. **Low Correlation**: Select strategies that don't move together
3. **Complementary Signals**: Use strategies that confirm each other

### Risk Management

1. **Portfolio-Level Controls**: Implement maximum drawdown limits
2. **Position Sizing**: Use consistent sizing across strategies
3. **Capital Allocation**: Don't over-allocate to similar strategies

### Performance Monitoring

1. **Individual Metrics**: Track each strategy separately
2. **Combined Metrics**: Monitor portfolio-level performance
3. **Attribution Analysis**: Understand which strategies contribute most

## Next Steps

- [Performance Optimization](performance-optimization.md) - Speed up your backtests
- [TS Mode Guide](ts-mode.md) - Time series optimization
- [CS Mode Guide](cs-mode.md) - Cross-section mode for portfolios