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
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 pandas
$ pip install numpy
$ pip install matplotlib # For visualizations
Core Concepts
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
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)
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

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
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
- Download Data: Get BTC/USDT historical data from Binance (free)
- Install Libraries: Run the pip install commands above
- Run the Code: Copy the implementation and backtest on your data
- Optimize: Try different MA periods (10/30, 20/50, 30/200)
- Validate: Test on multiple coins (BTC, ETH, SOL) and timeframes
- Paper Trade: Run live-simulated trading for 2-4 weeks
- 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
