Backtesting Crypto Trading Strategies with Historical Data

Note: This tutorial assumes that you have basic knowledge of Python programming and have set up a Python environment with the necessary libraries installed (pandas, numpy, requests, ccxt, etc.).

  1. 1. Introduction
  2. Step 1: Fetching historical data
  3. Step 2: Implementing the moving average crossover strategy
  4. Step 3: Backtesting the strategy
  5. Step 4: Visualizing the backtest results
  6. Conclusion

1. Introduction

Backtesting is the process of testing a trading strategy using historical data to determine its performance and potential profitability. In this tutorial, we will discuss how to backtest a simple moving average crossover strategy using Python.

Step 1: Fetching historical data

First, let’s fetch historical price data from a cryptocurrency exchange using the ccxt library. We will use daily data for this example.

import ccxt
import pandas as pd

exchange = ccxt.binance()
symbol = 'BTC/USDT'
timeframe = '1d'
limit = 1000

ohlcv_data = exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume']
df = pd.DataFrame(ohlcv_data, columns=columns)
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('timestamp', inplace=True)

Step 2: Implementing the moving average crossover strategy

The moving average crossover strategy generates a buy signal when a shorter-term moving average crosses above a longer-term moving average and a sell signal when the shorter-term moving average crosses below the longer-term moving average.

short_window = 50
long_window = 200

df['short_mavg'] = df['close'].rolling(window=short_window).mean()
df['long_mavg'] = df['close'].rolling(window=long_window).mean()

df['signal'] = 0
df.loc[df['short_mavg'] > df['long_mavg'], 'signal'] = 1
df.loc[df['short_mavg'] < df['long_mavg'], 'signal'] = -1

Step 3: Backtesting the strategy

To backtest the strategy, we will simulate trades based on the generated signals and calculate the total return over the backtest period.

1. Simulating trades

initial_balance = 10000
balance = initial_balance
position = 0

trades = []

for index, row in df.iterrows():
    if row['signal'] == 1 and position == 0:
        # Buy signal
        position = balance / row['close']
        balance = 0
        trades.append((index, 'buy', row['close']))
    elif row['signal'] == -1 and position > 0:
        # Sell signal
        balance = position * row['close']
        position = 0
        trades.append((index, 'sell', row['close']))

# Liquidate remaining position at the end of the backtest
if position > 0:
    balance = position * df.iloc[-1]['close']
    trades.append((df.index[-1], 'sell', df.iloc[-1]['close']))

# Calculate total return
total_return = (balance - initial_balance) / initial_balance

2: Analyzing the backtest results

To evaluate the performance of the strategy, we can calculate various performance metrics, such as the number of trades, win rate, average profit per trade, and annualized return.

import numpy as np

trade_df = pd.DataFrame(trades, columns=['timestamp', 'action', 'price'])
trade_df['profit'] = trade_df['price'].pct_change().shift(-1)
trade_df = trade_df[:-1]  # Remove the last liquidation trade
number_of_trades = len(trade_df)
winning_trades = trade_df[trade_df['profit'] > 0]
losing_trades = trade_df[trade_df['profit'] < 0]
win_rate = len(winning_trades) / number_of_trades
average_profit = trade_df['profit'].mean()
annualized_return = (1 + total_return) ** (365 / len(df)) - 1

print("Number of trades:", number_of_trades)
print("Win rate:", win_rate)
print("Average profit per trade:", average_profit)
print("Annualized return:", annualized_return)

Step 4: Visualizing the backtest results

To better understand the backtest results, we can create a simple plot of the historical price data, moving averages, and trade signals using matplotlib.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(15, 8))

# Historical price data and moving averages
ax.plot(df.index, df['close'], label='Close', alpha=0.5)
ax.plot(df.index, df['short_mavg'], label=f'Short MA ({short_window})', linestyle='--')
ax.plot(df.index, df['long_mavg'], label=f'Long MA ({long_window})', linestyle='--')

# Buy and sell signals
buy_signals = trade_df[trade_df['action'] == 'buy']
sell_signals = trade_df[trade_df['action'] == 'sell']
ax.scatter(buy_signals['timestamp'], buy_signals['price'], marker='^', color='g', label='Buy')
ax.scatter(sell_signals['timestamp'], sell_signals['price'], marker='v', color='r', label='Sell')

ax.legend(loc='best')
ax.set_title('Moving Average Crossover Backtest')
plt.show()

Conclusion

In this tutorial, we have demonstrated how to backtest a simple moving average crossover strategy using historical data and Python. Backtesting is an essential step in the development of any trading strategy, as it allows you to evaluate the strategy’s performance and make any necessary adjustments before deploying it in live trading. Keep in mind that past performance is not always indicative of future results, and you should always consider other factors such as risk management, position sizing, and diversification when designing and implementing a trading strategy.