Portfolio Performance Measurement › Computing Performance Metrics in Python
Quantitative FinanceAll chapters Portfolio Performance Measurement › Computing Performance Metrics in PythonOne section · Sections in this chapter5 / 11 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's implement these metrics and apply them to realistic portfolio data. We'll work with simulated returns that exhibit properties common in equity portfolios: modest positive drift, time-varying volatility, and occasional drawdowns.
import numpy as np
np.random.seed(42)First, we'll generate synthetic return series for a portfolio and its benchmark, simulating approximately five years of daily data.
import numpy as np
import pandas as pd
# Simulation parameters
n_days = 1260 # Approximately 5 years of trading days
daily_rf = 0.04 / 252 # 4% annual risk-free rate
# Generate benchmark returns (market index)
benchmark_returns = np.random.normal(0.08 / 252, 0.16 / np.sqrt(252), n_days)
# Generate portfolio returns with slight alpha and tracking error
alpha_daily = 0.02 / 252 # 2% annual alpha
tracking_noise = np.random.normal(0, 0.05 / np.sqrt(252), n_days)
portfolio_returns = benchmark_returns + alpha_daily + tracking_noise
# Create a date index
dates = pd.date_range(start="2019-01-01", periods=n_days, freq="B")# Build a DataFrame for analysis
df = pd.DataFrame(
{"portfolio": portfolio_returns, "benchmark": benchmark_returns},
index=dates,
)
# Calculate cumulative returns (wealth paths)
df["portfolio_cumulative"] = (1 + df["portfolio"]).cumprod()
df["benchmark_cumulative"] = (1 + df["benchmark"]).cumprod()Implementing the Sharpe Ratio
def sharpe_ratio(returns, risk_free_rate=0.0, periods_per_year=252):
"""
Calculate the annualized Sharpe ratio.
Parameters:
-----------
returns : array-like
Periodic returns (daily, monthly, etc.)
risk_free_rate : float
Periodic risk-free rate (same frequency as returns)
periods_per_year : int
Number of periods per year for annualization
Returns:
--------
float : Annualized Sharpe ratio
"""
excess_returns = returns - risk_free_rate
if np.std(excess_returns) == 0:
return np.nan
return (
np.mean(excess_returns)
/ np.std(excess_returns, ddof=1)
* np.sqrt(periods_per_year)
)# Calculate Sharpe ratios
portfolio_sharpe = sharpe_ratio(df["portfolio"], daily_rf)
benchmark_sharpe = sharpe_ratio(df["benchmark"], daily_rf)Portfolio Sharpe Ratio: 1.080 Benchmark Sharpe Ratio: 0.864
The portfolio's higher Sharpe ratio reflects both the embedded alpha and the independent tracking noise. A Sharpe ratio near 0.6-0.7 is realistic for a well-managed equity portfolio over a multi-year period.
Implementing the Sortino Ratio
def sortino_ratio(returns, risk_free_rate=0.0, mar=0.0, periods_per_year=252):
"""
Calculate the annualized Sortino ratio.
Parameters:
-----------
returns : array-like
Periodic returns
risk_free_rate : float
Periodic risk-free rate
mar : float
Minimum acceptable return (same frequency as returns)
periods_per_year : int
Number of periods per year for annualization
Returns:
--------
float : Annualized Sortino ratio
"""
excess_returns = returns - risk_free_rate
downside_returns = np.minimum(returns - mar, 0)
downside_deviation = np.sqrt(np.mean(downside_returns**2))
if downside_deviation == 0:
return np.nan
return (
np.mean(excess_returns) / downside_deviation * np.sqrt(periods_per_year)
)portfolio_sortino = sortino_ratio(df["portfolio"], daily_rf)
benchmark_sortino = sortino_ratio(df["benchmark"], daily_rf)Portfolio Sortino Ratio: 1.670 Benchmark Sortino Ratio: 1.309
The Sortino ratio exceeds the Sharpe ratio because our simulated returns are approximately symmetric. In practice, the relationship between these ratios reveals information about the skewness of the return distribution.
Implementing the Information Ratio
def information_ratio(
portfolio_returns, benchmark_returns, periods_per_year=252
):
"""
Calculate the annualized information ratio.
Parameters:
-----------
portfolio_returns : array-like
Portfolio periodic returns
benchmark_returns : array-like
Benchmark periodic returns
periods_per_year : int
Number of periods per year for annualization
Returns:
--------
tuple : (information_ratio, annualized_alpha, annualized_tracking_error)
"""
active_returns = portfolio_returns - benchmark_returns
tracking_error = np.std(active_returns, ddof=1) * np.sqrt(periods_per_year)
annualized_alpha = np.mean(active_returns) * periods_per_year
if tracking_error == 0:
return np.nan, annualized_alpha, tracking_error
ir = annualized_alpha / tracking_error
return ir, annualized_alpha, tracking_errorir, alpha, te = information_ratio(df["portfolio"], df["benchmark"])Information Ratio: 0.858 Annualized Alpha: 4.20% Annualized Tracking Error: 4.89%
The information ratio of approximately 0.4-0.5 is consistent with a skilled active manager. The alpha is close to our simulated 2%, and tracking error matches the noise we introduced.
Implementing Drawdown Analysis
def calculate_drawdowns(cumulative_returns):
"""
Calculate the drawdown series and maximum drawdown.
Parameters:
-----------
cumulative_returns : array-like
Cumulative return series (wealth path starting at 1)
Returns:
--------
tuple : (drawdown_series, max_drawdown, max_drawdown_duration)
"""
# Running maximum
running_max = np.maximum.accumulate(cumulative_returns)
# Drawdown at each point
drawdowns = (running_max - cumulative_returns) / running_max
# Maximum drawdown
max_dd = np.max(drawdowns)
# Calculate drawdown durations
is_in_drawdown = drawdowns > 0
drawdown_periods = []
current_duration = 0
for in_dd in is_in_drawdown:
if in_dd:
current_duration += 1
else:
if current_duration > 0:
drawdown_periods.append(current_duration)
current_duration = 0
if current_duration > 0:
drawdown_periods.append(current_duration)
max_duration = max(drawdown_periods) if drawdown_periods else 0
return drawdowns, max_dd, max_duration# Calculate drawdowns for portfolio and benchmark
portfolio_dd, portfolio_max_dd, portfolio_max_duration = calculate_drawdowns(
df["portfolio_cumulative"].values
)
benchmark_dd, benchmark_max_dd, benchmark_max_duration = calculate_drawdowns(
df["benchmark_cumulative"].values
)
# Add drawdowns to DataFrame
df["portfolio_drawdown"] = portfolio_dd
df["benchmark_drawdown"] = benchmark_ddPortfolio Maximum Drawdown: 19.65% Benchmark Maximum Drawdown: 21.30% Portfolio Max Drawdown Duration: 439 days Benchmark Max Drawdown Duration: 440 days
The maximum drawdown figures highlight the worst historical loss experienced by each strategy. The portfolio's maximum drawdown represents the deepest peak-to-trough decline, while the duration metric indicates the number of days required to recover from that loss. These metrics quantify the tail risk and psychological endurance required to stick with the strategy.
Implementing the Calmar Ratio
def calmar_ratio(returns, periods_per_year=252):
"""
Calculate the Calmar ratio.
Parameters:
-----------
returns : array-like
Periodic returns
periods_per_year : int
Number of periods per year for annualization
Returns:
--------
float : Calmar ratio
"""
# Calculate cumulative returns
cumulative = (1 + np.array(returns)).cumprod()
# Calculate maximum drawdown
running_max = np.maximum.accumulate(cumulative)
drawdowns = (running_max - cumulative) / running_max
max_dd = np.max(drawdowns)
# Annualized Return
total_return = cumulative[-1] - 1
n_years = len(returns) / periods_per_year
annualized_return = (1 + total_return) ** (1 / n_years) - 1
if max_dd == 0:
return np.nan
return annualized_return / max_ddportfolio_calmar = calmar_ratio(df["portfolio"])
benchmark_calmar = calmar_ratio(df["benchmark"])Portfolio Calmar Ratio: 1.158 Benchmark Calmar Ratio: 0.838
A Calmar ratio above 0.5 indicates that annualized returns exceed half the maximum drawdown, suggesting reasonable compensation for the drawdown risk experienced.
Visualizing Performance


The underwater chart (below) provides an intuitive visualization of drawdowns. The depth shows magnitude while the width indicates duration. This view helps investors compare the path their capital took with its endpoint.