I recently read Kritzmen and Li’s clever 2010 paper Skulls, Financial Turbulence, and Risk Management. Kritzmen and Li characterize financial turbulence as a period where established financial relationships uncouple, prices swing, and market predictions break down. Does that sound like financial markets in 2020? Yup. So I thought it would be interesting to take a look at how these authors’ proposed turbulence index characterizes the year 2020 so far. And of course, we’ll see that current markets are indeed ridiculously turbulent.
The core concept of this paper is that in a turbulent regime established correlations between assets break down, so a measurement of turbulence ought to take correlation into account. The way that accounting is done is by constructing a “turbulence index” based on the Mahalanobis distance: $$ d_t =(\vec{x}_t – \mu )\Sigma^{-1}(\vec{x}_t – \mu)^T$$ where:
- $d_t$ is the value of the index at time $t$
- $\vec{x}_t$ is an $1 \times n$ row vector of asset returns
- $\mu$ is the the $1 \times n$ row vector of means of the columns in the data set
- $\Sigma$ is the $n \times n$ covariance matrix for the columns in the data set
Kritzmen & Li consider a time frame to be turbulent when $d_t$ is above a particular percentile threshold (e.g. 75%).
To give this formula a try, I used a free Quandl account and its associated Python package to pull a bunch of economic and financial indicators and compute their monthly returns for a period from 1980 – 2020.
import pandas as pd
import quandl
quandl.ApiConfig.api_key = os.environ["QUANDL_API_KEY"] # put your API key in your environment or hard code it here.
# Associate the Quandl code to a human
series_names = {
"MULTPL/SP500_REAL_PRICE_MONTH": "S&P",
"LBMA/GOLD":"GOLD",
"CHRIS/CME_C1": "CORN",
"FRED/GDPC1": "GDP",
"FRED/CPIAUCSL": "INFL",
"FRED/DFF": "R",
"FRED/HOUST": "HOUSE",
"FRED/UNEMPLOY": "UNEMPLOY"
}
def fetch_series(key, value):
print(f"Fetching {key}...")
return quandl.get(key,
start_date='1980-01-01',
end_date='2020-04-30',
column_index=1,
collapse="monthly",
transformation="rdiff").rename(columns= lambda x: value)
# Fetch all the data we require and merge the resulting series into a data frame
series = [fetch_series(key, value) for key,value in series_names.items()]
df = pd.concat(series, axis=1)
#
# add the estimated 2020 Q1 GDP loss (yikes!!) https://fred.stlouisfed.org/series/STLENI
#
if 'GDP' in df:
if pd.isna(df.GDP['2020-04-30']):
df.GDP['2020-04-30'] = 0.1526
# GDP is calculated quarterly, so we'll apply linear interpolation to fill missing series values
df['GDP'] = df.GDP.interpolate(method='time', limit_direction='both')
Let’s plot these series:
import scipy as sp
import numpy as np
import numpy.linalg as la
# because 2020 series are such extreme outliers, we don't include them in the mean and variance caclulation
view = df[df.index.year < 2020]
mu = view.mean(axis=0)
sigma_inv = la.inv(sp.matrix(view.cov()))
# create the turbulence index
turbulence = df.apply(lambda x: np.sqrt((x-mu).dot(sigma_inv).dot((x-mu).transpose())), axis=1)
mse = df.apply(lambda x: np.sqrt((x-mu).dot((x-mu).transpose())), axis=1)

The magnitude of the economic dislocation that’s happening is staggering. Let’s see how that dislocation is reflected in the turbulence index.
import scipy as sp
import numpy as np
import numpy.linalg as la
# because 2020 series are such extreme outliers, we don't include them in the mean and variance caclulation
view = df[df.index.year < 2020]
mu = view.mean(axis=0)
sigma_inv = la.inv(sp.matrix(view.cov()))
# create the turbulence index
turbulence = df.apply(lambda x: np.sqrt((x-mu).dot(sigma_inv).dot((x-mu).transpose())), axis=1)
Plotting the turbulence index:
import matplotlib.pyplot as plt
import statsmodels.api as sm
fig = plt.figure()
# smooth the turbulence
lowess = sm.nonparametric.lowess(turbulence, t, frac=0.01)
# plot turbulence and smoothed tubulence
plt.plot(t, lowess[:,1], c='g', lw=0.75 )
plt.plot(t, turbulence, c='b', lw=0.25, alpha=1.0)
plt.xticks(fontsize=4, rotation=90);
plt.grid(which='both')
plt.title("TURBULENCE INDEX")
xmin, xmax, ymin, ymax = plt.axis()
# as in the paper, mark periods as turbulent when turbulence is above the 75 percentile
threshold = np.quantile(turbulence, 0.75)
plt.fill_between(t, ymin, ymax, color='r', where=lowess[:,1]>threshold, alpha=0.25)

As expected, we can see historical moments of financial distress indicated by the turbulence index, including stagflation, Black Monday, the Great Financial Crisis, and of course the COVID-19 outbreak.
Now, can we visualize the contribution of the correlation matrix to our turbulence index? One way to do this is to compare against the mean square error: $$(\vec{x}-\mu)(\vec{x}-\mu)^T.$$
#compute mse to compare against the turbulence index
mse = df.apply(lambda x: np.sqrt((x-mu).dot((x-mu).transpose())), axis=1)
# smooth the turbulence
mse_smooth = pd.Series(sm.nonparametric.lowess(mse, t, frac=0.01)[:,1])
turbulence_smooth = pd.Series(sm.nonparametric.lowess(turbulence, t, frac=0.01)[:,1])
# threshold at 75 percentile
mse_threshold = np.quantile(mse, 0.75)
turbulence_threshold = np.quantile(turbulence, 0.75)
# plot
plt.rcParams['figure.figsize'] = [5,2]
ax1 = plt.axes(frameon=False)
ax1.axes.get_yaxis().set_visible(False)
plt.grid(axis='x', linewidth=0.5)
plt.eventplot(t[mse_smooth > mse_threshold], colors='b', lineoffsets=1, linelengths=0.25, label='MSE')
plt.eventplot(t[turbulence_smooth > turbulence_threshold], colors='r', lineoffsets=0.5, linelengths=0.25, label='TURBULENCE')
plt.xticks(fontsize=4, rotation=90);
plt.ylim(0.25, 1.25)
plt.legend(loc='center', fontsize=4, bbox_to_anchor=(1.05, 1.0))
plt.title("MSE vs. TURBULENCE");

We can see that the turbulence index captures deviations from the mean differently than MSE. Whether the turbulence index is more useful is an empirical question but the Kritzmen & Li paper indicates that it does.
A few notes:
- The series considered here are a collection of economic and financial indicators. The original paper used a collection of global market indices. The data for these series wasn’t easily available through Quandl. Our series are not especially strongly correlated over the time window considered. A different selection of series may give quite different results.
- Means, variances, and covariances in this analysis are assumed to be constant. This seems like an unreasonable assumption, especially over long time horizons. A rolling computation window may give better results.
- The predictive usefulness of the turbulence index should be evaluated by careful back testing. An analysis along these lines may be found here.