Backtesting is more than simply checking if a trading strategy works on historical data — it’s about rigorously stress-testing your approach across various market conditions. In this article, we delve into a robust methodology for running multiple randomized backtests over random assets and time periods. This enables you to derive the statistical distribution of both market and strategy returns, revealing insights into the true performance and risk of your trading ideas.
Traditional backtests often run on a specific asset or a fixed timeframe. However, markets are dynamic, and a strategy that works on one asset or during one market cycle might not translate to another. Randomized backtesting addresses this by:
Our approach uses Python’s Backtrader library for strategy testing, along with yfinance for historical data acquisition. We add a dash of randomness to both asset selection and window period, then run each simulation in a loop. The aggregated results of multiple backtests provide the density distribution of returns, which is then visualized with overlapping histograms and mean markers.
Below is a comprehensive code example that demonstrates this process. The code:
import backtrader as bt
import yfinance as yf
import random
from datetime import datetime, timedelta
import warnings
='ignore')
warnings.filterwarnings(actionfrom tqdm import tqdm
import sys
import os
import contextlib
# Context manager for suppressing verbose outputs from yfinance
@contextlib.contextmanager
def suppress_stdout_stderr():
with open(os.devnull, 'w') as fnull:
= sys.stdout
old_stdout = sys.stderr
old_stderr try:
= fnull
sys.stdout = fnull
sys.stderr yield
finally:
= old_stdout
sys.stdout = old_stderr
sys.stderr
# EMA Crossover Strategy for comparison (optional)
class EMA_Crossover_Strategy(bt.Strategy):
= (
params 'short_window', 7),
('long_window', 30),
(
) def __init__(self):
self.data_close = self.datas[0].close
self.emas = bt.indicators.ExponentialMovingAverage(self.data_close, period=self.params.short_window)
self.emal = bt.indicators.ExponentialMovingAverage(self.data_close, period=self.params.long_window)
self.order = None
def next(self):
if not self.position:
= self.broker.get_cash()
cash = self.data_close[0]
asset_price = cash / asset_price * 0.99
position_size if self.emas[0] > self.emal[0]:
self.buy(size=position_size)
else:
if self.emas[0] < self.emal[0]:
self.close()
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
self.order = None
# RSI-based Mean Reversion Strategy: buy when oversold, sell when overbought.
class RSI_Strategy(bt.Strategy):
= (
params 'rsi_period', 14),
('overbought', 70),
('oversold', 30),
(
) def __init__(self):
self.data_close = self.datas[0].close
self.rsi = bt.indicators.RelativeStrengthIndex(self.data_close, period=self.params.rsi_period)
self.order = None
def next(self):
if not self.position:
if self.rsi[0] < self.params.oversold:
= self.broker.get_cash()
cash = self.data_close[0]
asset_price = cash / asset_price * 0.99
position_size self.buy(size=position_size)
else:
if self.rsi[0] > self.params.overbought:
self.close()
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
self.order = None
if __name__ == '__main__':
= [] # List to store market returns for each test
market_returns = [] # List to store strategy returns for each test
strategy_returns
= ['BTC-USD', 'ETH-USD', 'XRP-USD', 'LTC-USD', 'BCH-USD']
cryptos = datetime.strptime("2020-01-01", "%Y-%m-%d")
overall_start = datetime.strptime("2025-12-31", "%Y-%m-%d")
overall_end
= 100 # Number of backtest runs
iterations with tqdm(total=iterations) as pbar:
= 0
i while i < iterations:
try:
= random.choice(cryptos)
random_crypto = 60 # approx. 60 days for a 2-month window
two_months_days = overall_end - timedelta(days=two_months_days)
max_start_date = overall_start + (max_start_date - overall_start) * random.random()
random_start = random_start + timedelta(days=two_months_days)
random_end = random_start.strftime("%Y-%m-%d")
start_str = random_end.strftime("%Y-%m-%d")
end_str
with suppress_stdout_stderr():
= yf.download(random_crypto, start=start_str, end=end_str, progress=False)
data
if hasattr(data.columns, 'droplevel'):
= data.columns.droplevel(1).str.lower()
data.columns else:
= data.columns.str.lower()
data.columns
if data.empty:
+= 1
i 1)
pbar.update(continue
= bt.Cerebro()
cerebro = bt.feeds.PandasData(dataname=data)
data_feed
cerebro.adddata(data_feed)
# Uncomment one of the strategies to test:
cerebro.addstrategy(RSI_Strategy) # cerebro.addstrategy(EMA_Crossover_Strategy)
100.)
cerebro.broker.setcash(=0.001)
cerebro.broker.setcommission(commission
cerebro.run()
= 100 * (data.close[-1] / data.close[0] - 1)
market_return = cerebro.broker.getvalue() - 100
strategy_return
market_returns.append(market_return)
strategy_returns.append(strategy_return)
+= 1
i 1)
pbar.update(except Exception as e:
+= 1
i 1)
pbar.update(continue
# Visualize the distribution of returns using density histograms.
import matplotlib.pyplot as plt
import numpy as np
=(10, 6))
plt.figure(figsize= 25 # Number of histogram bins
bins =bins, density=True, alpha=0.5,
plt.hist(market_returns, bins='Market Returns', color='blue', edgecolor='black')
label=bins, density=True, alpha=0.5,
plt.hist(strategy_returns, bins='Strategy Returns', color='green', edgecolor='black')
label= np.mean(market_returns)
mean_market = np.mean(strategy_returns)
mean_strategy ='blue', linestyle='dashed', linewidth=2,
plt.axvline(mean_market, color=f'Market Mean: {mean_market:.2f}')
label='green', linestyle='dashed', linewidth=2,
plt.axvline(mean_strategy, color=f'Strategy Mean: {mean_strategy:.2f}')
label"Statistical Distribution of Market & Strategy Returns")
plt.title("Return Value")
plt.xlabel("Density")
plt.ylabel(
plt.legend() plt.show()
As you can see in the results, mean returns and also the distribution of returns show no obvious advantage over market returns for the simple classic strategies we tested for demonstration of the idea. You can test your strategies like this and analyze the results to see if they prove to be statistically significant returns over the market.
tqdm
for progress tracking and
custom context managers to suppress noisy output creates a clean,
automated testing environment.By embracing the randomness inherent in markets — randomized asset selection and time windows — you can perform a more comprehensive and robust evaluation of your strategy. This method gives you statistical insight into performance variability, paving the way for more confident decision-making and risk management. With Backtrader and the above framework, you’re well-equipped to “crack the code” on how your strategy might perform across a myriad of market conditions.