Predict financial crises with Python

As of March 9, 2020, we have a sharp decline in US markets, starting with a historic high in February 20, 2020, which currently stands at about -16%. News was full of headlines about the impending recession due to coronavirus, Russia withdrew from the Opec + deal, which hit oil prices (-20% per day) and tomorrow, (March 10, 2020), the MICEX market is also expected to decline by 20%, judging by quotes of our shares in Western markets.


Will a global recession await us? In this article we will try to figure out how you can see in advance the signals of the beginning of the recession using Python.

To answer this question, we will try to use the yield on bonds, stocks and technical analysis. We will use historical data from the US financial market as the first largest stock market in the world. In fact, if a recession begins in the United States, it will begin around the world (as happened in 2008). Also, it’s convenient for us that there are data for the US market for decades, which will allow us to analyze over a significant historical period.

We will take historical data from Yahoo Finance using the yfinance library , from the Fed site. US reserve using the fredapi library and from the Quandl website with various financial information through pandas_datareader. Please note that for Fed and Quandl you need to register to receive an API key (this is free).

All financial crises in the USA we will mark in gray areas directly on the charts.

Import libraries
from fredapi import Fred
import pandas as pd
import os
import pandas_datareader.data as web
import pandas_datareader as pdr
%matplotlib inline
from matplotlib import pyplot as plt
from datetime import date
import yfinance
import numpy as np

api = 'YOUR API HERE'
os.environ["QUANDL_API_KEY"] = 'YOUR API HERE'
os.environ["TIINGO_API_KEY"] = 'YOUR API HERE'

fred = Fred(api_key=api)


We get historical data for S & P500 with Yahoo, bond yield spreads with FRED and the accumulated yield index for bonds with FRED:

GSPC_h = yfinance.download("^GSPC", start="1962-01-01", end="2020-03-09") #SNP500
T10YFF = fred.get_series('T10YFF', observation_start='1962-01-01', observation_end='2020-03-09') #10YB-FFR
T10Y2Y = fred.get_series('T10Y2Y', observation_start='1976-06-01', observation_end='2020-03-09') #10YB-2YB
# ICE BofA US Corp 10-15yr Total Return Index Value
BAMLCC7A01015YTRIV = fred.get_series('BAMLCC7A01015YTRIV', observation_start='1962-01-01', observation_end='2020-03-09')

Risk premium


The risk premium is an indicator that reflects the additional profitability that an investor will receive by assuming increased risk.

This is how financial markets work - the higher the return on investment, the higher the risk.

In order to calculate the risk premium, it is necessary to subtract from the expected return of the risky asset the risk-free rate of return.

For the US market, the risk-free rate of return is the US Fed reserve rate (an analogue of our key rate) - FED.

The risk premium for stocks and bonds is considered differently.
For bonds: bond yield (Yield) minus FED.
For shares: Profit / Price (E / P) indicator minus FED,
where profit is the company's profit for the year, price is the share price at the time the indicator is calculated.

To calculate the profitability of a share, we do not take the dividends paid as profitability, but the profit received by the company, because dividends are only part of the profit that the company pays to shareholders. Becoming the owner of the company through the purchase of its shares, in fact, the final income for us will be precisely the profit that will be distributed among the shareholders in full at the time of liquidation of the company.

We get E / P for S & P500 (SP500_EARNINGS_YIELD_MONTH), risk-free rate (FED Funds Rate) and corporate bond yield of a wide market (Baa Bonds Yield):

# E/P
symbol = 'MULTPL/SP500_EARNINGS_YIELD_MONTH'
SP500_EARNINGS_YIELD_MONTH = web.DataReader(symbol, 'quandl', '1962-01-01', '2020-03-09')
# FED Funds Rate
FEDFUNDS = fred.get_series('FEDFUNDS', observation_start='1962-01-01', observation_end='2020-03-09')
# Baa Bonds Yield
BAA = fred.get_series('BAA', observation_start='1962-01-01', observation_end='2020-03-09')

We calculate the risk premium as profit minus the risk-free rate:

#     
risk_premium = pd.concat([SP500_EARNINGS_YIELD_MONTH, FEDFUNDS],axis=1).fillna(method='bfill')
risk_premium['premium'] = risk_premium['Value'] - risk_premium[0]

#     
risk_premium_b = pd.concat([BAA, FEDFUNDS],axis=1).fillna(method='bfill')
risk_premium_b.columns = ['BAA', 'FEDFUNDS']
risk_premium_b['premium_b'] = risk_premium_b['BAA'] - risk_premium_b['FEDFUNDS']

Let's see what happened for stocks and bonds separately.

For stocks:

Graph code
fig, ax = plt.subplots(figsize=(17,6))

line1, = ax.plot(risk_premium['premium'],linewidth=1)
line1.set_label('risk_premium_stocks')

line2, = ax.plot(SP500_EARNINGS_YIELD_MONTH,linewidth=1)
line2.set_label('SP500_EARNINGS_YIELD_MONTH')
ax.legend(loc='upper left')

par1 = ax.twinx()
line3, = par1.plot(np.log(GSPC_h['Close']),linewidth=0.7, color='red')
line3.set_label('S&P500')
par1.legend(loc='upper left', bbox_to_anchor=(0, 0, 1, 0.88))

plt.xlim(left=date(1962, 1, 1), right=date(2020, 3, 9))
ax.axhline(linewidth=2, color='black', alpha=0.7)
ax.axvspan(date(2007, 12, 1), date(2009, 6, 1), alpha=0.3, color='grey')
ax.axvspan(date(2001, 3, 1), date(2001, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1990, 8, 1), date(1991, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1981, 7, 1), date(1982, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1980, 1, 1), date(1980, 7, 1), alpha=0.3, color='grey')
ax.axvspan(date(1973, 12, 1), date(1975, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1969, 12, 1), date(1970, 11, 1), alpha=0.3, color='grey')

ax.set_xlabel('')
ax.set_ylabel('  , %')
par1.set_ylabel('Log  S&P500')

plt.show()



For bonds:

Graph code
fig, ax = plt.subplots(figsize=(17,6))

line1, = ax.plot(risk_premium_b['premium_b'][date(1987, 12, 1):],linewidth=1, color='k')
line1.set_label('risk_premium_bonds')
ax.legend(loc='upper left', bbox_to_anchor=(0, 0, 1, 0.95))

par1 = ax.twinx()
line2, = par1.plot(np.log(BAMLCC7A01015YTRIV),linewidth=0.7, color='green')
line2.set_label('Log ICE BofA US Corp 10-15yr Total Return Index Value')
par1.legend(loc='upper left')
plt.xlim(left=date(1987, 12, 1), right=date(2020, 3, 9))

ax.axhline(y=1.5, linewidth=2, color='red', ls='--', alpha=0.7)

ax.axvspan(date(2007, 12, 1), date(2009, 6, 1), alpha=0.3, color='grey')
ax.axvspan(date(2001, 3, 1), date(2001, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1990, 8, 1), date(1991, 2, 1), alpha=0.3, color='grey')
ax.axhline(linewidth=2, color='black', alpha=0.7)

ax.set_xlabel('')
ax.set_ylabel('  , %')
par1.set_ylabel('Log  BofA')

plt.show()




As we can see, pre-crisis periods are areas with a negative risk premium for stocks and a reduced risk premium for bonds.

At the same time, for bonds, not always a reduced risk premium is a signal for a crisis on the bond market (the Bof index decreased only in 2008), but a reduced premium on the stock market is almost always (except for the period after the crisis in the early 1980s, when a negative premium was held for a long time) leads to a decrease in the value of shares.

What does a negative risk premium mean for stocks?

When buying a stock, we get a return below the risk-free rate in the market. At the same time, taking full responsibility for the risks of fluctuations in the share price and potential losses. This is not a standard situation, investors understand that the risk premium cannot be negative, as a result of which the markets come to the realization that the current value of the shares is too high and sales are starting. The fall intensifies after investors of a wide range (ordinary people, not institutional investors) begin to panic, selling their portfolios and exacerbating the fall. Thus, the reaction of the markets is fast and strong.
It is worth noting that the market decline does not go to the previous level of risk premium, but always happens much stronger, which increases the risk premium significantly and again enhances the attractiveness of securities for investors.

Moreover, if we look at the risk premiums for stocks and bonds on one chart, we will see that the risk premium for bonds has traditionally been higher than for shares, however, over the past 5 years they have synchronized and both are gradually tending to zero:

Graph code
fig, ax = plt.subplots(figsize=(17,6))

line1, = ax.plot(risk_premium['premium'],linewidth=1)
line1.set_label('risk_premium_stocks')

par1 = ax.twinx()
line3, = par1.plot(np.log(GSPC_h['Close']),linewidth=0.7, color='red')
line3.set_label('S&P500')
par1.legend(loc='upper left', bbox_to_anchor=(0, 0, 1, 0.88))

line2, = ax.plot(risk_premium_b['premium_b'],linewidth=1, color='k')
line2.set_label('risk_premium_bonds')
ax.legend(loc='upper left')
plt.xlim(left=date(1968, 1, 1), right=date(2020, 3, 9))

ax.axhline(linewidth=2, color='black', alpha=0.7)
ax.axvspan(date(2007, 12, 1), date(2009, 6, 1), alpha=0.3, color='grey')
ax.axvspan(date(2001, 3, 1), date(2001, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1990, 8, 1), date(1991, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1981, 7, 1), date(1982, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1980, 1, 1), date(1980, 7, 1), alpha=0.3, color='grey')
ax.axvspan(date(1973, 12, 1), date(1975, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1969, 12, 1), date(1970, 11, 1), alpha=0.3, color='grey')

ax.set_xlabel('')
ax.set_ylabel('  , %')
par1.set_ylabel('Log  S&P500')

plt.show()




As of March 9, 2020, despite a sharp and strong drop in the US stock market, stock returns are still far from the negative zone, which gives an encouraging signal.

Treasury yields spread


An alternative indicator of the pre-crisis market situation is the difference between the yields of long-term and short-term treasury bonds.

The least noisy and most practical is the spread between the yields of 10 year and 2 year treasury bonds:

Graph code
fig, ax = plt.subplots(figsize=(17,6))
par1 = ax.twinx()
line, = ax.plot(pd.DataFrame(T10Y2Y),linewidth=0.4)
line.set_label('10YB-2YB')
ax.legend(loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.1))

line1, = par1.plot(np.log(GSPC_h['Close']),linewidth=0.7, color='red')
line1.set_label('S&P500')
par1.legend(loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.18))
plt.xlim(left=date(1976, 1, 1), right=date(2020, 3, 9))
plt.ylim(bottom=4)
ax.axvspan(date(2007, 12, 1), date(2009, 6, 1), alpha=0.3, color='grey')
ax.axvspan(date(2001, 3, 1), date(2001, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1990, 8, 1), date(1991, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1981, 7, 1), date(1982, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1980, 1, 1), date(1980, 7, 1), alpha=0.3, color='grey')
ax.axvspan(date(1973, 12, 1), date(1975, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1969, 10, 1), date(1970, 11, 1), alpha=0.3, color='grey')
ax.axhline(linewidth=2, color='black', alpha=0.7)
plt.scatter(date(2019, 9, 1), 6., color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(2007, 1, 1), 6., color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(2000, 1, 1), 5.9, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1998, 8, 1), 5.9, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1989, 4, 1), 5.8, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1981, 1, 1), 5.9, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1978, 11, 1), 5.9, color='orange', s=500, marker='o', alpha=0.5)

ax.set_xlabel('')
ax.set_ylabel('  , %')
par1.set_ylabel('Log  S&P500')

plt.show()




This indicator is the most truthful from a historical point of view and predicted the last 7 financial crises.

The spread between bond yields in this case reflects the mood of investors - if they believe that the economy will soon deteriorate, they begin to transfer money from short-term bonds (with a maturity of 2 years) to long-term instruments (with a maturity of 10 years) with a fixed yield The most reliable issuer is the US Treasury.

The purchase of long-term bonds affects their yield, since the purchase of bonds for significant amounts of cash leads to an increase in the cost of the bond. When the coupon is a fixed value, the appreciation of the body of the bond leads to a decrease in its yield.
For short-term bonds, which are sold in this case, the situation is the opposite - the body begins to become cheaper, in a fixed coupon, respectively, the yield begins to grow.

However, FRED does not provide information further than 1976 on this spread, therefore, for illustrative purposes, we can take the yield on 10-year treasury bonds minus the FED rate (instead of short-term 2-year bonds) to see what has happened since 1962 and covered 2 more crises :

Graph code
fig, ax = plt.subplots(figsize=(17,6))
par1 = ax.twinx()
line, = ax.plot(pd.DataFrame(T10YFF),linewidth=0.4)
line.set_label('10-Year Treasury Constant Maturity Minus Federal Funds Rate')
ax.legend(loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.1))

line1, = par1.plot(np.log(GSPC_h['Close']),linewidth=0.7, color='red')
line1.set_label('S&P500')
par1.legend(loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.18))
plt.xlim(left=date(1962, 1, 1), right=date(2020, 3, 9))
plt.ylim(bottom=4)
ax.axvspan(date(2007, 12, 1), date(2009, 6, 1), alpha=0.3, color='grey')
ax.axvspan(date(2001, 3, 1), date(2001, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1990, 8, 1), date(1991, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1981, 7, 1), date(1982, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1980, 1, 1), date(1980, 7, 1), alpha=0.3, color='grey')
ax.axvspan(date(1973, 12, 1), date(1975, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1969, 12, 1), date(1970, 11, 1), alpha=0.3, color='grey')
ax.axhline(linewidth=2, color='black', alpha=0.7)
plt.scatter(date(2019, 9, 1), 6.7, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(2007, 1, 1), 6.7, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(2000, 11, 1), 6.7, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1998, 11, 1), 6.6, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1989, 6, 1), 6.4, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1973, 6, 1), 6.6, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1981, 3, 1), 6.3, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1979, 3, 1), 6.6, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1969, 3, 1), 6.6, color='orange', s=500, marker='o', alpha=0.5)
plt.scatter(date(1967, 1, 1), 6.6, color='orange', s=500, marker='o', alpha=0.5)

ax.set_xlabel('')
ax.set_ylabel('  , %')
par1.set_ylabel('Log  S&P500')

plt.show()




Despite the general noisiness of this spread (compare with the spread of 10 year - 2 year), the crises of 73 and 69 were also implemented by reducing the spread to the negative zone.

What at the moment?
Things are not very - the indicator already showed a negative zone in 2019.
Institutional investors expect that in the near future, serious shocks await us and because of this they are shifted to long-term fixed-income instruments.

How much is left before the crisis?
As can be seen from the spread chart, inverted yield anticipates crises for a year or two.
Given that the inversion occurred at the end of 2019 and the US markets have already begun to fall due to the expectation of a future recession due to the COVID-19 virus, the crisis is approaching right now.

As of March 9, 2020, the yield on 10-year US Treasury bonds fell to 0.318% - the lowest value ever!
It seems that something big is waiting for us and it has already begun.

current value T10YFF: -0.17
current value T10Y2Y: 0.25

Past prices do not predict crisis


For an example of the helplessness of technical analysis in this issue, we take the RSI indicator.
RSI - relative strength index in theory shows the "overbought" and "oversold" market.
Overbought market - this is the state when prices must adjust down, that is, a crisis in the stock market.

Link to the Wiki with a description of the indicator

We can calculate this indicator using Python, for calculation we take the period - 244 trading sessions (1 calendar year):

SP500_returns = GSPC_h['Close'].pct_change()
delta = GSPC_h['Close'].diff()
window_length = 500

# Make the positive gains (up) and negative gains (down) Series
up, down = delta.copy(), delta.copy()
up[up < 0] = 0
down[down > 0] = 0

# Calculate the EWMA
roll_up1 = up.ewm(span=window_length).mean()
roll_down1 = down.abs().ewm(span=window_length).mean()

# Calculate the RSI based on EWMA
RS1 = roll_up1 / roll_down1
RSI1 = 100.0 - (100.0 / (1.0 + RS1))

Graph code
# Compare graphically
fig, ax = plt.subplots(figsize=(20,4))
plt.xlim(left=date(1968, 1, 1), right=date(2020, 3, 9))
line, = ax.plot(np.log(GSPC_h['Close']),linewidth=0.7, color='red')

line.set_label('SNP500')
ax.legend(loc='upper left')

fig1, ax1 = plt.subplots(figsize=(20,4))
plt.xlim(left=date(1968, 1, 1), right=date(2020, 3, 9))
line1, = ax1.plot(RSI1[80:],linewidth=1)

line1.set_label('RSI (1Y)')
ax1.legend(loc='upper left')

ax.axvspan(date(2007, 12, 1), date(2009, 6, 1), alpha=0.3, color='grey')
ax.axvspan(date(2001, 3, 1), date(2001, 11, 1), alpha=0.3, color='grey')
ax1.axhline(y=57, linewidth=2, color='black', alpha=0.7)
ax1.axhline(y=45, linewidth=2, color='black', alpha=0.7)

ax.axvspan(date(1990, 8, 1), date(1991, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1981, 7, 1), date(1982, 11, 1), alpha=0.3, color='grey')
ax.axvspan(date(1980, 1, 1), date(1980, 7, 1), alpha=0.3, color='grey')
ax.axvspan(date(1973, 12, 1), date(1975, 2, 1), alpha=0.3, color='grey')
ax.axvspan(date(1969, 12, 1), date(1970, 11, 1), alpha=0.3, color='grey')

ax1.axvspan(date(1990, 8, 1), date(1991, 2, 1), alpha=0.3, color='grey')
ax1.axvspan(date(1981, 7, 1), date(1982, 11, 1), alpha=0.3, color='grey')
ax1.axvspan(date(1980, 1, 1), date(1980, 7, 1), alpha=0.3, color='grey')
ax1.axvspan(date(1973, 12, 1), date(1975, 2, 1), alpha=0.3, color='grey')
ax1.axvspan(date(1969, 12, 1), date(1970, 11, 1), alpha=0.3, color='grey')
ax1.axvspan(date(2007, 12, 1), date(2009, 6, 1), alpha=0.3, color='grey')
ax1.axvspan(date(2001, 3, 1), date(2001, 11, 1), alpha=0.3, color='grey')

ax1.set_xlabel('')
ax.set_xlabel('')
ax1.set_ylabel(' RSI')
ax.set_ylabel('Log  S&P500')

plt.show()




What do we see on the RSI chart?

Many zones of “overbought”, which could signal too much market, which it is time to adjust down. But it continued to go up (for example, a long period since 1995, the indicator shows “overbought”, but before the crisis of 2001 it returns to its usual zone and does not signal about “overbought”, which, however, ends with the crisis).

In other words, the use of oscillators to predict a crisis is a highly controversial exercise.

Before crises, the market grows smoothly, without showing high upward volatility, unlike falls - they are always sharp and swift. We see this just near the lower border of the oscillator - its intersection almost always showed exactly when the crisis came and the bottom of the fall was close. Buy signal?

findings


At the moment, the situation looks alarming; there are direct indicators showing a possible recession. In addition to the reviewed ones, there is also an industrial production index (it is leading for economic (not financial!) Crises). One could also delve into macroeconomic statistics and add the balance of payments, GDP and the like to the analysis, however, institutional investors have already done this for us - their reaction is reflected in the yield spread of long-term and short-term bonds. It remains to relax and watch the show, if you do not have financial investments. And if there is - think about a possible hedge of your positions or go to the cache until better times.

All Articles