12 Risk metrics for investments with Python. From standard deviation to R-squared

In the investing world, where gains and losses are changing every second, understanding the risk is one of the most important things to do. This article guides us through 12 risk metrics you can easily compute with Python. These metrics range from the classic Standard Deviation, which measures the variability of returns, to R-squared, which compares your investment’s performance with a benchmark index.

In this article, along with the Python code, I will provide a high-level introduction to each metric. Each metric requires a separate article for a detailed explanation. Also, we will calculate the metrics for each stock, but I will not calculate the portfolio total risk. This will be the subject of a separate article.

To start, we should first get the daily prices for 2023 of Apple (AAPL) and Tesla (TSLA) using the yfinance python module. For some metrics, you will need to use a market index as a benchmark in addition to the stock itself. The most common practice for major stocks is to use the S&P 500 Index. You can change the 2 stocks as well as the market with the tickers of your choice.

import pandas as pd
import numpy as np
import yfinance as yf 

symbol_1 = 'AAPL'
symbol_2 = 'TSLA'
market = '^GSPC'

df_asset1 = yf.download(symbol_1, start='2023-01-01', end='2023-12-31')['Adj Close'].rename(symbol_1);
df_asset2 = yf.download(symbol_2, start='2023-01-01', end='2023-12-31')['Adj Close'].rename(symbol_2);
df_market = yf.download(market, start='2023-01-01', end='2023-12-31')['Close'].rename(market);
df_main = pd.merge(df_asset1, df_asset2, on='Date')
df_main = pd.merge(df_main, df_market, on='Date')
df = df_main.pct_change().dropna()
df

The pandas dataframe (df) handles the daily returns of the 2 stocks and the market, but not the actual prices which are not needed.

Standard deviation

Standard deviation indicates the degree of variation or volatility in the returns of each stock or index. A higher standard deviation implies higher volatility, while a lower standard deviation suggests lower volatility. Being a risk measure means that higher values represent a potentially riskier investment.

def calculate_standard_deviation(returns):
    return np.std(returns)

print(f'{symbol_1} has a Standard Deviation of {calculate_standard_deviation(df[symbol_1])}')
print(f'{symbol_2} has a Standard Deviation of {calculate_standard_deviation(df[symbol_2])}')
print(f'{market} has a Standard Deviation of {calculate_standard_deviation(df[market])}')

AAPL has a Standard Deviation of 0.01254478155112219
TSLA has a Standard Deviation of 0.03309894573475674
^GSPC has a Standard Deviation of 0.008241846331439819

Among the assets, AAPL displays the lowest standard deviation (0.0125), suggesting relatively stable returns. TSLA exhibits higher volatility (0.0331), indicating more significant price fluctuations. S&P 500 shows moderate volatility (0.00824), reflecting its broader market representation and comparatively stable performance.

Beta

Beta measures the sensitivity of a stock’s returns to changes in the returns of a benchmark index. A beta greater than 1 indicates higher volatility compared to the benchmark, while a beta less than 1 suggests lower volatility.

def calculate_beta(returns, benchmark_returns):
    covariance = returns.cov(benchmark_returns)
    variance = benchmark_returns.var()
    beta = covariance / variance
    return beta

print(f'{symbol_1} has a Beta of {calculate_beta(df[symbol_1], df[market])} using {market} as the benchmark')
print(f'{symbol_2} has a Beta of {calculate_beta(df[symbol_2], df[market])} using {market} as the benchmark')

AAPL has a Beta of 1.1045140840353886 using ^GSPC as the benchmark
TSLA has a Beta of 2.219296862746592 using ^GSPC as the benchmark

AAPL’s beta of 1.104 indicates it’s slightly more volatile than the S&P 500. TSLA’s higher beta of 2.219 signifies it’s more than twice as volatile as the market, which means greater sensitivity to market movements.

Maximum Drawdown

Maximum Drawdown is a risk metric that measures the maximum loss from a peak to a trough of a portfolio’s value during a specific period, usually expressed as a percentage. It provides insight into the worst-case scenario or the largest decline an investment experienced over a certain time frame.

def calculate_max_drawdown(returns):
    wealth_index = np.cumprod(1 + returns)
    peak_index = np.argmax(wealth_index)
    trough_index = np.argmin(wealth_index[:peak_index + 1])

    max_drawdown = (wealth_index.iloc[trough_index] - wealth_index.iloc[peak_index]) / wealth_index.iloc[peak_index]
    return max_drawdown

print(f'{symbol_1} has a maximum drawdown of {calculate_max_drawdown(df[symbol_1])}')
print(f'{symbol_2} has a maximum drawdown of {calculate_max_drawdown(df[symbol_2])}')
print(f'{market} has a maximum drawdown of {calculate_max_drawdown(df[market])}')

AAPL has a maximum drawdown of -0.3724441566909941
TSLA has a maximum drawdown of -0.6238494657551136
^GSPC has a maximum drawdown of -0.2038843028608435

TSLA exhibits the deepest drawdown (-0.624), indicating the largest peak-to-trough decline in value historically. AAPL follows with a drawdown of -0.372, while the S&P 500 “suffers” the smallest drawdown of -0.204 among the three assets.

Sharpe ratio

The Sharpe ratio is a measure of risk-adjusted performance and indicates the excess return generated per unit of risk, with risk measured as the standard deviation of returns. Higher Sharpe ratios generally suggest better risk-adjusted performance

def calculate_sharpe_ratio(returns, risk_free_rate):
    average_return = np.mean(returns)
    volatility = np.std(returns)
    sharpe_ratio = (average_return - risk_free_rate) / volatility
    return sharpe_ratio

print(f'{symbol_1} has a Sharpe ratio of {calculate_sharpe_ratio(df[symbol_1], 0)}')
print(f'{symbol_2} has a Sharpe ratio of {calculate_sharpe_ratio(df[symbol_2], 0)}')
print(f'{market} has a Sharpe ratio of {calculate_sharpe_ratio(df[market], 0)}')

AAPL has a Sharpe ratio of 0.1462701765459904
TSLA has a Sharpe ratio of 0.11760865791443038
^GSPC has a Sharpe ratio of 0.11184268785503289

AAPL’s Sharpe ratio of 0.146 indicates its return per unit of risk is higher than TSLA’s (0.118) and the S&P 500’s (0.112). It suggests AAPL provides better risk-adjusted returns compared to TSLA and the broader market represented by the S&P 500.

Sortino ratio

Similar to the Sharpe ratio, the Sortino ratio focuses on downside volatility, considering only the standard deviation of negative returns. It provides a measure of risk-adjusted return with a focus on downside risk.

def calculate_sortino_ratio(returns, risk_free_rate=0):
    downside_returns = np.minimum(returns - risk_free_rate, 0)
    downside_volatility = np.std(downside_returns)

    average_return = np.mean(returns - risk_free_rate)  # Adjust for risk-free rate

    if downside_volatility == 0:
        return np.inf  # Avoid division by zero

    sortino_ratio = average_return / downside_volatility
    return sortino_ratio

print(f'{symbol_1} has a Sortino ratio of {calculate_sortino_ratio(df[symbol_1])}')
print(f'{symbol_2} has a Sortino ratio of {calculate_sortino_ratio(df[symbol_2])}')
print(f'{market} has a Sortino ratio of {calculate_sortino_ratio(df[market])}')

AAPL has a Sortino ratio of 0.2703070004934494
TSLA has a Sortino ratio of 0.2198285494487039
^GSPC has a Sortino ratio of 0.20205301637520193

AAPL’s Sortino ratio of 0.270 indicates its risk-adjusted return, focusing on downside volatility, surpasses that of TSLA (0.220) and S&P 500 (0.202). This suggests that AAPL offers superior risk-adjusted returns, especially in mitigating downside risk.

Treynor Ratio

The Treynor Ratio is another risk-adjusted performance measure similar to the Sharpe ratio. It’s calculated by dividing the excess return over the risk-free rate by the beta of the investment, where beta represents the investment’s sensitivity to market movements

def calculate_treynor_ratio(returns, market_returns, risk_free_rate):
    excess_return = returns - risk_free_rate
    beta = calculate_beta(returns, market_returns)

    if beta == 0:
        return np.inf  # Avoid division by zero

    treynor_ratio = excess_return.mean() / beta
    return treynor_ratio

print(f'{symbol_1} has a Treynor ratio of {calculate_treynor_ratio(df[symbol_1], df[market], 0)} used {market} as the market')
print(f'{symbol_2} has a Treynor ratio of {calculate_treynor_ratio(df[symbol_2], df[market], 0)} used {market} as the market')

AAPL has a Treynor ratio of 0.0016612983380977283 used ^GSPC as the market
TSLA has a Treynor ratio of 0.0017540341950601794 used ^GSPC as the market

AAPL’s Treynor ratio of 0.00166 suggests its risk-adjusted return per unit of systematic risk, relative to the S&P 500, is lower than TSLA’s (0.00175). While both assets yield positive returns relative to market risk, TSLA demonstrates slightly better performance in this context.

Calmar Ratio

The Calmar Ratio is a risk-adjusted performance measure that evaluates the ratio of the average annual rate of return to the maximum drawdown, providing a measure of risk-adjusted returns. Higher Calmar ratios generally indicate better risk-adjusted performance.

def calculate_calmar_ratio(returns, period=252):  # Assuming 252 trading days in a year
    annualized_return = np.mean(returns) * period
    max_drawdown = calculate_max_drawdown(returns)

    if max_drawdown == 0:
        return np.inf  # Avoid division by zero

    calmar_ratio = annualized_return / abs(max_drawdown)
    return calmar_ratio

print(f'{symbol_1} has a Calmar ratio of {calculate_calmar_ratio(df[symbol_1])}')
print(f'{symbol_2} has a Calmar ratio of {calculate_calmar_ratio(df[symbol_2])}')
print(f'{market} has a Calmar ratio of {calculate_calmar_ratio(df[market])}')

AAPL has a Calmar ratio of 1.241532991109455
TSLA has a Calmar ratio of 1.5724403811854653
^GSPC has a Calmar ratio of 1.1393282311725124

TSLA’s Calmar ratio of 1.572 indicates its average annual return relative to its maximum drawdown is higher compared to AAPL’s (1.242) and S&P 500’s (1.139). This suggests that TSLA has delivered superior risk-adjusted returns based on its drawdown levels.

Ulcer Index

The Ulcer Index is a measure of the depth and duration of drawdowns in an investment. It quantifies the extent of the price decline from its most recent peak, with lower values indicating less severe and shorter drawdowns

def calculate_ulcer_index(returns):
    returns = returns.dropna()
    wealth_index = np.cumprod(1 + returns)
    previous_peaks = np.maximum.accumulate(wealth_index)
    drawdowns = (wealth_index - previous_peaks) / previous_peaks
    squared_drawdowns = np.square(drawdowns)
    ulcer_index = np.sqrt(np.mean(squared_drawdowns))
    return ulcer_index

print(f'{symbol_1} has an Ulcer index of {calculate_ulcer_index(df[symbol_1])}')
print(f'{symbol_2} has an Ulcer index of {calculate_ulcer_index(df[symbol_2])}')
print(f'{market} has an Ulcer index of {calculate_ulcer_index(df[market])}')

AAPL has an Ulcer index of 0.05509672077820546
TSLA has an Ulcer index of 0.14917579484097326
^GSPC has an Ulcer index of 0.03408355669692506

S&P 500 has the lowest Ulcer Index (0.034), indicating the least severe drawdowns relative to returns. AAPL follows with an Ulcer Index of 0.055, while TSLA exhibits the highest Ulcer Index (0.149), reflecting the most significant drawdowns relative to returns among the three assets.

Value at Risk (VaR)

Value at Risk (VaR) is a statistical measure used to quantify the potential loss on an investment over a specific time horizon and with a certain confidence level. There are different methods to calculate VaR, and one common approach is the historical simulation method.

def calculate_var(returns, confidence_level=0.95):
    returns_sorted = np.sort(returns)
    n = len(returns)
    position = int(n * (1 - confidence_level))
    
    var = -returns_sorted[position]
    return var

print(f'{symbol_1} has a Value at Risk of {calculate_var(df[symbol_1])}')
print(f'{symbol_2} has a Value at Risk of {calculate_var(df[symbol_2])}')
print(f'{market} has a Value at Risk of {calculate_var(df[market])}')

AAPL has a Value at Risk of 0.01725376628542985
TSLA has a Value at Risk of 0.05030873801899527
^GSPC has a Value at Risk of 0.013788741489130674

Among the assets, TSLA has the highest Value at Risk (VaR) at 0.050, indicating the largest potential loss over a specific period at a given confidence level. AAPL follows with a VaR of 0.017, while the S&P 500 exhibits the lowest VaR at 0.014, suggesting the least potential loss among the three assets.

Conditional Value at Risk (CVaR)

Conditional Value at Risk (CVaR), also known as Expected Shortfall (ES), is a risk measure that quantifies the expected loss beyond the Value at Risk (VaR) at a certain confidence level. While VaR gives the maximum loss with a certain probability, CVaR provides an estimate of the expected loss given that the loss exceeds the VaR.

def calculate_cvar(returns, confidence_level=0.95):
    sorted_returns = np.sort(returns)
    n = len(returns)
    position = int(n * (1 - confidence_level))

    cvar = -np.mean(sorted_returns[:position])
    return cvar

print(f'{symbol_1} has a Conditional Value at Risk of {calculate_cvar(df[symbol_1])}')
print(f'{symbol_2} has a Conditional Value at Risk of {calculate_cvar(df[symbol_2])}')
print(f'{market} has a Conditional Value at Risk of {calculate_cvar(df[market])}')

AAPL has a Conditional Value at Risk of 0.02563116524184404
TSLA has a Conditional Value at Risk of 0.06672154043052804
^GSPC has a Conditional Value at Risk of 0.015845102985801485

TSLA displays the highest Conditional Value at Risk (CVaR) at 0.067, indicating the expected loss beyond the VaR level. AAPL follows with a CVaR of 0.026, while the S&P 500 demonstrates the lowest CVaR at 0.016, suggesting the least expected loss beyond the VaR level as expected being the market it self.

Downside Deviation

Downside Deviation is a risk metric that focuses on the volatility of negative returns or downside risk. It measures the dispersion of returns below a certain threshold, typically zero.

def calculate_downside_deviation(returns, threshold=0):
    downside_returns = np.minimum(returns - threshold, 0)
    downside_deviation = np.std(downside_returns)
    return downside_deviation

print(f'{symbol_1} has a Downside Deviation of {calculate_downside_deviation(df[symbol_1])}')
print(f'{symbol_2} has a Downside Deviation of {calculate_downside_deviation(df[symbol_2])}')
print(f'{market} has a Downside Deviation of {calculate_downside_deviation(df[market])}')

AAPL has a Downside Deviation of 0.0067883086479370355
TSLA has a Downside Deviation of 0.01770799377974175
^GSPC has a Downside Deviation of 0.004562120690564971

Among the assets, TSLA exhibits the highest Downside Deviation at 0.018, indicating greater volatility in negative returns. AAPL follows with a Downside Deviation of 0.007, while the S&P 500 demonstrates the lowest at 0.005, suggesting relatively lower volatility in negative returns among the three assets.

R-squared

R-squared, also known as the coefficient of determination, is a statistical measure that represents the proportion of the variance in the dependent variable that is explained by the independent variable(s) in a regression model. In the context of financial analysis, it can be used to assess how well the performance of an investment or portfolio is explained by a benchmark index or other factors.

def calculate_r_squared(actual, predicted):
    mean_actual = np.mean(actual)
    total_sum_of_squares = np.sum((actual - mean_actual) ** 2)
    residual_sum_of_squares = np.sum((actual - predicted) ** 2)
    
    r_squared = 1 - (residual_sum_of_squares / total_sum_of_squares)
    return r_squared

print(f'{symbol_1} has a R-Squared of {calculate_r_squared(df[symbol_1], df[market])} using {market} as the benchmark index')
print(f'{symbol_2} has a R-Squared of {calculate_r_squared(df[symbol_2], df[market])} using {market} as the benchmark index')

AAPL has a R-Squared of 0.5165678698098413 using ^GSPC as the benchmark index
TSLA has a R-Squared of 0.20515035436500972 using ^GSPC as the benchmark index

AAPL’s R-squared of 0.517 suggests approximately 51.7% of its variability is explained by movements in the benchmark index (S&P 500). TSLA’s lower R-squared of 0.205 indicates only about 20.5% of its variability is explained by movements in the S&P 500, suggesting a weaker correlation.

Before the end

While all metrics have similarities and differences, it’s not always necessary to use every metric. Selecting the most relevant metrics depends on your investment strategy and goals. It’s advisable to focus on a subset of metrics that align with your investment style and objectives.

You can find the above code in this Jupyter Notebook here.

Thanks for reading!