# Financial Turbulence: Off the Chart

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:

# 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.

# 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");