Machine Learning how to Concepts Backtesting a Strategy: A Backtrader Script for Crypto Trend Following

Backtesting a Strategy: A Backtrader Script for Crypto Trend Following

Complete Guide to Building a Trend Following Bot with Backtrader.

Why Backtest Cryptocurrency Strategies?

Before deploying real capital to any trading strategy, backtesting allows you to evaluate performance against historical data. For crypto traders, this is critical because:

  • Validate Logic: Test if your strategy’s rules are sound before risking money
  • Quantify Risk: Measure maximum drawdown, volatility, and win rates
  • Identify Weaknesses: Spot patterns where your strategy fails (e.g., sideways markets)
  • Optimize Parameters: Find the best moving average periods, stop-loss levels, or position sizes
  • Build Confidence: Psychological assurance before live trading
  • Avoid Overtrading: Discipline enforced by clear, rule-based signals
⚠️ Critical Caveat: Backtesting is NOT a guarantee of future performance. Markets change, black swan events occur, and past results don’t predict future returns. The best backtested strategies can fail in live trading due to slippage, liquidity constraints, and emotional execution errors.


Introduction to Backtrader

Backtrader is a Python framework that simplifies building, testing, and optimizing trading strategies. It handles the complexity of simulating trades, managing positions, and calculating performance metrics.

Why Choose Backtrader?

πŸ“Š Professional-Grade

Used by institutional traders and quants for serious backtesting, not just hobbyists

πŸ”Œ Modular Design

Easily plug in custom indicators, data feeds, and broker integrations

πŸ“ˆ Built-in Indicators

50+ technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands, etc.) included

πŸš€ Live Trading

Transition from backtesting to live trading with minimal code changes

Installation

$ pip install backtrader
$ pip install pandas
$ pip install numpy
$ pip install matplotlib # For visualizations

Core Concepts

πŸ“Œ Cerebro Engine: The main simulation engine that orchestrates backtesting. It manages data feeds, strategies, brokers, and execution.
πŸ“Œ Strategy: Your custom trading logic. Inherits from bt.Strategy and defines buy/sell rules via the next() method.
πŸ“Œ Data Feeds: Historical OHLCV (open, high, low, close, volume) data. Backtrader supports CSV, Pandas DataFrames, and live feeds.
πŸ“Œ Broker: Simulates a real broker with commission, slippage, cash management, and margin handling.
πŸ“Œ Indicators: Technical analysis tools (moving averages, RSI, etc.) calculated on each new bar.

Strategy Design: Simple Trend Following

For this guide, we’ll build a Moving Average Crossover strategy optimized for cryptocurrency trading. This is one of the most tested and robust trend-following approaches.

Trading Logic

  • 🟒 BUY Signal: When the 20-period EMA crosses above the 50-period SMA (bullish momentum)
  • πŸ”΄ SELL Signal: When the 20-period EMA crosses below the 50-period SMA (bearish momentum)
  • πŸ’° Position Sizing: Risk 2% of portfolio per trade
  • πŸ›‘ Stop Loss: 5% below entry price to limit downside
  • πŸ’Ή Take Profit: Exit at 2:1 risk-reward ratio or on opposite signal
🎯 Why This Works: Moving averages smooth out noise and capture sustained trends. The EMA/SMA combination filters out false signals in ranging markets while catching real breakouts. Cryptocurrency’s high volatility makes this particularly effective.

Performance Expectations

Based on backtests of this strategy on BTC/USDT hourly data (2020-2024):

Metric Bull Market Bear Market Sideways Market
Win Rate 68% 55% 35%
Max Drawdown -12% -25% -18%
Annual Return 65% -15% 8%

Note: These are illustrative figures. Actual results depend on parameters, market conditions, and data quality.

Preparing Historical Data

Data Sources for Crypto

  • Binance: Free historical OHLCV data via API or CSV download
  • CoinGecko: Free but limited historical data (1-day only)
  • Kraken: High-quality historical data for major pairs
  • Polygon.io: Paid service with comprehensive crypto data
  • Backtrader Community: Pre-loaded datasets in sample code

CSV Data Format

Your CSV should have columns: Date, Open, High, Low, Close, Volume

Date,Open,High,Low,Close,Volume
2024-01-01 00:00:00,42150.50,42800.75,41900.25,42500.00,850000000
2024-01-01 01:00:00,42500.00,42950.25,42350.00,42700.50,920000000
2024-01-01 02:00:00,42700.50,43100.00,42600.75,42950.00,1050000000

Loading Data into Backtrader

# Method 1: Using Pandas DataFrame
import pandas as pd
import backtrader as bt

df = pd.read_csv('btc_hourly.csv', index_col=0, parse_dates=True)

class PandasData(bt.feeds.PandasData):
    params = (
        ('datetime', None),
        ('open', 'Open'),
        ('high', 'High'),
        ('low', 'Low'),
        ('close', 'Close'),
        ('volume', 'Volume'),
        ('openinterest', None),
    )

cerebro = bt.Cerebro()
data = PandasData(dataname=df)
cerebro.adddata(data)

# Method 2: Using GenericCSVData for direct CSV loading
data = bt.feeds.GenericCSVData(
    dataname='btc_hourly.csv',
    dtformat=('%Y-%m-%d %H:%M:%S'),
    openinterest=-1,
    fromdate=dt.datetime(2023, 1, 1),
    todate=dt.datetime(2024, 12, 31)
)
cerebro.adddata(data)
πŸ’‘ Tip: Always inspect your data for gaps, missing values, or price spikes before backtesting. Use df.isna().sum() and df.describe() to validate.

Complete Backtrader Implementation

Here’s a production-ready trend following strategy with full risk management:

#!/usr/bin/env python3
"""
Cryptocurrency Trend Following Strategy with Backtrader
Moving Average Crossover with Risk Management
"""

import backtrader as bt
import pandas as pd
import datetime as dt

class TrendFollowingStrategy(bt.Strategy):
    """
    Moving Average Crossover Strategy for Crypto
    
    Buy Signal: Fast MA (20 EMA) crosses above Slow MA (50 SMA)
    Sell Signal: Fast MA crosses below Slow MA
    Risk Management: 2% portfolio risk, 5% stop loss
    """
    
    params = (
        ('fast_ma', 20),           # EMA period for fast moving average
        ('slow_ma', 50),           # SMA period for slow moving average
        ('risk_percent', 2.0),     # Risk 2% of portfolio per trade
        ('stop_loss_pct', 5.0),    # Stop loss at 5% below entry
        ('take_profit_ratio', 2.0), # 2:1 reward:risk ratio
    )
    
    def __init__(self):
        """Initialize indicators and tracking variables"""
        
        # Moving averages
        self.fast_ma = bt.indicators.EMA(
            self.data.close, 
            period=self.params.fast_ma
        )
        self.slow_ma = bt.indicators.SMA(
            self.data.close, 
            period=self.params.slow_ma
        )
        
        # Crossover detection
        self.crossover = bt.indicators.CrossOver(self.fast_ma, self.slow_ma)
        
        # Tracking variables
        self.entry_price = None
        self.stop_loss = None
        self.take_profit = None
        self.order = None
        self.trade_count = 0
        self.win_count = 0
    
    def log(self, txt, dt=None):
        """Log trading activity"""
        dt = dt or self.datas[0].datetime.date(0)
        print(f'{dt.isoformat()} {txt}')
    
    def notify_order(self, order):
        """Called when broker processes an order"""
        if order.status in [order.Submitted, order.Accepted]:
            return
        
        if order.status in [order.Completed]:
            if order.isbuy():
                self.entry_price = order.executed.price
                self.trade_count += 1
                
                # Calculate stop loss and take profit
                risk = self.entry_price * (self.params.stop_loss_pct / 100.0)
                self.stop_loss = self.entry_price - risk
                self.take_profit = self.entry_price + (risk * self.params.take_profit_ratio)
                
                self.log(
                    f'BUY EXECUTED @ {order.executed.price:.2f} | '
                    f'Stop: {self.stop_loss:.2f} | '
                    f'Target: {self.take_profit:.2f}'
                )
            
            elif order.issell():
                self.log(f'SELL EXECUTED @ {order.executed.price:.2f}')
        
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
        
        self.order = None
    
    def notify_trade(self, trade):
        """Called when a trade closes"""
        if trade.isclosed:
            profit_pct = (trade.pnl / trade.value) * 100
            self.log(
                f'TRADE CLOSED | Profit: {trade.pnl:.2f} ({profit_pct:.2f}%) | '
                f'Duration: {trade.barlen} bars'
            )
            
            if trade.pnl > 0:
                self.win_count += 1
    
    def next(self):
        """Called on each new bar; main strategy logic"""
        
        # Skip if order is pending
        if self.order:
            return
        
        # Exit if current price hits stop loss or take profit
        if self.position:
            if self.data.close[0] <= self.stop_loss: self.log(f'STOP LOSS HIT @ {self.data.close[0]:.2f}') self.close() elif self.data.close[0] >= self.take_profit:
                self.log(f'TAKE PROFIT HIT @ {self.data.close[0]:.2f}')
                self.close()
        
        # Entry signals (only if no position)
        if not self.position:
            
            # Golden Cross: Buy signal
            if self.crossover > 0:
                
                # Calculate position size based on 2% risk
                cash = self.broker.getcash()
                risk_amount = cash * (self.params.risk_percent / 100.0)
                entry_price = self.data.close[0]
                stop_distance = entry_price * (self.params.stop_loss_pct / 100.0)
                size = int(risk_amount / stop_distance)
                
                if size > 0:
                    self.order = self.buy(size=size)
                    self.log(
                        f'BUY SIGNAL | EMA({self.params.fast_ma}) crosses above '
                        f'SMA({self.params.slow_ma})'
                    )
        
        # Exit signals (only if in position)
        elif self.position:
            
            # Death Cross: Sell signal
            if self.crossover < 0: self.order = self.sell() self.log( f'SELL SIGNAL | EMA({self.params.fast_ma}) crosses below ' f'SMA({self.params.slow_ma})' ) def stop(self): """Called when backtest is complete""" win_rate = (self.win_count / self.trade_count * 100) if self.trade_count > 0 else 0
        
        self.log(f'\n{"="*50}')
        self.log(f'FINAL RESULTS')
        self.log(f'{"="*50}')
        self.log(f'Total Trades: {self.trade_count}')
        self.log(f'Winning Trades: {self.win_count}')
        self.log(f'Win Rate: {win_rate:.2f}%')
        self.log(f'Final Portfolio Value: ${self.broker.getvalue():.2f}')
        self.log(f'{"="*50}\n')


def run_backtest(data_file, initial_cash=100000):
    """
    Run the backtest
    
    Args:
        data_file: Path to CSV with OHLCV data
        initial_cash: Starting portfolio size (default $100,000)
    """
    
    cerebro = bt.Cerebro()
    
    # Add strategy
    cerebro.addstrategy(TrendFollowingStrategy)
    
    # Load data
    df = pd.read_csv(data_file, index_col=0, parse_dates=True)
    
    data = bt.feeds.PandasData(
        dataname=df,
        fromdate=dt.datetime(2023, 1, 1),
        todate=dt.datetime(2024, 12, 31)
    )
    
    cerebro.adddata(data)
    
    # Broker settings
    cerebro.broker.setcash(initial_cash)
    cerebro.broker.setcommission(commission=0.0005)  # 0.05% trading fee
    
    # Run backtest
    print(f'Starting Portfolio Value: ${cerebro.broker.getvalue():.2f}')
    print(f'Starting Cash: ${initial_cash:,.2f}\n')
    
    results = cerebro.run()
    
    final_value = cerebro.broker.getvalue()
    total_return = ((final_value - initial_cash) / initial_cash) * 100
    
    print(f'\nFinal Portfolio Value: ${final_value:.2f}')
    print(f'Total Return: {total_return:.2f}%')
    
    # Plot results
    cerebro.plot(style='candlestick')


if __name__ == '__main__':
    run_backtest('btc_hourly.csv', initial_cash=100000)

βœ… Key Features in This Code:

  • Automatic position sizing based on 2% portfolio risk
  • Dynamic stop loss and take profit calculation
  • Complete trade logging for analysis
  • Win rate tracking
  • Realistic commission (0.05% on Binance)
  • Easy parameter optimization

Strategy Performance Comparison

Different trading approaches have vastly different risk-return profiles. Here’s how our trend following strategy compares:

12-Month Cumulative Returns by Strategy Type

Cryptocurrency Strategy Comparison Chart

Chart shows that Moving Average Crossover is volatile but can achieve 120% returns in trending markets, while Trend Following RSI provides steadier 75% returns with lower volatility.

Parameter Optimization

You can optimize strategy parameters using Backtrader’s optimization engine:

def run_optimization():
    """Optimize moving average periods"""
    
    cerebro = bt.Cerebro()
    
    # Load data
    df = pd.read_csv('btc_hourly.csv', index_col=0, parse_dates=True)
    data = bt.feeds.PandasData(dataname=df)
    cerebro.adddata(data)
    
    # Broker settings
    cerebro.broker.setcash(100000)
    cerebro.broker.setcommission(0.0005)
    
    # Optimize parameters
    strats = cerebro.optstrategy(
        TrendFollowingStrategy,
        fast_ma=range(10, 30, 5),   # Test 10, 15, 20, 25 EMA periods
        slow_ma=range(40, 100, 10),  # Test 40, 50, 60, ... 90 SMA periods
    )
    
    results = cerebro.run()
    
    # Find best result
    for strat in results:
        final_value = strat[0].broker.getvalue()
        print(f'Fast MA: {strat[0].params.fast_ma}, '
              f'Slow MA: {strat[0].params.slow_ma}, '
              f'Final Value: ${final_value:.2f}')


# Find best parameters
run_optimization()

Testing Different Time Periods

Critical: Test across different market phases to avoid overfitting:

  • Bull Market (2020-2021): Most trend followers excel, but may give false signals
  • Bear Market (2022-2023): Tests strategy’s downside protection and stop losses
  • Sideways Market (Q1 2024): Reveals whipsaws and false signals
  • Recovery (Q4 2024): Shows how strategy adapts to changing conditions

Evaluating Backtest Results

Key Performance Metrics

Metric Definition Good Target
Cumulative Return Total profit as % of starting capital >20% annually
Sharpe Ratio Return per unit of risk (higher is better) >1.0
Max Drawdown Largest peak-to-trough decline <-25%
Win Rate % of profitable trades >50%
Risk/Reward Avg profit Γ· Avg loss >1.5:1
Profit Factor Gross profit Γ· Gross loss >1.5

Analyzing Results

import matplotlib.pyplot as plt

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

# Extract metrics from broker
portfolio_value = strat.broker.getvalue()
returns = (portfolio_value - 100000) / 100000 * 100

# Plot equity curve
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

# Portfolio value over time
ax1.plot(strat.data.datetime.datetime(), strat.broker.getvalue())
ax1.set_title('Portfolio Value Over Time')
ax1.set_ylabel('Value ($)')
ax1.grid()

# Drawdown
ax2.bar(strat.data.datetime.datetime(), strat.broker.getvalue(), alpha=0.5)
ax2.set_title('Daily Returns')
ax2.set_ylabel('Return (%)')
ax2.set_xlabel('Date')
ax2.grid()

plt.tight_layout()
plt.show()

Common Backtesting Pitfalls to Avoid

🎯 Overfitting

Problem: Optimizing parameters specifically for historical data.
Solution: Test on out-of-sample data and use walk-forward analysis.

πŸ‘€ Look-Ahead Bias

Problem: Using future information not available at the time.
Solution: Only use data available at each bar, not closing prices.

πŸ’΅ Ignoring Costs

Problem: Not accounting for commissions and slippage.
Solution: Include realistic trading costs (0.05-0.1% for crypto).

βš–οΈ Survivorship Bias

Problem: Only testing assets that currently exist.
Solution: Include delisted coins to get realistic results.

Reality Checks Before Live Trading

  • Trade During Different Timeframes: Test on 1H, 4H, 1D data separatelyβ€”results differ significantly
  • Include Extreme Volatility: Add tests during crypto crashes (March 2020, May 2021, Nov 2022)
  • Check Execution Slippage: Reduce expected returns by 0.5-1% to account for real execution delays
  • Validate on Recent Data: Make sure your best parameters work on the most recent 3-6 months
  • Paper Trade First: Run the strategy live-simulated on a paper account for 2-4 weeks before using real capital
⚠️ The Backtester’s Dilemma: A strategy backtested on 5 years of data with stellar returns might fail miserably in live trading because market regimes change. Always expect live results to be 30-50% worse than backtest results.

Next Steps

Backtesting is an essential skill for any systematic trader. With Backtrader, you can quickly validate your trading ideas, optimize parameters, and gain confidence before risking real capital.

βœ… Key Takeaways:

  • Backtrader provides a professional framework for crypto strategy testing
  • Simple trend following beats complex overoptimized strategies
  • Always test across multiple market phases and timeframes
  • Include realistic costs (commissions, slippage)
  • Live results will likely be 30-50% worse than backtests
  • Paper trading is essential before deploying real capital

Your Next Steps

  1. Download Data: Get BTC/USDT historical data from Binance (free)
  2. Install Libraries: Run the pip install commands above
  3. Run the Code: Copy the implementation and backtest on your data
  4. Optimize: Try different MA periods (10/30, 20/50, 30/200)
  5. Validate: Test on multiple coins (BTC, ETH, SOL) and timeframes
  6. Paper Trade: Run live-simulated trading for 2-4 weeks
  7. Deploy: When confident, start with 1-2% of capital on your best setup

Advanced Topics to Explore

  • Multi-timeframe confirmation (trade 1H signals confirmed by 4H trend)
  • Machine learning for signal generation
  • Advanced risk management (Kelly Criterion, volatility-adjusted sizing)
  • Walk-forward analysis for robust parameter selection
  • Integration with live Binance API for automated trading
πŸš€ Final Thought: The goal of backtesting isn’t to find the holy grail of trading strategiesβ€”it’s to validate that your approach has an edge and is robust enough to survive real market conditions. Remember: trading is 10% the strategy and 90% the execution and risk management. A mediocre strategy executed flawlessly beats a perfect strategy executed poorly.
Backtesting a Strategy: A Backtrader Script for Crypto Trend Following

Last Updated: January 2026

Leave a Reply

Your email address will not be published. Required fields are marked *