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
yfinance
to get
the price data, pandas
to work with it efficiently in a
table-like structure (DataFrame), numpy
for mathematical
calculations, and matplotlib
to visualize the strategy’s
performance.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%)
ETH-USD
), the
date range for our backtest, the lookback period (window
)
for the Sharpe calculation, the thresholds
that trigger
trades, the maximum hold_days
for any trade, and a
stop_loss_pct
for risk management.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
yf.download
to
fetch the Open, High, Low, Close, Adjusted Close, and Volume data for
Ethereum. We simplify the column names and then calculate the daily
return
based on the percentage change in the
close
price from one day to the next. This
return
series is the basis for our Sharpe calculation.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)
window
(30 days). We calculate the average daily
return (rolling_mean
) and the standard deviation of daily
returns (rolling_std
) during that period. The Sharpe Ratio
is then rolling_mean / rolling_std
. We annualize it by
multiplying by the square root of trading days per year
(np.sqrt(252)
used here; np.sqrt(365)
is often
better for crypto). A higher value suggests better risk-adjusted returns
recently.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
signal
column, initially all zeros. If a day’s rolling_sharpe
is
above the upper_threshold
(2.0), we set the signal to 1
(buy). If it’s below the lower_threshold
(-2.0), we set the
signal to -1 (short). Otherwise, it remains 0 (do nothing).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
df['signal'].shift(1)
: This is crucial. It shifts the
signals forward by one day. This means the signal generated using data
up to the end of Day T is used to make a trading decision on
Day T+1. This prevents lookahead bias. .fillna(0)
handles
the first day which has no prior signal.position
to 0 (flat), track the
entry_price
and days_in_trade
for the current
position, start with a hypothetical equity
of 1, and create
a list equity_curve
to record how this equity changes 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)
window
period needed for calculations.position != 0
).days_in_trade
and calculates the
current_return_pct
based on the entry_price
and the price
for the current day.current_return_pct
shows a loss greater than or equal to stop_loss_pct
, the
position is closed, and equity is reduced by the
stop_loss_pct
.days_in_trade
reaches hold_days
, the position is closed, and the realized
profit or loss (current_return_pct
) is applied to the
equity
.position == 0
), it
checks if there’s a new entry signal (sig != 0
). If yes, it
sets the position
, records the entry_price
,
and resets days_in_trade
.equity
value to the equity_curve
list for that day.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
equity_curve
list
contains one extra point at the beginning (the initial 1.0). We remove
this and create a pandas Series using the dates from the DataFrame that
correspond to the backtest period (starting from the
window
-th index). This equity series is then added as a new
column to our DataFrame df
.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
matplotlib
. It shows the strategy’s equity
curve over time. For comparison, it also plots the performance of simply
buying and holding ETH-USD over the same period (normalized to start at
1). This visual comparison helps assess the strategy’s relative
performance and risk profile.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:
np.sqrt(365)
is likely more accurate for crypto.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.