Sharpe Ratio Deep Dive: 5 Common Mistakes in Strategy Evaluation
Sharpe Ratio Deep Dive: 5 Common Mistakes in Strategy Evaluation
A strategy with Sharpe 3.0 sounds amazing — until you realize the number is wrong. Here are the five most common mistakes.
Correct Sharpe Calculation
import numpy as np
import pandas as pd
def sharpe_ratio(
returns: pd.Series,
risk_free_rate: float = 0.05, # Annual risk-free rate
periods_per_year: int = 252, # Trading days
) -> float:
"""Calculate annualized Sharpe ratio correctly."""
# Convert annual risk-free to per-period
rf_per_period = (1 + risk_free_rate) ** (1 / periods_per_year) - 1
excess_returns = returns - rf_per_period
mean_excess = excess_returns.mean()
std_excess = excess_returns.std(ddof=1) # Use sample std
if std_excess == 0:
return 0.0
return mean_excess / std_excess * np.sqrt(periods_per_year)
Mistake #1: Wrong Annualization Factor
# WRONG: Using 365 for daily crypto data
wrong_sharpe = returns.mean() / returns.std() * np.sqrt(365)
# RIGHT: Crypto trades 365 days, but vol structure differs
# Use 365 for crypto, 252 for stocks, 12 for monthly
correct_sharpe = returns.mean() / returns.std() * np.sqrt(365)
# But ensure your returns match the frequency!
# Common error: mixing daily returns with monthly frequency
daily_returns = prices.pct_change() # Daily
monthly_returns = prices.resample('M').last().pct_change() # Monthly
# These give DIFFERENT Sharpe ratios
sharpe_daily = sharpe_ratio(daily_returns, periods_per_year=365)
sharpe_monthly = sharpe_ratio(monthly_returns, periods_per_year=12)
# They should be similar but won't be identical due to compounding
Mistake #2: Ignoring the Risk-Free Rate
# In 2024, the risk-free rate is ~5% (T-bills)
# A strategy returning 8% with 10% vol:
# Without risk-free: 8% / 10% * sqrt(252) = 12.7 (inflated!)
# With risk-free: 3% / 10% * sqrt(252) = 4.76 (realistic)
Mistake #3: Look-Ahead Bias
# WRONG: Optimizing parameters on the full dataset
best_sharpe = 0
for window in [5, 10, 20, 50]:
returns = strategy(full_data, window=window)
s = sharpe_ratio(returns)
best_sharpe = max(best_sharpe, s)
# This Sharpe is overfitted!
# RIGHT: Walk-forward or train/test split
train_data = data[:split_point]
test_data = data[split_point:]
# Optimize on train
best_window = optimize_on(train_data)
# Evaluate on test (this is the real Sharpe)
test_returns = strategy(test_data, window=best_window)
real_sharpe = sharpe_ratio(test_returns)
Mistake #4: Survivorship Bias
If you only test on assets that still exist today, you're excluding all the ones that went to zero. This inflates Sharpe by 0.3-0.5 typically.
Mistake #5: Not Accounting for Transaction Costs
def sharpe_with_costs(
returns: pd.Series,
signals: pd.Series,
cost_per_trade_bps: float = 10,
) -> float:
"""Sharpe after transaction costs."""
# Count trades (signal changes)
trades = (signals.diff().abs() > 0).astype(float)
trade_costs = trades * cost_per_trade_bps / 10000
net_returns = returns - trade_costs
return sharpe_ratio(net_returns)
# A strategy trading 10x/day with 10bps cost:
# Loses 10 * 10 = 100 bps/day = 365% annually in costs alone!
Quick Reference
| Sharpe Range | Interpretation |
|---|---|
| < 0 | Losing money |
| 0 - 0.5 | Below average |
| 0.5 - 1.0 | Acceptable |
| 1.0 - 2.0 | Good |
| 2.0 - 3.0 | Very good |
| > 3.0 | Suspicious — check for errors |
On ClawDUX, every listed strategy's Sharpe ratio is independently verified by the AI engine — using the correct annualization, with transaction costs, and without look-ahead bias.
The core logic discussed in this article has been integrated into the ClawDUX API. Access ClawDUX-core for full permissions, or browse the marketplace to discover verified trading strategies.