← Back to Home
Crypto Trading Strategy using the Sharpe Ratio with Python Code

Crypto Trading Strategy using the Sharpe Ratio with Python Code

Cryptocurrencies like Ethereum and Bitcoin are known for their high volatility, presenting both opportunities and significant risks for traders. Navigating these turbulent markets often requires a disciplined approach. Quantitative trading strategies, which rely on mathematical models and historical data to make trading decisions, offer one such framework.

This article explores a specific quantitative strategy using Sharpe Ratio to generate long and short signals, aiming to capitalize on periods of strong risk-adjusted performance. We’ll break down the logic, examine its implementation in Python step-by-step, and discuss crucial considerations for evaluating such a strategy.

1. Setup: Importing Libraries

First, we import the necessary Python libraries:

Python

import yfinance as yf       # To download historical market data from Yahoo Finance
import pandas as pd         # For data manipulation and analysis (DataFrames)
import numpy as np          # For numerical operations (like square root)
import matplotlib.pyplot as plt # For plotting the results

2. Strategy Parameters

We define the core parameters that control the strategy’s behavior:

Python

# Parameters
symbol = "ETH-USD"          # The asset we want to trade
start_date = "2018-01-01"   # Start date for historical data
end_date = "2025-04-17"     # End date for historical data (use current date for latest)
window = 30                 # Rolling window (in days) for Sharpe Ratio calculation
upper_threshold = 2.0       # Sharpe Ratio threshold to trigger a long signal
lower_threshold = -2.0      # Sharpe Ratio threshold to trigger a short signal
hold_days = 7               # Maximum number of days to hold a position
stop_loss_pct = 0.20        # Stop-loss percentage (e.g., 0.20 = 20%)

3. Data Acquisition and Preparation

We download the historical price data and calculate basic returns:

Python

# 1) Download data
df = yf.download(symbol, start=start_date, end=end_date, progress=False)

# Clean column names (sometimes yfinance returns multi-level columns)
df.columns = [col.lower() for col in df.columns] # Make column names lowercase

# 2) Calculate daily returns
df["return"] = df["close"].pct_change() # Calculate daily percentage change in closing price

4. Calculating the Rolling Sharpe Ratio

This is the core signal generation step. We calculate the Sharpe Ratio over a rolling window:

Python

# 3) Compute rolling Sharpe (annualized)
# Calculate rolling mean return and rolling standard deviation
rolling_mean = df["return"].rolling(window).mean()
rolling_std = df["return"].rolling(window).std()

# Calculate annualized Sharpe Ratio
# Note: np.sqrt(365) is arguably more appropriate for crypto (24/7 market)
df["rolling_sharpe"] = (rolling_mean / rolling_std) * np.sqrt(252)

5. Generating Trading Signals

Based on the calculated Sharpe Ratio, we generate the raw entry signals:

Python

# 4) Generate entry signals based on thresholds
df["signal"] = 0 # Default signal is neutral (0)
df.loc[df["rolling_sharpe"] > upper_threshold, "signal"] = 1  # Go Long if Sharpe > upper
df.loc[df["rolling_sharpe"] < lower_threshold, "signal"] = -1 # Go Short if Sharpe < lower

6. Preparing for Backtesting

Before running the simulation, we adjust the signal timing and initialize variables:

Python

# 5) Prepare for Backtest: Shift signals to avoid lookahead bias
# We use yesterday's signal to trade on today's price
df['signal_shifted'] = df['signal'].shift(1).fillna(0)

# Initialize backtest variables
position = 0                # Current position: 1=long, -1=short, 0=flat
entry_price = 0.0           # Price at which the current position was entered
days_in_trade = 0           # Counter for days held in the current trade
equity = 1.0                # Starting equity (normalized to 1)
equity_curve = [equity]     # List to store equity values over time

7. The Backtesting Engine Loop

This loop simulates the strategy day by day:

Python

# 6) Backtest Engine Loop
# Start loop from 'window' index to ensure enough data for rolling calculation and shift
for i in range(window, len(df)):
    idx = df.index[i]           # Current date
    row = df.iloc[i]            # Current day's data row
    price = row["close"]        # Use today's close price for trading action (simplification)
    sig = row["signal_shifted"] # Use *yesterday's* calculated signal

    # --- Check Exits First ---
    if position != 0: # Are we currently in a trade?
        days_in_trade += 1
        # Calculate potential return if we closed the trade *right now*
        current_return_pct = (price / entry_price - 1) * position

        # Check Stop Loss: Did the trade lose more than stop_loss_pct?
        if current_return_pct <= -stop_loss_pct:
            equity *= (1 - stop_loss_pct) # Apply the max loss percentage
            position = 0 # Exit position
        # Check Holding Period: Have we held for the max number of days?
        elif days_in_trade >= hold_days:
            equity *= (1 + current_return_pct) # Realize the profit/loss
            position = 0 # Exit position

    # --- Check Entries Second ---
    # Can only enter if currently flat (position == 0) and there's a non-zero signal
    if position == 0 and sig != 0:
        position = sig            # Enter long (1) or short (-1)
        entry_price = price       # Record entry price (using today's close)
        days_in_trade = 0         # Reset trade duration counter

    # Append the *current* equity value to the curve after all checks/trades
    equity_curve.append(equity)

8. Post-Processing and Alignment

We align the calculated equity curve with the corresponding dates in our DataFrame:

Python

# 7) Align Equity Curve with DataFrame
# Remove the initial equity point (1.0) as it corresponds to the day *before* the loop starts
equity_curve_series = pd.Series(equity_curve[1:], index=df.index[window:])

# Add the equity curve to the DataFrame
df["equity"] = equity_curve_series

9. Plotting the Results

Finally, we visualize the performance:

Python

# 8) Plot Equity Curve vs. Buy-and-Hold
plt.figure(figsize=(12, 7)) # Create a figure for the plot

# Plot the strategy's equity curve
plt.plot(df.index, df["equity"], label="Sharpe Strategy Equity")

# Plot the normalized price of the asset (Buy and Hold)
# Normalize by dividing by the first closing price in the backtest period
buy_hold = df['close'] / df['close'].iloc[window] # Adjust starting point
plt.plot(buy_hold.index, buy_hold, label=f"Buy & Hold {symbol}", alpha=0.7)

# Add plot titles and labels
plt.title(f"Equity Curve: {symbol} {window}-day Sharpe Strategy")
plt.ylabel("Equity (Normalized)")
plt.xlabel("Date")
plt.legend() # Show the legend
plt.grid(True) # Add a grid for readability
plt.show() # Display the plot
Pasted image 20250417003609.png

Interpreting the Results & Crucial Caveats

The output plot visualizes the hypothetical growth of capital. Comparing the strategy’s equity curve to the normalized price of Ethereum helps assess whether the strategy added value (outperformed buy-and-hold) during the tested period, potentially with different risk characteristics (e.g., lower drawdowns).

However, it’s vital to understand the limitations:

Conclusion

This Sharpe Ratio-based strategy provides a structured, quantitative approach to trading cryptocurrencies. While the backtest (with the corrected logic avoiding lookahead bias) offers insights, this code and article are for educational purposes only and do not constitute financial advice. Real-world trading requires incorporating costs, rigorous testing against overfitting, and robust risk management before committing capital.