Financial Metrics
Overview
To evaluate trading strategies, you need a common language of performance metrics. This chapter covers the essential metrics with Python implementations you’ll use throughout the rest of the guide.
Returns
Simple Returns
The percentage change in price from one period to the next.
import numpy as np
import pandas as pd
# Simple return
def simple_return(prices: pd.Series) -> pd.Series:
return prices.pct_change()
# Example
prices = pd.Series([100, 102, 99, 105, 103])
returns = simple_return(prices)
# [NaN, 0.02, -0.0294, 0.0606, -0.0190]
Log Returns
Log returns are additive across time, making them useful for multi-period analysis.
def log_return(prices: pd.Series) -> pd.Series:
return np.log(prices / prices.shift(1))
# Log returns ≈ simple returns for small values
# but sum correctly: total_log_return = sum(daily_log_returns)
Use simple returns for portfolio calculations and reporting. Use log returns for statistical analysis and modeling.
Cumulative Returns
def cumulative_return(returns: pd.Series) -> pd.Series:
return (1 + returns).cumprod() - 1
# Starting with $10,000
initial_capital = 10_000
equity_curve = initial_capital * (1 + returns).cumprod()
Volatility
Volatility measures the dispersion of returns — how much prices fluctuate.
def annualized_volatility(returns: pd.Series, periods_per_year: int = 252) -> float:
return returns.std() * np.sqrt(periods_per_year)
# Example: if daily std is 1%, annualized vol ≈ 15.87%
daily_std = 0.01
annual_vol = daily_std * np.sqrt(252) # 0.1587
252 is the typical number of trading days per year for US equities. Use 365 for crypto (24/7 markets).
Sharpe Ratio
The Sharpe ratio measures risk-adjusted return — how much return you earn per unit of risk.
def sharpe_ratio(
returns: pd.Series,
risk_free_rate: float = 0.05, # Annualized; update to current T-bill rate, use 0.0 for crypto
periods_per_year: int = 252
) -> float:
excess_returns = returns - risk_free_rate / periods_per_year
return (excess_returns.mean() / excess_returns.std()) * np.sqrt(periods_per_year)
Interpreting Sharpe Ratios
| Sharpe Ratio | Interpretation |
|---|---|
| < 0 | Losing money (underperforming risk-free rate) |
| 0 – 1.0 | Below average |
| 1.0 – 2.0 | Good |
| 2.0 – 3.0 | Very good |
| > 3.0 | Excellent (verify — may indicate overfitting) |
A Sharpe ratio above 3.0 in backtesting often indicates overfitting to historical data. Be skeptical of strategies with extremely high Sharpe ratios.
Drawdown
Drawdown measures the decline from a portfolio’s peak value — the worst-case loss an investor would experience.
def drawdown(equity_curve: pd.Series) -> pd.DataFrame:
peak = equity_curve.cummax()
dd = (equity_curve - peak) / peak
return pd.DataFrame({
'equity': equity_curve,
'peak': peak,
'drawdown': dd
})
def max_drawdown(equity_curve: pd.Series) -> float:
peak = equity_curve.cummax()
dd = (equity_curve - peak) / peak
return dd.min()
# Example: max_drawdown of -0.15 means the portfolio lost 15% from its peak
Drawdown Duration
How long the portfolio stayed below its previous peak.
def drawdown_duration(equity_curve: pd.Series) -> int:
peak = equity_curve.cummax()
underwater = equity_curve < peak
# Count consecutive True values
groups = (~underwater).cumsum()
durations = underwater.groupby(groups).sum()
return int(durations.max()) if len(durations) > 0 else 0
Alpha and Beta
Beta measures a strategy’s sensitivity to market movements. Alpha is the excess return after accounting for market exposure.
def alpha_beta(
strategy_returns: pd.Series,
benchmark_returns: pd.Series,
risk_free_rate: float = 0.05, # Annualized; update to current T-bill rate, use 0.0 for crypto
periods_per_year: int = 252
) -> tuple[float, float]:
# Beta = Cov(strategy, benchmark) / Var(benchmark)
covariance = np.cov(strategy_returns.dropna(), benchmark_returns.dropna())
beta = covariance[0, 1] / covariance[1, 1]
# Alpha (annualized) = strategy_return - risk_free - beta * (benchmark_return - risk_free)
rf_daily = risk_free_rate / periods_per_year
alpha = (
(strategy_returns.mean() - rf_daily)
- beta * (benchmark_returns.mean() - rf_daily)
) * periods_per_year
return alpha, beta
Interpreting Alpha and Beta
| Metric | Value | Meaning |
|---|---|---|
| Beta = 1.0 | — | Moves with the market |
| Beta > 1.0 | — | More volatile than the market |
| Beta < 1.0 | — | Less volatile than the market |
| Beta ≈ 0 | — | Market-neutral strategy |
| Alpha > 0 | — | Outperforming after adjusting for risk |
| Alpha < 0 | — | Underperforming after adjusting for risk |
Win Rate and Profit Factor
Trade-level metrics for evaluating strategy quality.
def trade_metrics(pnl: pd.Series) -> dict:
wins = pnl[pnl > 0]
losses = pnl[pnl < 0]
win_rate = len(wins) / len(pnl) if len(pnl) > 0 else 0
avg_win = wins.mean() if len(wins) > 0 else 0
avg_loss = abs(losses.mean()) if len(losses) > 0 else 0
# Profit factor = gross profits / gross losses
profit_factor = wins.sum() / abs(losses.sum()) if len(losses) > 0 else float('inf')
return {
'total_trades': len(pnl),
'win_rate': win_rate,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': profit_factor,
'expectancy': pnl.mean(),
}
| Metric | Good Value | Meaning |
|---|---|---|
| Win Rate | > 50% | More winning trades than losing (depends on payoff ratio) |
| Profit Factor | > 1.5 | Gross profits are 1.5x gross losses |
| Expectancy | > 0 | Average trade is profitable |
A strategy can be profitable with a low win rate (30%) if the average win is much larger than the average loss. Win rate alone is not meaningful without the payoff ratio.
Putting It All Together
def strategy_report(equity_curve: pd.Series, benchmark: pd.Series) -> dict:
returns = equity_curve.pct_change().dropna()
bench_returns = benchmark.pct_change().dropna()
alpha, beta = alpha_beta(returns, bench_returns)
return {
'total_return': (equity_curve.iloc[-1] / equity_curve.iloc[0]) - 1,
'annualized_return': (1 + returns.mean()) ** 252 - 1,
'annualized_volatility': annualized_volatility(returns),
'sharpe_ratio': sharpe_ratio(returns),
'max_drawdown': max_drawdown(equity_curve),
'alpha': alpha,
'beta': beta,
}
Summary
- Returns: Simple for reporting, log for statistical analysis
- Volatility: Annualized standard deviation of returns (√252 scaling)
- Sharpe ratio: Risk-adjusted return — the single most important metric
- Drawdown: Peak-to-trough loss — measures worst-case pain
- Alpha/Beta: Performance relative to a benchmark
- Win rate / Profit factor: Trade-level quality metrics
Next Steps
The next chapter explores historical market events and their implications for algorithmic trading.