
Performance Metrics
- 401 installs
- 161 repo stars
- Updated July 18, 2026
- joellewis/finance_skills
performance-metrics is a Claude Code skill that defines and computes portfolio and business performance metrics—including returns, drawdowns, Sharpe ratio, and attribution—for developers building auditable investment das
About
performance-metrics is a Claude Code skill for defining and computing portfolio and business performance metrics with consistent, auditable methodology. The skill covers return calculations, maximum drawdown, Sharpe ratio, and performance attribution logic so dashboards and investor updates reflect numbers that reconcile across reports. Developers reach for performance-metrics when building fund analytics pipelines, investor portals, or internal performance dashboards that must not drift between reporting periods. The skill suits quant and fintech engineers implementing standardized financial metric libraries rather than ad hoc spreadsheet formulas embedded in application code.
- Return and drawdown calculations
- Risk-adjusted ratios
- Attribution breakdowns
- Investor-ready metric definitions
- Consistent dashboard inputs
Performance Metrics by the numbers
- 401 all-time installs (skills.sh)
- +16 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #252 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joellewis/finance_skills --skill performance-metricsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 401 |
|---|---|
| repo stars | ★ 161 |
| Last updated | July 18, 2026 |
| Repository | joellewis/finance_skills ↗ |
How do you compute Sharpe ratio and drawdown metrics?
Define and compute portfolio or business performance metrics—returns, drawdowns, Sharpe, attribution—so dashboards and investor updates reflect consistent, auditable numbers.
Who is it for?
Quant and fintech developers building investment performance dashboards, fund reporting pipelines, or auditable portfolio analytics systems.
Skip if: Marketing website analytics or product usage metrics unrelated to portfolio or fund performance measurement.
When should I use this skill?
A developer asks to calculate Sharpe ratio, maximum drawdown, return attribution, or standardize performance metrics across investor reports.
What you get
Standardized return, drawdown, Sharpe, and attribution metric outputs for dashboards and investor reporting.
- Performance metric calculations
- Attribution breakdown
- Standardized reporting outputs
Files
Performance Metrics
Core Concepts
Sharpe Ratio
The most widely used risk-adjusted performance measure. It divides excess return (over the risk-free rate) by total volatility.
SR = (R_p - R_f) / sigma_p- R_p: annualized portfolio return
- R_f: annualized risk-free rate
- sigma_p: annualized portfolio volatility (standard deviation of returns)
A higher Sharpe ratio indicates more return per unit of total risk. Typical benchmarks: SR < 0.5 is poor, 0.5-1.0 is acceptable, > 1.0 is strong, > 2.0 is exceptional.
Annualization: If computed from monthly data, SR_annual = SR_monthly * sqrt(12).
Sortino Ratio
Replaces total volatility with downside deviation, penalizing only harmful volatility (returns below a Minimum Acceptable Return).
Sortino = (R_p - R_f) / sigma_downsidewhere sigma_downside = sqrt((1/n) * sum(min(R_i - MAR, 0)^2)).
Common MAR choices: 0%, risk-free rate, or a target return. Always state which MAR is used.
Information Ratio
Measures active return (alpha) per unit of active risk (tracking error) relative to a benchmark.
IR = (R_p - R_b) / TEwhere TE = std(R_p - R_b) * sqrt(N).
An IR above 0.5 is generally considered good; above 1.0 is exceptional and difficult to sustain.
Treynor Ratio
Measures excess return per unit of systematic risk (beta) rather than total risk.
Treynor = (R_p - R_f) / beta_pUseful for evaluating diversified portfolios where idiosyncratic risk has been diversified away. For undiversified holdings, the Sharpe ratio is more appropriate.
Calmar Ratio
Relates annualized return to the worst peak-to-trough drawdown.
Calmar = CAGR / |MaxDrawdown|A Calmar ratio above 1.0 means the annualized return exceeds the maximum drawdown. This ratio is popular among CTAs and hedge fund investors. Typically computed over a 3-year window.
Omega Ratio
A gain-loss ratio that considers the entire return distribution above and below a threshold tau.
Omega(tau) = integral from tau to +inf of [1 - F(r)] dr
/ integral from -inf to tau of F(r) drwhere F(r) is the cumulative distribution function of returns.
In practice, this is computed as:
Omega(tau) = sum(max(R_i - tau, 0)) / sum(max(tau - R_i, 0))Omega > 1 means expected gains above tau exceed expected losses below tau. Unlike Sharpe, Omega captures the full shape of the distribution (skewness, kurtosis).
Upside and Downside Capture Ratios
Measure how the portfolio participates in benchmark up and down markets.
Up Capture = R_p(in up months) / R_b(in up months) * 100
Down Capture = R_p(in down months) / R_b(in down months) * 100
Capture Ratio = Up Capture / Down CaptureIdeal profile: Up Capture > 100% and Down Capture < 100%, yielding a Capture Ratio > 1. "Up months" and "down months" are defined by the benchmark return being positive or negative, respectively.
M-Squared (Modigliani-Modigliani)
Expresses risk-adjusted return in the same units as return, by leveraging or deleveraging the portfolio to match benchmark volatility.
M^2 = R_f + SR_p * sigma_b
= R_f + ((R_p - R_f) / sigma_p) * sigma_bInterpretation: "If this portfolio were scaled to have the same volatility as the benchmark, it would have returned M-squared." This makes it directly comparable to benchmark returns.
Key Formulas
| Formula | Expression | Use Case |
|---|---|---|
| Sharpe Ratio | (R_p - R_f) / sigma_p | Return per unit of total risk |
| Sortino Ratio | (R_p - R_f) / sigma_downside | Return per unit of downside risk |
| Information Ratio | (R_p - R_b) / TE | Active return per unit of active risk |
| Treynor Ratio | (R_p - R_f) / beta_p | Return per unit of systematic risk |
| Calmar Ratio | CAGR / | MaxDD |
| Omega Ratio | sum(max(R_i - tau, 0)) / sum(max(tau - R_i, 0)) | Full-distribution gain-loss ratio |
| Up Capture | R_p(up) / R_b(up) * 100 | Participation in rising markets |
| Down Capture | R_p(down) / R_b(down) * 100 | Participation in falling markets |
| M-Squared | R_f + SR_p * sigma_b | Risk-adjusted return in return units |
Worked Examples
Example 1: Sharpe Ratio Calculation
Given: A fund returned 12% annualized, the risk-free rate is 4%, and the fund's annualized volatility is 15%.
Calculate: Sharpe Ratio.
Solution:
SR = (0.12 - 0.04) / 0.15
= 0.08 / 0.15
= 0.533The fund earned 0.533 units of excess return per unit of risk. This is in the "acceptable" range but below 1.0.
Example 2: Comparing Funds with Sharpe and Sortino
Given:
- Fund A: Sharpe = 0.8, Sortino = 1.2
- Fund B: Sharpe = 0.7, Sortino = 1.5
Calculate: Which fund is better for a downside-averse investor?
Solution:
Fund A has a higher Sharpe ratio (0.8 vs 0.7), indicating better total-risk-adjusted performance. However, Fund B has a notably higher Sortino ratio (1.5 vs 1.2), meaning it delivers significantly more return per unit of downside risk.
The divergence implies Fund B's volatility is more skewed to the upside -- its total volatility includes more "good" volatility (gains), while its downside volatility is relatively contained.
For a downside-averse investor, Fund B is preferable because the Sortino ratio better captures the risk they care about (losses), and Fund B's superior Sortino indicates better downside risk management.
Example 3: Information Ratio
Given: A portfolio returned 10% annualized, its benchmark returned 8%, and the tracking error is 4%.
Calculate: Information Ratio.
Solution:
IR = (0.10 - 0.08) / 0.04
= 0.02 / 0.04
= 0.50The manager generated 0.50 units of active return per unit of active risk. This is generally considered a good IR, suggesting consistent alpha generation relative to benchmark deviations.
Common Pitfalls
- Annualizing Sharpe incorrectly: The Sharpe ratio scales by sqrt(N) where N is the number of periods per year. SR_annual = SR_monthly sqrt(12), not 12. The excess return and volatility must be in consistent units before dividing.
- Using wrong risk-free rate frequency: If computing monthly Sharpe, use the monthly risk-free rate (annual rate / 12), not the annual rate directly.
- Sortino MAR ambiguity: The Sortino ratio result changes significantly depending on whether MAR = 0, MAR = risk-free rate, or MAR = some target return. Always state the MAR assumption explicitly.
- Small sample sizes making ratios unreliable: Ratios computed from fewer than 36 monthly observations are statistically unreliable. A Sharpe ratio from 12 months of data has a standard error of approximately sqrt((1 + SR^2/2) / 12), which is very wide.
- Comparing Sharpe ratios across different time periods: A Sharpe of 1.0 in a low-vol environment is not the same as 1.0 in a high-vol environment. Performance ratios are period-specific and not directly comparable across different market regimes.
Cross-References
- historical-risk (wealth-management plugin, Layer 1a): Provides the risk measures (volatility, drawdown, downside deviation, tracking error) used as denominators in these performance ratios.
- performance-reporting (wealth-management plugin, Layer 8) and return-calculations (core plugin, Layer 0): For TWR/MWR calculation methodology and reporting presentation, see performance-reporting and core/return-calculations.
- forward-risk (wealth-management plugin, Layer 1b): Forward-looking risk measures (VaR, CVaR) complement retrospective performance assessment by estimating future potential losses.
- volatility-modeling (wealth-management plugin, Layer 1b): Volatility forecasts from GARCH or EWMA can be used to compute forward-looking or conditional Sharpe ratios.
Running the script
Run with uv run scripts/performance_metrics.py (the PEP 723 header resolves numpy automatically) or with python3 scripts/performance_metrics.py after pip install numpy scipy. A bare run prints a full scorecard (Sharpe, Sortino, Information Ratio, Calmar, Treynor, Omega, capture ratios, batting average, win/loss) on seeded synthetic portfolio and benchmark data. Use --verify to assert outputs match this skill's worked examples and the demo's expected values (exit code 0 on PASS) and --help for an overview of the class. The file is primarily meant to be imported as a module (e.g., from performance_metrics import PerformanceScorecard).
# /// script
# dependencies = ["numpy"]
# requires-python = ">=3.11"
# ///
"""
Performance Scorecard
======================
Compute risk-adjusted performance ratios: Sharpe, Sortino, Information Ratio,
Calmar, Treynor, Omega, capture ratios, batting average, and win/loss ratio.
Part of Layer 1a (Retrospective) in the finance skills framework.
"""
import argparse
import math
import sys
import numpy as np
class PerformanceScorecard:
"""Compute risk-adjusted performance metrics from historical returns.
Parameters
----------
returns : np.ndarray
Array of periodic portfolio simple returns (decimals, e.g., 0.01 = 1%).
benchmark_returns : np.ndarray or None, optional
Array of periodic benchmark returns. Required for Information Ratio,
Treynor Ratio, capture ratios, and batting average. Default is None.
risk_free_rate : float, optional
Risk-free rate per period. Default is 0.0. For daily returns with an
annual risk-free rate of 4%, use 0.04/252 ~ 0.000159.
periods_per_year : int, optional
Number of periods in a year for annualization. Default is 252
(trading days). Use 52 for weekly, 12 for monthly.
"""
def __init__(
self,
returns: np.ndarray,
benchmark_returns: np.ndarray | None = None,
risk_free_rate: float = 0.0,
periods_per_year: int = 252,
):
self.returns = np.asarray(returns, dtype=np.float64)
self.benchmark_returns = (
np.asarray(benchmark_returns, dtype=np.float64)
if benchmark_returns is not None
else None
)
self.risk_free_rate = risk_free_rate
self.periods_per_year = periods_per_year
def _require_benchmark(self, metric_name: str) -> np.ndarray:
"""Validate that benchmark returns are available."""
if self.benchmark_returns is None:
raise ValueError(
f"{metric_name} requires benchmark_returns, but none were provided."
)
return self.benchmark_returns
def sharpe_ratio(self) -> float:
"""Compute the annualized Sharpe ratio.
Returns
-------
float
Sharpe = (mean(R_p - R_f) / std(R_p)) * sqrt(periods_per_year)
"""
excess = self.returns - self.risk_free_rate
if np.std(self.returns, ddof=1) == 0:
return 0.0
sharpe = np.mean(excess) / np.std(self.returns, ddof=1)
return float(sharpe * np.sqrt(self.periods_per_year))
def sortino_ratio(self) -> float:
"""Compute the annualized Sortino ratio.
Uses downside deviation (threshold = risk-free rate) as the
denominator instead of total standard deviation.
Returns
-------
float
Sortino = (mean(R_p - R_f) / DD) * sqrt(periods_per_year)
"""
excess = self.returns - self.risk_free_rate
downside = np.minimum(excess, 0.0)
dd = np.sqrt(np.mean(downside ** 2))
if dd == 0:
return float("inf") if np.mean(excess) > 0 else 0.0
sortino = np.mean(excess) / dd
return float(sortino * np.sqrt(self.periods_per_year))
def information_ratio(self) -> float:
"""Compute the annualized Information Ratio.
Requires benchmark returns.
Returns
-------
float
IR = (mean(R_p - R_b) * periods_per_year) / (std(R_p - R_b) * sqrt(periods_per_year))
= (mean(R_p - R_b) / std(R_p - R_b)) * sqrt(periods_per_year)
"""
benchmark = self._require_benchmark("Information Ratio")
active_returns = self.returns - benchmark
te = np.std(active_returns, ddof=1)
if te == 0:
return 0.0
ir = np.mean(active_returns) / te
return float(ir * np.sqrt(self.periods_per_year))
def calmar_ratio(self) -> float:
"""Compute the Calmar ratio.
Returns
-------
float
Calmar = annualized_return / |max_drawdown|
"""
# Annualized return via geometric compounding
cumulative = np.prod(1.0 + self.returns)
n_years = len(self.returns) / self.periods_per_year
if n_years <= 0:
return 0.0
annualized_return = cumulative ** (1.0 / n_years) - 1.0
# Maximum drawdown
cumulative_series = np.cumprod(1.0 + self.returns)
running_max = np.maximum.accumulate(cumulative_series)
drawdowns = (cumulative_series - running_max) / running_max
max_dd = abs(np.min(drawdowns))
if max_dd == 0:
return float("inf") if annualized_return > 0 else 0.0
return float(annualized_return / max_dd)
def treynor_ratio(self) -> float:
"""Compute the annualized Treynor ratio.
Requires benchmark returns for beta calculation.
Returns
-------
float
Treynor = (annualized_excess_return) / beta
"""
benchmark = self._require_benchmark("Treynor Ratio")
# Beta = cov(r_p, r_b) / var(r_b)
cov_matrix = np.cov(self.returns, benchmark)
beta = cov_matrix[0, 1] / cov_matrix[1, 1]
if beta == 0:
return 0.0
mean_excess = np.mean(self.returns - self.risk_free_rate)
annualized_excess = mean_excess * self.periods_per_year
return float(annualized_excess / beta)
def omega_ratio(self, threshold: float = 0.0) -> float:
"""Compute the Omega ratio.
The ratio of cumulative gains above the threshold to cumulative
losses below the threshold.
Parameters
----------
threshold : float, optional
Return threshold. Default is 0.0.
Returns
-------
float
Omega = sum(max(r_i - threshold, 0)) / sum(max(threshold - r_i, 0))
"""
gains = np.sum(np.maximum(self.returns - threshold, 0.0))
losses = np.sum(np.maximum(threshold - self.returns, 0.0))
if losses == 0:
return float("inf") if gains > 0 else 1.0
return float(gains / losses)
def up_capture(self) -> float:
"""Compute the up capture ratio.
Requires benchmark returns.
Returns
-------
float
Up capture = mean(r_p | r_b > 0) / mean(r_b | r_b > 0)
Expressed as a ratio (1.0 = 100% capture).
"""
benchmark = self._require_benchmark("Up Capture")
up_mask = benchmark > 0
if not np.any(up_mask):
return 0.0
return float(np.mean(self.returns[up_mask]) / np.mean(benchmark[up_mask]))
def down_capture(self) -> float:
"""Compute the down capture ratio.
Requires benchmark returns.
Returns
-------
float
Down capture = mean(r_p | r_b < 0) / mean(r_b | r_b < 0)
Expressed as a ratio. Values < 1.0 mean the portfolio loses
less than the benchmark in down markets.
"""
benchmark = self._require_benchmark("Down Capture")
down_mask = benchmark < 0
if not np.any(down_mask):
return 0.0
return float(np.mean(self.returns[down_mask]) / np.mean(benchmark[down_mask]))
def batting_average(self) -> float:
"""Compute the batting average vs the benchmark.
Requires benchmark returns.
Returns
-------
float
Fraction of periods where the portfolio outperformed the benchmark.
"""
benchmark = self._require_benchmark("Batting Average")
return float(np.mean(self.returns > benchmark))
def win_loss_ratio(self) -> float:
"""Compute the win/loss ratio.
Returns
-------
float
Average win / average loss (magnitudes).
"""
wins = self.returns[self.returns > 0]
losses = self.returns[self.returns < 0]
if len(losses) == 0:
return float("inf") if len(wins) > 0 else 0.0
if len(wins) == 0:
return 0.0
return float(np.mean(wins) / abs(np.mean(losses)))
def summary(self) -> dict:
"""Compute all available metrics and return as a dictionary.
Returns
-------
dict
Dictionary of metric names to values. Metrics that require
a benchmark will be None if no benchmark is provided.
"""
result = {
"sharpe_ratio": self.sharpe_ratio(),
"sortino_ratio": self.sortino_ratio(),
"calmar_ratio": self.calmar_ratio(),
"omega_ratio": self.omega_ratio(),
"win_loss_ratio": self.win_loss_ratio(),
}
# Benchmark-dependent metrics
if self.benchmark_returns is not None:
result["information_ratio"] = self.information_ratio()
result["treynor_ratio"] = self.treynor_ratio()
result["up_capture"] = self.up_capture()
result["down_capture"] = self.down_capture()
result["batting_average"] = self.batting_average()
else:
result["information_ratio"] = None
result["treynor_ratio"] = None
result["up_capture"] = None
result["down_capture"] = None
result["batting_average"] = None
return result
def _demo_scorecard() -> PerformanceScorecard:
"""Build the seeded demo scorecard used by both the demo and --verify."""
np.random.seed(42)
n_days = 504
portfolio_returns = np.random.normal(loc=0.0004, scale=0.013, size=n_days)
benchmark_returns = np.random.normal(loc=0.0003, scale=0.011, size=n_days)
return PerformanceScorecard(
returns=portfolio_returns,
benchmark_returns=benchmark_returns,
risk_free_rate=0.04 / 252,
periods_per_year=252,
)
def run_demo() -> None:
"""Run the demonstration (default when executed with no arguments)."""
# ----------------------------------------------------------------
# Demo: Performance scorecard on synthetic data
# ----------------------------------------------------------------
np.random.seed(42)
# Generate 2 years of daily returns
n_days = 504
# Portfolio: slightly positive alpha with moderate volatility
portfolio_returns = np.random.normal(loc=0.0004, scale=0.013, size=n_days)
# Benchmark: market returns
benchmark_returns = np.random.normal(loc=0.0003, scale=0.011, size=n_days)
# Risk-free rate: ~4% annualized -> daily
rf_daily = 0.04 / 252
scorecard = PerformanceScorecard(
returns=portfolio_returns,
benchmark_returns=benchmark_returns,
risk_free_rate=rf_daily,
periods_per_year=252,
)
print("=" * 60)
print("Performance Scorecard - Demo")
print("=" * 60)
# Individual metrics
print(f"\nSharpe Ratio: {scorecard.sharpe_ratio():.4f}")
print(f"Sortino Ratio: {scorecard.sortino_ratio():.4f}")
print(f"Information Ratio: {scorecard.information_ratio():.4f}")
print(f"Calmar Ratio: {scorecard.calmar_ratio():.4f}")
print(f"Treynor Ratio: {scorecard.treynor_ratio():.4f}")
print(f"Omega Ratio: {scorecard.omega_ratio():.4f}")
print(f"\nUp Capture: {scorecard.up_capture():.4f} ({scorecard.up_capture()*100:.1f}%)")
print(f"Down Capture: {scorecard.down_capture():.4f} ({scorecard.down_capture()*100:.1f}%)")
print(f"Batting Average: {scorecard.batting_average():.4f} ({scorecard.batting_average()*100:.1f}%)")
print(f"Win/Loss Ratio: {scorecard.win_loss_ratio():.4f}")
# Summary
print("\n" + "-" * 40)
print("Full Summary:")
print("-" * 40)
summary = scorecard.summary()
for metric, value in summary.items():
if value is not None:
print(f" {metric:25s}: {value:.4f}")
else:
print(f" {metric:25s}: N/A (no benchmark)")
# Context: annualized return and volatility
cumulative = np.prod(1.0 + portfolio_returns)
n_years = n_days / 252
ann_return = cumulative ** (1.0 / n_years) - 1.0
ann_vol = np.std(portfolio_returns, ddof=1) * np.sqrt(252)
print(f"\n Annualized Return: {ann_return:.4f} ({ann_return*100:.2f}%)")
print(f" Annualized Volatility: {ann_vol:.4f} ({ann_vol*100:.2f}%)")
print("\n" + "=" * 60)
print("Demo complete.")
print("=" * 60)
def run_verify() -> int:
"""Assert the demo outputs and the SKILL.md worked-example numbers.
Returns
-------
int
0 if all checks pass, 1 otherwise.
"""
failures = 0
def check(name: str, actual: float, expected: float,
rel_tol: float = 1e-6, abs_tol: float = 1e-9) -> None:
nonlocal failures
ok = math.isclose(actual, expected, rel_tol=rel_tol, abs_tol=abs_tol)
status = "PASS" if ok else "FAIL"
print(f"[{status}] {name}: actual={actual:.10g} expected={expected:.10g}")
if not ok:
failures += 1
# SKILL.md Example 1: Sharpe = (0.12 - 0.04) / 0.15 = 0.533
check("SKILL.md Ex1 Sharpe ratio", (0.12 - 0.04) / 0.15, 0.533,
rel_tol=1e-3)
# SKILL.md Example 3: IR = (0.10 - 0.08) / 0.04 = 0.50
check("SKILL.md Ex3 Information Ratio", (0.10 - 0.08) / 0.04, 0.50)
# Seeded demo scorecard values
scorecard = _demo_scorecard()
check("Demo Sharpe ratio", scorecard.sharpe_ratio(), 0.4727816064)
check("Demo Sortino ratio", scorecard.sortino_ratio(), 0.6997682020)
check("Demo Information Ratio", scorecard.information_ratio(),
-0.1557715541)
check("Demo Calmar ratio", scorecard.calmar_ratio(), 0.6669373286)
check("Demo Treynor ratio", scorecard.treynor_ratio(), 1.4205101790)
check("Demo Omega ratio", scorecard.omega_ratio(), 1.1119106743)
check("Demo Up Capture", scorecard.up_capture(), 0.1339458972)
check("Demo Down Capture", scorecard.down_capture(), 0.0201295542)
check("Demo Batting Average", scorecard.batting_average(), 0.4940476190)
check("Demo Win/Loss Ratio", scorecard.win_loss_ratio(), 1.0108278858)
if failures:
print(f"\nFAIL: {failures} check(s) did not match expected values.")
return 1
print("\nPASS: all checks matched expected values.")
return 0
def main() -> None:
parser = argparse.ArgumentParser(
description=(
"Risk-adjusted performance metrics via the PerformanceScorecard "
"class: Sharpe, Sortino, Information Ratio, Calmar, Treynor, "
"Omega, up/down capture, batting average, and win/loss ratio."
),
epilog=(
"Run with no arguments to print a demo scorecard on seeded "
"synthetic data. Import as a module: "
"from performance_metrics import PerformanceScorecard"
),
)
parser.add_argument(
"--verify",
action="store_true",
help="assert demo outputs and SKILL.md worked-example numbers; "
"exits nonzero on mismatch",
)
args = parser.parse_args()
if args.verify:
sys.exit(run_verify())
run_demo()
if __name__ == "__main__":
main()
Related skills
FAQ
Which metrics does performance-metrics compute?
performance-metrics defines and computes portfolio performance metrics including returns, maximum drawdown, Sharpe ratio, and performance attribution for consistent, auditable dashboards and investor reporting.
When should developers use performance-metrics?
Developers should use performance-metrics when building fund analytics pipelines or investor portals that need standardized, reconcilable return and risk metric calculations across reporting periods.