Are Financial Returns Really Normal?
This document is intended for educational purposes as part of my Risk Financial Management course at Tec de Monterrey. For any questions or suggestions, feel free to contact me at iguizar@tec.mx.
1 Intro
Up to now, the calculation of Value at Risk (VaR) and Expected Shortfall (ES) has been simplified by the assumption that returns follow a normal distribution. Empirical evidence, however, suggests that financial returns frequently exhibit heavy tails and asymmetries that a normal distribution would fail to capture.
Wrongly assuming that financial returns follow a normal distribution can lead to significant misestimations in risk management:
It may underestimate the probability of extreme losses (fat tails). This means that risk models relying on normality may understate the likelihood and magnitude of financial shocks or extreme market downturns, leaving institutions unprepared for extreme losses.
Regulatory frameworks (e.g., Basel Accords) require robust risk estimation. If an institution incorrectly assumes normality, it might not meet capital adequacy requirements, leading to regulatory penalties or increased capital charges.
If multiple institutions rely on flawed risk models assuming normality, the underestimation of systemic risk could exacerbate financial instability during crises.
In this document, we will learn to test the validity of the normality assumption. Then, we will introduce the t-distribution as a more flexible alternative for modeling risk, useful when significant deviations from normality are found, ensuring that our risk measurements better reflect the actual behavior of financial markets.
2 The normal distribution
The normal distribution is one of the most fundamental probability distributions in financial statistics. As we have seen, it serves as a common assumption for risk estimations. Before testing whether financial returns follow a normal distribution, we will review its characteristics, shape, and some properties.
A random variable \(X\) follows a normal distribution if its probability density function (PDF) is given by:
\[ f(x) = \frac{1}{\sigma\sqrt{2\pi}}exp^{-\frac{(x-\mu)^2}{2\sigma^2}} \]
where:
\(\mu\) is the mean (location parameter)
\(\sigma\) is the standard deviation (measures dispersion).
The PDF describes a bell-shaped curve as shown in Figure 1.
2.1 Properties of the Normal Distribution
- Symmetry Around the Mean.
The normal distribution is perfectly symmetric around its mean \(\mu\), meaning that: \(P(X \le \mu)=1-P(X \ge \mu)=50\%\). As shown by the coloured area in Figure 1.
- Unimodal Distribution.
The normal distribution has a single peak, occurring at \(x =\mu\), meaning that the most probable outcome is the mean.
- Skewness and Kurtosis.
A normal distribution is symmetric (zero skewness) and its tails are neither particularly thick nor thin compared to other distributions (kurtosis = 3).
2.2 Assessing Normality
Visual analysis is a useful first step in detecting departures from normality. Skewness (asymmetry) and fat tails (higher peak and wider tails than a normal distribution) signal deviations from normality. Calculate skewness and kurtosis as follows:
- Skewness: \[ S = \frac{E(X-\mu)^3}{\sigma^3}=0 \]
\(S>0\) \(\rightarrow\) Right-skewed (long right tail).
\(S<0\) \(\rightarrow\) Left-skewed (long left tail)
\(S=0\) \(\rightarrow\) Symmetric. As a Normal distribution.
- Kurtosis: \[ K = \frac{E(X-\mu)^4}{\sigma^4}=3 \]
\(K>3\). Leptokurtic: Heavy tails \(\rightarrow\) higher risk of extreme events.
\(K<3\). Platykurtic: Thin tails \(\rightarrow\) lower risk of extreme events.
\(K=3\). Mesokurtic. As a Normal distribution.
While visual methods provide an intuitive understanding, formal statistical tests help confirm deviations from normality:
Jarque-Bera (JB) test
It tests the null hypothesis (\(H_0\)) that the distribution is normal. The JB statistic is:
\[ JB = \frac{T}{6}\Big(S^2 + \frac{1}{4}(K-3)^2 \Big) \]
where \(T\) is the number of observations, \(S\) is the sample skewness, \(K\) is the sample kurtosis. Note, if the distribution is normal, \(JB=0\). Deviations from the normal, increases \(JB\) which lowers the p-value.
\[ \text{If} \ p-value < \alpha \Rightarrow \text{Reject} \ H_0 \]
and conclude that the distribution is not normal.
3 Application
Load the libraries
import numpy as np
import pandas as pd
import scipy.stats as st
import matplotlib.pyplot as plt
import pandas_datareader.data as web
import statsmodels.api as sm
import statsmodels.tsa.api as tsa
import seaborn as snsObtain and clean data
# Define parameters
symbol = 'SP500'
start_date = '2021-01-01'
end_date = '2025-01-01'
# Fetch S&P 500 data
SP = web.DataReader(symbol, 'fred', start=start_date, end=end_date)
# Convert index to datetime and rename column
SP.index = pd.to_datetime(SP.index)
SP.rename(columns={symbol: 'Price'}, inplace=True)
# Generate a business day range and reindex
business_days = pd.date_range(start=SP.index.min(), end=SP.index.max(), freq='B')
SP = SP.reindex(business_days, method='ffill') # Forward fill missing data
SP.index.name = 'DATE'
# Calculate daily log returns
SP['Returns'] = np.log(SP['Price'] / SP['Price'].shift(1))
SP.dropna(inplace=True)The distribution of the SP500 daily log returns is shown in the histogram as follows:
plt.figure(figsize=(7, 5))
plt.hist(SP['Returns'], bins=50, density=True, alpha=0.6, color='lightseagreen', label='Empirical Distribution')
plt.title('Distribution of S&P 500 Daily Log Returns')
plt.xlabel('Log Return')
plt.ylabel('Density')
plt.legend()
plt.show()Or as density distribution in Figure 2, where, to facilitate the comparison, a normal distribution with the mean and standard deviation of the SP500 log returns has been added.
The distribution of the actual returns shows clear deviation from the normal function. We then procced to formally estimate the measures of sweness, kurtosis, and the JB statistic, to test for normality.
# Calculate skewness and kurtosis
skew = st.skew(SP['Returns'])
kurt = st.kurtosis(SP['Returns'], fisher=True) # Fisher=True gives excess kurtosis
print(f"Skewness: {skew:,.2f}")
print(f"Kurtosis: {kurt:,.2f}")
# Run Jarque-Bera test
jb_stat, jb_p_value = st.jarque_bera(SP['Returns'])
print(f"JB Statistic: {jb_stat:,.2f}")
print(f"JB p_value: {jb_p_value:,.4f}")Skewness: -0.28
Kurtosis: 1.94
JB Statistic: 164.25
JB p_value: 0.0000
The results show that the distribution of actual returns is slightly left-skewed, with “thin” tails. The JB test rejects the normality assumption.
4 The t-distribution
Having established that S&P 500 returns deviate from normality, we need a more flexible distribution that better captures the actual distribution. The t-Distribution is a powerful alternative because, it serves as a base to allow for both skewness and tail heaviness to be explicitly modeled, addressing the limitations of the normal distribution.
The probability density function of a students t-distribution is given by:
\[ f(x) = \frac{\Gamma \big(\frac{\nu+1}{2}\big)}{\sqrt{\nu \pi}\ \Gamma(\frac{\nu}{2})}\Big(1+\frac{x^2}{\nu}\Big)^{-\frac{\nu+1}{2}} \]
Where \(\Gamma\) is the gamma function and \(\nu\), the degrees of freedom, is a parameter to control for tail heaviness.
Small \(\nu\) \(\rightarrow\) Heavier tails (more extreme returns).
Large \(\nu\) \(\rightarrow\) Approaches a normal distribution.
Key Properties:
Mean: 0, for \(\nu>1\), symmetric around zero.
Variance: , for \(\nu>2\).
Has fatter tails than the normal distribution.
The t-distribution is better at capturing fat tails but remains symmetric, meaning it does not address skewness in financial data.
4.1 The Re-scaled t-Distribution
The student’s t distribution can be re-scaled by introducing a location parameter (\(\hat{\mu}\)) and a scale parameter (\(\hat{\sigma}\)).
We describe the distribution of daily log returns as: \[ return = \hat{\mu} + \hat{\sigma}\varepsilon \]
where \(\varepsilon = \frac{x-\hat\mu}{\hat\sigma}\) has the classical student’s t-distribution with \(\nu\) degrees of freedom.
Employing the normal distribution, we estimated two parameters: the mean(\(\mu\)) and standard deviation (\(\sigma\)). There are now three parameters to be estimated:
\(\hat{\mu}\), which adjusts the location (mean)
\(\hat{\sigma}\), which adjusts the scale (volatility)
\(\nu\), the degrees of freedom.
To apply this in python we must install the library “distfit”. Only need to do it once.
!pip install distfitfrom distfit import distfit
returns = SP['Returns']
# Fit a re-scaled t-distribution using distfit
dist = distfit(distr="t")
dist.fit_transform(returns)
fitted_params = dist.model
# Extract relevant parameters
nu_hat, mu_hat, sigma_hat = fitted_params["params"]
# Store results in a DataFrame for display
params_t = pd.DataFrame({
"Parameter": ["Location (μ)", "Scale (σ)", "Degrees of Freedom (ν)"],
"Estimated Value": [mu_hat, sigma_hat, nu_hat]
})[distfit] >INFO> fit
[distfit] >INFO> transform
[distfit] >INFO> [t] [0.03 sec] [RSS: 249.002] [loc=0.001 scale=0.008]
[distfit] >INFO> [t] [0.03 sec] [RSS: 249.002] [loc=0.001 scale=0.008]
[distfit] >INFO> Compute confidence intervals [parametric]
params_t| Parameter | Estimated Value | |
|---|---|---|
| 0 | Location (μ) | 0.000811 |
| 1 | Scale (σ) | 0.008178 |
| 2 | Degrees of Freedom (ν) | 4.954356 |
Comparing the three distributions:
x_vals = np.linspace(returns.min(), returns.max(), 1000)
mu = np.mean(returns)
sd = np.std(returns)
normal_pdf = st.norm.pdf(x_vals, loc=mu, scale=sd)
t_pdf = st.t.pdf(x_vals, df=nu_hat, loc=mu_hat, scale=sigma_hat)
# Plot the empirical density
plt.figure(figsize=(7, 5))
sns.kdeplot(returns, fill=True, color="#69b3a2", alpha=0.4, linewidth=2, label="Empirical Density")
# Plot the normal distribution fit
plt.plot(x_vals, normal_pdf, color="orange", linestyle="solid", linewidth=1.5, label="Normal Fit")
# Plot the t-distribution fit
plt.plot(x_vals, t_pdf, color="blue", linestyle="solid", linewidth=1.5, label="t-dist Fit")
plt.xlim(-0.05, 0.05)
plt.title("Distribution of the Log Returns")
plt.xlabel("Log Return")
plt.ylabel("Density")
plt.legend()
plt.show()Calculating risk:
# Set the confindene level
X = 0.95
alpha = 1-X
# Set random seed for reproducibility
np.random.seed(123123)
# Simulate returns using the fitted re-scaled t-distribution parameters
sim_returns = st.t.rvs(df=nu_hat, loc=mu_hat, scale=sigma_hat, size=100000)
VaR = np.percentile(sim_returns, alpha * 100)
ES = np.mean(sim_returns[sim_returns < VaR])
print(f"1-day 95% VaR: {-(np.exp(VaR)-1):.2%}")
print(f"1-day 95% ES: {-(np.exp(ES)-1):.2%}")1-day 95% VaR: 1.55%
1-day 95% ES: 2.25%
Observe the difference in the risk metrics when assuming a normal distribution:
VaR_Normal = st.norm.ppf(1-X,loc=mu, scale=sd)
z = st.norm.ppf(1-X)
ES_Normal = mu - sd * st.norm.pdf(z) / (1-X)
print(f"VaR under normality assump.: {-(np.exp(VaR_Normal)-1):.2%}")
print(f"ES uner normality assump.: {-(np.exp(ES_Normal)-1):.2%}")VaR under normality assump.: 1.65%
ES uner normality assump.: 2.07%