Cryptocurrency Backtesting in Python: A Step-by-Step Guide for Traders

Introduction: In the rapidly evolving world of cryptocurrency trading, having an edge is essential. One proven method to gain this advantage is through backtesting. This article delves into the significance of backtesting for cryptocurrency strategies and introduces a comprehensive Python tool tailored for this purpose.

Why Backtesting is Crucial for Cryptocurrency Trading

Backtesting allows traders to simulate their trading strategies using historical data. This method offers a glimpse into potential strategy performance without risking actual capital. Especially in the volatile cryptocurrency market, backtesting provides insights that can be the difference between profit and loss.

Features of Our Advanced Cryptocurrency Backtester

  • Real-world Trading Fees Consideration: Most backtesters overlook trading fees, leading to skewed results. Our tool factors in these fees, ensuring a realistic representation of potential profits.
  • Customizable Strategy Integration: While our backtester comes with a default moving average crossover strategy, it’s designed for adaptability. Traders can seamlessly integrate their unique strategies, making the tool versatile and future-proof.
  • Versatile Data Management: Using yfinance, our backtester fetches up-to-date historical data. However, it’s also compatible with saved datasets, catering to offline usage or custom data preferences.
  • Visual Analytics: Our tool doesn’t just crunch numbers; it visualizes them. Detailed graphs highlight indicators, profit trajectories, and potential trading signals, offering traders a visual strategy assessment.

Here you can take a look at the full code:

import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pickle

# Constants for trading fees
MAKER_FEE = 0.0008  # 0.08%
TAKER_FEE = 0.001  # 0.1%

def fetch_data(tickers, start_date, end_date):
    """
    Fetch historical data for given tickers from yfinance.
    """
    data = {}
    for ticker in tickers:
        try:
            df = yf.download(ticker, start=start_date, end=end_date, interval="1m")
            if not df.empty and 'Close' in df.columns:
                data[ticker] = df['Close']
            else:
                print(f"'Close' column missing for {ticker}. Skipping...")
        except Exception as e:
            print(f"Error fetching data for {ticker}: {e}")
    return pd.DataFrame(data)

def calculate_volatility_and_return(price_data):
    """
    Calculate volatility and total return for the given price data.
    """
    daily_returns = price_data.pct_change().dropna()
    volatility = daily_returns.std()
    total_return = (price_data.iloc[-1] - price_data.iloc[0]) / price_data.iloc[0]
    return volatility, total_return

def backtest_moving_averages(price_data, s_window, l_window, check_interval):
    """
    Backtest using the moving average crossover strategy.
    """
    sig = pd.DataFrame(index=price_data.index)
    sig['price'] = price_data
    sig['short_mavg'] = price_data.rolling(window=s_window).mean()
    sig['long_mavg'] = price_data.rolling(window=l_window).mean()

    # Check for crossover in the last 'check_interval' minutes
    sig['crossed_above'] = (sig['short_mavg'] > sig['long_mavg']).rolling(window=check_interval).sum()
    sig['crossed_below'] = (sig['short_mavg'] <= sig['long_mavg']).rolling(window=check_interval).sum()

    # Simplified buy/sell signal detection
    sig['signal'] = 0.0
    mask = sig['short_mavg'][s_window:] > sig['long_mavg'][s_window:]
    sig.loc[mask.index, 'signal'] = np.where(mask, 1.0, 0.0)
    sig['positions'] = sig['signal'].diff()

    sig['daily_return'] = sig['price'].pct_change()
    sig['strategy_return'] = sig['signal'].shift(1) * sig['daily_return']
    sig['cumulative_return'] = (1 + sig['strategy_return']).cumprod()

    # Adjust returns for trading fees
    sig['strategy_return'] = np.where(sig['positions'] == 1.0, sig['strategy_return'] - TAKER_FEE, sig['strategy_return'])
    sig['strategy_return'] = np.where(sig['positions'] == -1.0, sig['strategy_return'] - MAKER_FEE, sig['strategy_return'])
    sig['cumulative_return'] = (1 + sig['strategy_return']).cumprod()

    return sig

def plot_results(data, signals, stock_symbol, short_win, long_win):
    """
    Plot the results including price, moving averages, and buy/sell signals.
    """
    sns.set_style("whitegrid")

    # Plot Price, Moving Averages, Buy and Sell signals
    plt.figure(figsize=(14, 7))
    plt.plot(data.index, data, label='Price', alpha=0.5)
    plt.plot(signals.index, signals['short_mavg'], label=f'Short {short_win}D MA', alpha=0.8)
    plt.plot(signals.index, signals['long_mavg'], label=f'Long {long_win}D MA', alpha=0.8)
    plt.plot(signals[signals['positions'] == 1.0].index, signals['short_mavg'][signals['positions'] == 1.0], '^',
             markersize=10, color='g', label='Buy Signal', alpha=1)
    plt.plot(signals[signals['positions'] == -1.0].index, signals['short_mavg'][signals['positions'] == -1.0], 'v',
             markersize=10, color='r', label='Sell Signal', alpha=1)
    plt.title(f'{stock_symbol} Moving Average Crossover Strategy')
    plt.xlabel('Date')
    plt.ylabel('Price')
    plt.legend(loc='best')
    plt.tight_layout()
    plt.show()

    # Plot Cumulative Returns
    plt.figure(figsize=(14, 7))
    plt.plot(signals.index, signals['cumulative_return'], label='Cumulative Return', alpha=0.8)
    plt.title(f'{stock_symbol} Cumulative Returns')
    plt.xlabel('Date')
    plt.ylabel('Cumulative Returns')
    plt.legend(loc='best')
    plt.tight_layout()
    plt.show()

# Parameters
crypto_tickers = [
    "BTC-USD", "ETH-USD", "DOT-USD", "DOGE-USD", "LTC-USD", "LINK-USD", "ADA-USD", "BCH-USD", "XLM-USD", "XRP-USD",
    "FIL-USD", "UNI-USD", "TRX-USD", "BSV-USD", "EOS-USD", "ZEC-USD", "BNB-USD", "XTZ-USD", "ATOM-USD", "DASH-USD",
    "MKR-USD", "NEO-USD", "OMG-USD", "ONT-USD", "VET-USD", "ZRX-USD", "BAT-USD", "REP-USD", "ICX-USD", "LSK-USD",
    "XMR-USD", "Aave-USD", "ALGO-USD", "CRV-USD", "DAI-USD", "MANA-USD", "SNX-USD", "SUSHI-USD", "USDC-USD",
    "USDT-USD", "WBTC-USD", "YFI-USD", "UMA-USD", "BTT-USD", "CEL-USD", "ENJ-USD", "IOST-USD", "KNC-USD",
    "MATIC-USD", "OKB-USD", "QTUM-USD", "RVN-USD", "SC-USD", "STMX-USD", "TUSD-USD", "WAVES-USD", "YFII-USD",
    "ZIL-USD", "DCR-USD", "REN-USD", "BAL-USD", "BAND-USD", "BNT-USD", "BUSD-USD", "CTSI-USD", "CVC-USD", "DGB-USD",
    "DMG-USD", "DRGN-USD", "ELF-USD", "ETC-USD", "FTM-USD", "GUSD-USD", "HNT-USD", "KAVA-USD", "KMD-USD", "LOOM-USD",
    "LRC-USD", "NKN-USD", "NMR-USD", "NPXS-USD", "NULS-USD", "OCEAN-USD", "PAXG-USD", "RLC-USD", "RSR-USD",
    "SAND-USD", "SKL-USD", "SRM-USD", "STORJ-USD", "SXP-USD", "THETA-USD", "TOMO-USD", "TRB-USD", "TWT-USD", "XEM-USD",
    "XVG-USD", "ZEN-USD", "ANKR-USD", "BTS-USD", "CKB-USD", "COTI-USD", "CRV-USD", "DNT-USD",
    "DOCK-USD", "DOT-USD", "DUSK-USD", "EGLD-USD", "ENG-USD", "FLM-USD", "FLOW-USD", "FOR-USD", "FRONT-USD", "FUND-USD",
    "GXC-USD", "HARD-USD", "HBAR-USD", "HIVE-USD", "HOT-USD", "ICP-USD", "INJ-USD", "IOTX-USD", "IRIS-USD",
    "JST-USD", "JUV-USD", "KAI-USD", "KSM-USD", "LIT-USD", "MIR-USD", "MTL-USD",
    "NAS-USD", "NAV-USD", "NBS-USD", "NEAR-USD", "NEXO-USD", "NGC-USD", "NXT-USD", "OGN-USD", "OM-USD", "ONE-USD",
    "ONT-USD", "ORBS-USD", "ORN-USD", "OXT-USD", "POA-USD", "POWR-USD", "PUNDIX-USD", "QKC-USD",
    "QNT-USD", "RARI-USD", "RDN-USD", "REEF-USD", "RING-USD", "ROSE-USD", "RSV-USD", "RUNE-USD", "SNT-USD",
    "STAKE-USD", "STMX-USD", "SUN-USD", "SUSHI-USD", "SWRV-USD", "SXP-USD", "SYS-USD", "TFUEL-USD", "TRU-USD",
    "TRX-USD",
    "TUSD-USD", "UMA-USD", "UNI-USD", "USDC-USD", "USDT-USD", "UTK-USD", "WAN-USD", "WAVES-USD", "WNXM-USD", "WRX-USD",
    "XEM-USD", "XLM-USD", "XMR-USD", "XRP-USD", "XTZ-USD", "XVG-USD", "YFI-USD", "YFII-USD", "ZEC-USD", "ZEN-USD",
    "ZIL-USD", "ZRX-USD"
]
end_date = pd.Timestamp.now()
start_date = end_date - pd.Timedelta(days=7)
short_win = 100
long_win = 300
check_interval = 15

# Fetch data
price_data = fetch_data(crypto_tickers, start_date, end_date)
pickle.dump(price_data, open("trading_data.pkl", "wb"))
# load data from save
#price_data = pickle.load(open("trading_data.pkl", "rb"))

# Calculate volatility
#volatility, total_return = calculate_volatility_and_return(price_data)

# Filter cryptocurrencies with a neutral or positive price (e.g., price change of 0% or higher over the 7 days)
#neutral_or_positive_tickers = total_return[total_return >= 0].index.tolist()

# Rank the neutral or positive cryptocurrencies by their volatility
#most_volatile_neutral_or_positive_cryptos = volatility[neutral_or_positive_tickers].sort_values(ascending=False)

# Sort the needed items that meet the volatility specification
#filtered_price_data_items = [(ticker, data) for ticker, data in price_data.items() if
                             #ticker in most_volatile_neutral_or_positive_cryptos.index]

#for ticker, data in filtered_price_data_items:
for ticker, data in price_data.items():
    signals = backtest_moving_averages(data, short_win, long_win, check_interval)  # Use 'data' directly
    plot_results(data, signals, ticker, short_win, long_win)

The Edge of Cryptocurrency Trading

Cryptocurrencies, with their dynamic nature, present numerous profit opportunities. Free from traditional trading constraints like pattern day trading rules, crypto traders enjoy unparalleled flexibility. Backtesting, especially with our advanced Python tool, ensures they make the most of this freedom.

Conclusion

In the competitive realm of cryptocurrency trading, being prepared is half the battle. With our Python backtester, traders can test, refine, and perfect their strategies, ensuring they’re always one step ahead in the crypto game.

Leave a Comment

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

Scroll to Top