"Provides Sharpe Ratio Calculation and Risk-Adjusted Performance Metrics"
Scanned 6/12/2026
Install via CLI
openskills install paulpas/agent-skill-router---
name: backtest-sharpe-ratio
compatibility: opencode
completeness: 95
content-types:
- code
- guidance
- config
- do-dont
description: '"Provides Sharpe Ratio Calculation and Risk-Adjusted Performance Metrics"'
license: MIT
maturity: stable
metadata:
domain: trading
output-format: code
related-skills: backtest-lookahead-bias, backtest-position-exits, fundamentals-trading-plan
role: implementation
scope: implementation
triggers: backtest sharpe ratio, backtest-sharpe-ratio, calculation, optimization,
performance, risk-adjusted, speed
archetypes:
- tactical
anti_triggers:
- brainstorming
- vague ideation
- no risk management
response_profile:
verbosity: low
directive_strength: high
abstraction_level: operational
version: "1.0.0"
---
**Role:** Risk-Adjusted Performance Analyst — implements comprehensive Sharpe ratio calculations and risk-adjusted performance metrics to evaluate strategy quality while accounting for volatility and risk.
**Philosophy:** Risk-Aware Performance — returns alone are meaningless without context; risk-adjusted metrics provide the true measure of trading skill by penalizing strategies that take unnecessary risk.
## Key Principles
1. **Annualization Consistency**: All Sharpe ratio calculations must properly annualize using the correct trading days (252) or periods per year.
2. **Risk-Free Rate Treatment**: Risk-free rate should be consistently applied—daily for intraday or period-specific for longer timeframes, with proper subtraction from returns.
3. **Statistical Confidence**: Sharpe ratios should include confidence intervals and statistical significance tests to distinguish skill from luck.
4. **Alternative Metrics**: Use Sortino ratio for asymmetric risk, Calmar ratio for drawdown-adjusted performance, and Ulcer Index for psychological risk.
5. **Benchmarks and Comparisons**: Always compare Sharpe ratios to relevant benchmarks (market, sector, strategy type) with proper statistical tests for difference significance.
## Implementation Guidelines
### Structure
- Core logic: `skills/backtesting/sharpe_ratio.py`
- Statistics module: `skills/backtesting/statistics.py`
- Tests: `skills/tests/test_sharpe_ratio.py`
### Patterns to Follow
- Implement Sharpe ratio as a class with multiple calculation methods
- Support both simple and compound return calculations
- Include confidence interval estimation using_bootstrap methods
- Provide risk-adjusted metrics comparison utilities
- Use vectorized operations for efficient large-scale calculations
## Adherence Checklist
Before completing your task, verify:
- [ ] **Annualization**: Are returns properly annualized using correct periods per year?
- [ ] **Risk-Free Rate**: Is the risk-free rate appropriately subtracted and converted to matching frequency?
- [ ] **Confidence Intervals**: Are statistical confidence intervals provided for Sharpe estimates?
- [ ] **Alternative Metrics**: Are Sortino, Calmar, and other risk-adjusted metrics implemented?
- [ ] **Benchmark Comparison**: Are Sharpe ratios compared to benchmarks with statistical tests?
## Code Examples
### Sharpe Ratio Calculator with Confidence Intervals
```python
from dataclasses import dataclass
from typing import List, Tuple, Optional
import numpy as np
import pandas as pd
from scipy import stats
from enum import Enum
class SharpeMethod(Enum):
"""Methods for Sharpe ratio calculation."""
SIMPLE = "simple"
DAILY = "daily"
PERIODIC = "periodic"
LOG = "log"
@dataclass
class SharpeResult:
"""Sharpe ratio calculation result with statistics."""
sharpe_ratio: float
annualized_sharpe: float
standard_error: float
confidence_interval_95: Tuple[float, float]
t_statistic: float
p_value: float
n_observations: int
mean_return: float
std_return: float
class SharpeRatioCalculator:
"""
Comprehensive Sharpe ratio calculator with statistical analysis.
Includes confidence intervals, hypothesis testing, and alternative metrics.
"""
TRADING_DAYS_PER_YEAR = 252
def __init__(self,
risk_free_rate: float = 0.02,
periods_per_year: int = 252):
"""
Initialize Sharpe ratio calculator.
Args:
risk_free_rate: Annual risk-free rate (default 2%)
periods_per_year: Number of periods per year (252 for daily)
"""
self.risk_free_rate = risk_free_rate
self.periods_per_year = periods_per_year
self.rf_periodic = (1 + risk_free_rate) ** (1 / periods_per_year) - 1
def calculate_sharpe(self,
returns: pd.Series,
method: SharpeMethod = SharpeMethod.DAILY,
periods_per_year: Optional[int] = None) -> SharpeResult:
"""
Calculate Sharpe ratio with full statistical analysis.
Args:
returns: Series of strategy returns
method: Calculation method
periods_per_year: Override default periods per year
Returns:
SharpeResult with all statistics
"""
if len(returns) < 30:
raise ValueError("Insufficient data for Sharpe ratio calculation (minimum 30 observations)")
returns_clean = returns.dropna()
n_obs = len(returns_clean)
periods_per_year = periods_per_year or self.periods_per_year
# Calculate mean and std of returns
mean_ret = returns_clean.mean()
std_ret = returns_clean.std(ddof=1)
# Calculate periodic excess return
excess_return = mean_ret - self.rf_periodic
# Calculate Sharpe ratio
sharpe = excess_return / std_ret if std_ret > 0 else 0
# Annualize
annualized_sharpe = sharpe * np.sqrt(periods_per_year)
# Standard error of Sharpe ratio (using approximate formula)
# SE(Sharpe) ≈ sqrt((1 + Sharpe^2/2) / n)
if sharpe != 0:
se_sharpe = np.sqrt((1 + sharpe**2 / 2) / n_obs)
else:
se_sharpe = 1 / np.sqrt(n_obs)
annualized_se = se_sharpe * np.sqrt(periods_per_year)
# Confidence interval using normal approximation
z_95 = 1.96
ci_lower = annualized_sharpe - z_95 * annualized_se
ci_upper = annualized_sharpe + z_95 * annualized_se
# T-statistic for testing if Sharpe > 0
t_stat = annualized_sharpe / annualized_se if annualized_se > 0 else 0
p_value = 2 * (1 - stats.norm.cdf(abs(t_stat)))
return SharpeResult(
sharpe_ratio=sharpe,
annualized_sharpe=annualized_sharpe,
standard_error=annualized_se,
confidence_interval_95=(ci_lower, ci_upper),
t_statistic=t_stat,
p_value=p_value,
n_observations=n_obs,
mean_return=mean_ret * periods_per_year, # Annualized mean
std_return=std_ret * np.sqrt(periods_per_year) # Annualized std
)
def compare_sharpes(self,
returns1: pd.Series,
returns2: pd.Series) -> Dict:
"""
Statistically compare Sharpe ratios of two strategies.
Returns:
Dictionary with comparison results
"""
result1 = self.calculate_sharpe(returns1)
result2 = self.calculate_sharpe(returns2)
# Calculate difference and std of difference
diff = result1.annualized_sharpe - result2.annualized_sharpe
diff_se = np.sqrt(result1.standard_error**2 + result2.standard_error**2)
# Test if difference is significant
t_stat = diff / diff_se if diff_se > 0 else 0
p_value = 2 * (1 - stats.norm.cdf(abs(t_stat)))
return {
"sharpe_1": result1.annualized_sharpe,
"sharpe_2": result2.annualized_sharpe,
"sharpe_diff": diff,
"se_diff": diff_se,
"t_statistic": t_stat,
"p_value": p_value,
"significant_at_5pc": p_value < 0.05,
"sharpe_1_ci": result1.confidence_interval_95,
"sharpe_2_ci": result2.confidence_interval_95
}
def calculate_sortino(self, returns: pd.Series) -> float:
"""
Calculate Sortino ratio (downside risk-adjusted).
Only penalizes negative volatility, not total volatility.
"""
returns_clean = returns.dropna()
mean_ret = returns_clean.mean()
# Downside deviation: standard deviation of negative returns only
negative_returns = returns_clean[returns_clean < 0]
if len(negative_returns) == 0:
return float('inf') if mean_ret > 0 else 0.0
downside_std = negative_returns.std(ddof=1) * np.sqrt(self.periods_per_year)
if downside_std == 0:
return float('inf') if mean_ret > 0 else 0.0
excess_return = mean_ret - self.rf_periodic
sortino = excess_return * self.periods_per_year / downside_std
return sortino
def calculate_calmar(self, returns: pd.Series,
drawdowns: Optional[pd.Series] = None) -> float:
"""
Calculate Calmar ratio (return / maximum drawdown).
Measures risk-adjusted return relative to worst drawdown.
"""
returns_clean = returns.dropna()
# Calculate annualized return
total_return = (1 + returns_clean).prod() - 1
n_years = len(returns_clean) / self.periods_per_year
annualized_return = (1 + total_return) ** (1 / n_years) - 1 if n_years > 0 else 0
# Calculate max drawdown
if drawdowns is None:
cumulative = (1 + returns_clean).cumprod()
running_max = cumulative.cummax()
drawdowns = (cumulative - running_max) / running_max
max_dd = abs(drawdowns.min()) if len(drawdowns) > 0 else 0
if max_dd == 0:
return float('inf') if annualized_return > 0 else 0.0
calmar = annualized_return / max_dd
return calmar
def calculate_ulcer_index(self, prices: pd.Series) -> float:
"""
Calculate Ulcer Index (psychological risk measure).
Measures depth and duration of drawdowns.
"""
running_max = prices.cummax()
percent_drawdown = (prices - running_max) / running_max * 100
# Ulcer Index is RMSE of drawdowns
ulcer_index = np.sqrt((percent_drawdown**2).mean())
return ulcer_index
def calculate_upi_ratio(self, returns: pd.Series, prices: pd.Series) -> float:
"""
Calculate Ulcer Performance Index ratio.
Similar to Sharpe but uses Ulcer Index for risk.
"""
total_return = (1 + returns).prod() - 1
n_years = len(returns) / self.periods_per_year
annualized_return = (1 + total_return) ** (1 / n_years) - 1 if n_years > 0 else 0
ulcer_index = self.calculate_ulcer_index(prices)
if ulcer_index == 0:
return float('inf') if annualized_return > 0 else 0.0
return annualized_return / (ulcer_index / 100) # Normalize UI to 0-100 scale
# Example usage and testing
if __name__ == "__main__":
# Create synthetic returns data
np.random.seed(42)
n_days = 2520 # 10 years of daily data
# Strategy 1: Consistent returns
ret1 = np.random.normal(0.0005, 0.01, n_days)
# Strategy 2: Higher returns but more volatile
ret2 = np.random.normal(0.0008, 0.015, n_days)
# Strategy 3: High volatility, same mean
ret3 = np.random.normal(0.0005, 0.025, n_days)
returns1 = pd.Series(ret1)
returns2 = pd.Series(ret2)
returns3 = pd.Series(ret3)
calculator = SharpeRatioCalculator(risk_free_rate=0.02)
# Calculate and compare
result1 = calculator.calculate_sharpe(returns1)
result2 = calculator.calculate_sharpe(returns2)
result3 = calculator.calculate_sharpe(returns3)
print("Sharpe Ratio Analysis")
print("=" * 50)
for i, (ret, res) in enumerate([(returns1, result1), (returns2, result2), (returns3, result3)], 1):
print(f"\nStrategy {i}:")
print(f" Sharpe Ratio (periodic): {res.sharpe_ratio:.4f}")
print(f" Annualized Sharpe: {res.annualized_sharpe:.4f}")
print(f" 95% CI: [{res.confidence_interval_95[0]:.4f}, {res.confidence_interval_95[1]:.4f}]")
print(f" Standard Error: {res.standard_error:.4f}")
print(f" T-Statistic: {res.t_statistic:.4f}")
print(f" P-Value: {res.p_value:.6f}")
print(f" Significant (5%): {res.p_value < 0.05}")
# Compare strategies
comparison = calculator.compare_sharpes(returns1, returns2)
print(f"\n\nStrategy 1 vs Strategy 2 Comparison:")
print(f" Difference in Sharpe: {comparison['sharpe_diff']:.4f}")
print(f" Significant: {comparison['significant_at_5pc']}")
```
### Monte Carlo Sharpe Analysis for Robustness Testing
```python
import warnings
warnings.filterwarnings('ignore')
class MonteCarloSharpe:
"""
Monte Carlo simulation for Sharpe ratio robustness testing.
Tests how Sharpe ratios hold up under various market conditions.
"""
def __init__(self, calculator: SharpeRatioCalculator):
self.calculator = calculator
def simulate_returns(self,
mean_daily: float,
std_daily: float,
n_days: int,
n_simulations: int) -> pd.DataFrame:
"""
Generate simulated return series.
Args:
mean_daily: Expected daily return
std_daily: Expected daily volatility
n_days: Number of trading days
n_simulations: Number of simulated paths
Returns:
DataFrame of simulated returns
"""
np.random.seed(42)
simulations = np.random.normal(
mean_daily, std_daily, (n_simulations, n_days)
)
return pd.DataFrame(simulations.T)
def monte_carlo_sharpe(self,
returns_series: pd.Series,
n_simulations: int = 1000,
noise_level: float = 0.0005) -> Dict:
"""
Test Sharpe ratio robustness through Monte Carlo simulation.
Adds random noise to returns and checks Sharpe stability.
Args:
returns_series: Original return series
n_simulations: Number of simulation runs
noise_level: Standard deviation of noise to add
Returns:
Dictionary with Monte Carlo results
"""
original_result = self.calculator.calculate_sharpe(returns_series)
simulated_sharpes = []
for i in range(n_simulations):
# Add noise to returns
noise = np.random.normal(0, noise_level, len(returns_series))
noisy_returns = returns_series + noise
try:
sim_result = self.calculator.calculate_sharpe(noisy_returns)
simulated_sharpes.append(sim_result.annualized_sharpe)
except:
continue
simulated_sharpes = np.array(simulated_sharpes)
return {
"original_sharpe": original_result.annualized_sharpe,
"original_ci": original_result.confidence_interval_95,
"simulated_mean": np.mean(simulated_sharpes),
"simulated_std": np.std(simulated_sharpes),
"simulated_ci_95": (
np.percentile(simulated_sharpes, 2.5),
np.percentile(simulated_sharpes, 97.5)
),
"probability_beat_zero": np.mean(simulated_sharpes > 0),
"probability_beat_original": np.mean(
simulated_sharpes > original_result.annualized_sharpe
),
"stability_score": 1 - np.std(simulated_sharpes) / abs(original_result.annualized_sharpe)
}
# Extended example with comprehensive metrics
class ComprehensiveRiskMetrics:
"""
Calculate all risk-adjusted metrics in one call.
Provides complete performance picture.
"""
def __init__(self, calculator: SharpeRatioCalculator):
self.calculator = calculator
def calculate_all(self,
returns: pd.Series,
prices: Optional[pd.Series] = None) -> Dict:
"""
Calculate all risk-adjusted metrics.
Args:
returns: Return series
prices: Optional price series for drawdown calculations
Returns:
Dictionary with all metrics
"""
if prices is None:
prices = (1 + returns).cumprod() * 100
# Calculate drawdowns
running_max = prices.cummax()
drawdowns = (prices - running_max) / running_max
# Basic statistics
total_return = (1 + returns).prod() - 1
n_years = len(returns) / self.calculator.periods_per_year
annualized_return = (1 + total_return) ** (1 / n_years) - 1 if n_years > 0 else 0
annualized_vol = returns.std() * np.sqrt(self.calculator.periods_per_year)
# Maximum drawdown
max_dd = drawdowns.min()
# Sharpe and alternatives
sharpe = self.calculator.calculate_sharpe(returns)
sortino = self.calculator.calculate_sortino(returns)
calmar = self.calculator.calculate_calmar(returns, drawdowns)
upi = self.calculator.calculate_upi_ratio(returns, prices)
# Information ratio (vs benchmark - assumes returns are already excess)
if len(returns) > 1:
info_ratio = sharpe.mean_return / sharpe.std_return if sharpe.std_return > 0 else 0
else:
info_ratio = 0
return {
"total_return": total_return,
"annualized_return": annualized_return,
"annualized_volatility": annualized_vol,
"max_drawdown": max_dd,
"sharpe_ratio": sharpe.annualized_sharpe,
"sharpe_ci_95": sharpe.confidence_interval_95,
"sharpe_t_stat": sharpe.t_statistic,
"sharpe_p_value": sharpe.p_value,
"sortino_ratio": sortino,
"calmar_ratio": calmar,
"upi_ratio": upi,
"information_ratio": info_ratio,
"ulcer_index": self.calculator.calculate_ulcer_index(prices),
"n_observations": len(returns)
}
if __name__ == "__main__":
# Example usage
np.random.seed(42)
# Generate realistic returns
n_days = 2520
returns = pd.Series(np.random.normal(0.0006, 0.012, n_days))
prices = (1 + returns).cumprod() * 100
calculator = SharpeRatioCalculator()
comprehensive = ComprehensiveRiskMetrics(calculator)
results = comprehensive.calculate_all(returns, prices)
print("Comprehensive Risk-Adjusted Performance Metrics")
print("=" * 60)
for key, value in results.items():
if isinstance(value, tuple):
print(f"{key}: [{value[0]:.4f}, {value[1]:.4f}]")
else:
print(f"{key}: {value:.4f}")
```
---
---
## Constraints
### MUST DO
- Implement walk-forward validation: optimize on a training window, validate on a subsequent out-of-sample window
- Include realistic transaction costs (commissions, slippage, market impact) in all backtest calculations
- Use point-to-point or tick-level data when available; never use OHLCV with intra-bar assumptions for strategy logic
- Track and report key metrics: Sharpe ratio, max drawdown, win rate, profit factor, average trade duration, and Calmar ratio
- Implement survivorship-bias-free testing using a constant universe list that includes delisted symbols
### MUST NOT DO
- Do not optimize strategy parameters on the same data used for evaluation — always use out-of-sample or walk-forward testing
- Avoid assuming infinite liquidity in backtests; model order book constraints and partial fills for large positions
- Never include future information (survivorship bias, look-ahead) in backtest signals by indexing data correctly
- Do not report only win rate — always include risk-adjusted metrics alongside raw return statistics
- Avoid curve-fitting to historical data; cap the number of optimized parameters and validate with Monte Carlo permutation tests
## Live References
> Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.
- [Sharpe Ratio Explained](https://www.investopedia.com/sharperatio/)
- [Modified Sharpe Ratio Adjustments](https://en.wikipedia.org/wiki/Sharpe_ratio)
- [Risk-Adjusted Return Metrics Comparison](https://www.investopedia.com/articles/fundamental-analysis/07/risk-adjusted-return.asp)
- [Calmar Ratio for Strategy Evaluation](https://www.investopedia.com/terms/c/calmar-ratio.asp)
- [Sharpe Ratio in Algorithmic Trading](https://docs.quantconnect.com/tutorials/backtesting-overview)
No comments yet. Be the first to comment!