MultiBacktest from backtesting.lib runs a single strategy against a list of OHLCV DataFrames in parallel, returning per-instrument statistics in a single pd.DataFrame. It is designed for comparing strategy performance across many instruments or asset classes.
MultiBacktest constructor
from backtesting.lib import MultiBacktest
btm = MultiBacktest(df_list, strategy_cls, **kwargs)
| Parameter | Description |
|---|
df_list | List of OHLCV pd.DataFrame objects, one per instrument |
strategy_cls | The Strategy subclass to run on each instrument |
**kwargs | Additional keyword arguments forwarded to Backtest.__init__() (e.g. cash, commission, margin) |
Running across multiple instruments
MultiBacktest.run(**kwargs) returns a pd.DataFrame where each column corresponds to one instrument (indexed 0, 1, 2, …) and each row is a stats key:
from backtesting import Strategy
from backtesting.lib import MultiBacktest, crossover
from backtesting.test import EURUSD, BTCUSD
def SMA(series, n):
import pandas as pd
return pd.Series(series).rolling(n).mean().values
class SmaCross(Strategy):
n1 = 10
n2 = 20
def init(self):
self.sma1 = self.I(SMA, self.data.Close, self.n1)
self.sma2 = self.I(SMA, self.data.Close, self.n2)
def next(self):
if crossover(self.sma1, self.sma2):
self.buy()
elif crossover(self.sma2, self.sma1):
self.position.close()
btm = MultiBacktest([EURUSD, BTCUSD], SmaCross, cash=10_000, commission=.002)
# Returns pd.DataFrame: rows = stat keys, columns = instrument index
stats_df = btm.run(n1=10, n2=20)
print(stats_df)
Keyword arguments to run() are forwarded to Strategy as parameter overrides — equivalent to calling bt.run(n1=10, n2=20) on each instrument individually.
Parallel execution
Under the hood, MultiBacktest.run() uses Python’s multiprocessing pool to distribute each instrument’s backtest across available CPU cores:
# Internally, MultiBacktest.run() uses:
from . import Pool
with Pool() as pool:
results = pool.imap(self._mp_task_run, ...)
No configuration is required — parallelism is automatic. Instruments with zero trades are returned as None columns in the result DataFrame.
On Windows or when running inside Jupyter notebooks, place the MultiBacktest.run() call inside a if __name__ == '__main__': guard in scripts to avoid multiprocessing errors:if __name__ == '__main__':
stats_df = btm.run()
Full example: two currency pairs
from backtesting import Strategy
from backtesting.lib import MultiBacktest, crossover
from backtesting.test import EURUSD, BTCUSD, SMA
class SmaCross(Strategy):
n1 = 10
n2 = 20
def init(self):
self.sma1 = self.I(SMA, self.data.Close, self.n1)
self.sma2 = self.I(SMA, self.data.Close, self.n2)
def next(self):
if crossover(self.sma1, self.sma2):
self.position.close()
self.buy()
elif crossover(self.sma2, self.sma1):
self.position.close()
self.sell()
btm = MultiBacktest(
[EURUSD, BTCUSD],
SmaCross,
cash=10_000,
commission=.002
)
stats_df = btm.run(n1=10, n2=20)
print(stats_df.T[['Equity Final [$]', 'Sharpe Ratio', '# Trades', 'Win Rate [%]']])
Example output:
Equity Final [$] Sharpe Ratio # Trades Win Rate [%]
0 11234.50 0.61 42 47.62
1 18920.30 0.83 38 52.63
Optimization across instruments
MultiBacktest.optimize(**kwargs) returns a pd.DataFrame where each column is the heatmap Series from one instrument:
heatmap_df = btm.optimize(
n1=range(5, 30, 5),
n2=range(10, 60, 5),
constraint=lambda p: p.n1 < p.n2,
maximize='Sharpe Ratio',
return_heatmap=True,
return_optimization=False
)
MultiBacktest.optimize() runs each instrument’s optimization sequentially (since Backtest.optimize() already parallelizes internally). The run() method, by contrast, parallelizes across instruments.
To aggregate heatmaps across instruments (e.g. find parameters that perform well on average):
from backtesting.lib import plot_heatmaps
# Average heatmap across all instruments
combined_heatmap = heatmap_df.mean(axis=1)
plot_heatmaps(combined_heatmap, agg='mean')
Filtering results
Instruments with zero trades produce None entries. Filter them out before analysis:
stats_df = btm.run(n1=10, n2=20)
# Drop instruments with no trades
active = stats_df.dropna(axis=1)
print(active.loc['Sharpe Ratio'].sort_values(ascending=False))
Scaling to many instruments
For larger universe backtests, pass a list of DataFrames loaded from files or a data provider:
import pandas as pd
from backtesting.lib import MultiBacktest
tickers = ['AAPL', 'MSFT', 'GOOG', 'AMZN', 'TSLA']
dataframes = [pd.read_csv(f'data/{t}.csv', index_col=0, parse_dates=True)
for t in tickers]
btm = MultiBacktest(dataframes, SmaCross, cash=10_000, commission=.002)
stats_df = btm.run(n1=10, n2=30)
# Summary across all tickers
print(stats_df.loc[['Equity Final [$]', 'Sharpe Ratio', 'Max. Drawdown [%]']])
MultiBacktest does not support different strategy parameters per instrument. If you need instrument-specific parameter sets, run individual Backtest instances in a loop or a multiprocessing.Pool manually.