/Inverted_Yield_Curves

An inverse yield structure exists when long-term interest rates on the capital market are lower than short-term interest rates. Normally, the exact opposite is the case. Inverse yield curves rarely occur and are considered a quite solid signal for an upcoming economic recession.

Primary LanguageJupyter Notebook

Inverted_Yield_Curves

An inverse yield structure exists when long-term interest rates on the capital market are lower than short-term interest rates. Normally, the exact opposite is the case. Inverse yield curves rarely occur and are considered a quite solid signal for an upcoming economic recession.

Import libraries

import pandas as pd
import numpy as np
from matplotlib.dates import MonthLocator, YearLocator
import matplotlib.pyplot as plt
from datetime import date
from dateutil.relativedelta import relativedelta
import requests
from bs4 import BeautifulSoup
import datetime
import matplotlib.dates as mdates
from matplotlib.patches import Rectangle

Download Yield data

We obtain the Treasury Par Yield Curve Rates from the official site of the U.S. DEPARTMENT OF THE TREASURY

%%time
yield_ = pd.DataFrame(columns=['1Mo','2Mo','3Mo','6Mo','1Yr','2Yr','3Yr','5Yr','7Yr','10Yr','20Yr','30Yr'])
time_range = range(1990,2023,1)
base_url = "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/TextView?type=daily_treasury_yield_curve&field_tdr_date_value="

for i,year in enumerate(time_range):
    url = base_url + str(year)
    data = requests.get(url).text
    soup = BeautifulSoup(data, 'html.parser')
    table = soup.find('table')
    df_soup = pd.read_html(str(table))[0]
    # rearrange 
    df_soup = df_soup[['1 Mo','2 Mo','3 Mo','6 Mo','1 Yr','2 Yr','3 Yr','5 Yr','7 Yr','10 Yr','20 Yr','30 Yr']]
    df_soup.columns = ['1Mo','2Mo','3Mo','6Mo','1Yr','2Yr','3Yr','5Yr','7Yr','10Yr','20Yr','30Yr']
    df_soup.index = pd.to_datetime(pd.read_html(str(table))[0]['Date'])
    # append and delete duplicates
    yield_ = yield_.append(df_soup, ignore_index=False)
yield_.to_pickle("yield_data.pkl")
yield_.to_csv("yield_data.csv")
1Mo 2Mo 3Mo 6Mo 1Yr 2Yr 3Yr 5Yr 7Yr 10Yr 20Yr 30Yr
1990-01-02 nan nan 7.83 7.89 7.81 7.87 7.9 7.87 7.98 7.94 nan 8
1990-01-03 nan nan 7.89 7.94 7.85 7.94 7.96 7.92 8.04 7.99 nan 8.04
1990-01-04 nan nan 7.84 7.9 7.82 7.92 7.93 7.91 8.02 7.98 nan 8.04
-- -- -- -- -- -- -- -- -- -- -- -- --
-- -- -- -- -- -- -- -- -- -- -- -- --
2022-04-06 0.21 0.44 0.67 1.15 1.79 2.5 2.67 2.7 2.69 2.61 2.81 2.63
2022-04-07 0.21 0.5 0.68 1.15 1.78 2.47 2.66 2.7 2.73 2.66 2.87 2.69
2022-04-08 0.2 0.49 0.7 1.19 1.81 2.53 2.73 2.76 2.79 2.72 2.94 2.76

Calculation - Proportion how many yield curves are inverted?

def highlight_negative_values(cell):
    if type(cell) != str and cell < 0 :
        return 'color: red'
    else:
        return 'color: black'

def time_ft(time):
    return str(time.strftime("%Y%m%d"))

def calculate_matrix():
    df = pd.DataFrame(np.zeros((len(yield_.columns),len(yield_.columns))))
    df.index = considered_yield_data.index
    df.columns = df.index 
    df[:] = np.nan
    for hor in range(0,len(df.index)):
        for ver in range(0,len(df.index)):
            if hor>ver:
                df.iloc[hor,ver] = considered_yield_data.iloc[hor]-considered_yield_data.iloc[ver]
    available_entries = (len(df))**2 - df.isna().sum().sum()
    negative_entries = np.sum((df < 0).values.ravel())
    inverted_percentage = round(negative_entries/available_entries*100,1)
    return inverted_percentage, df.style.applymap(highlight_negative_values)

def add_patch(startdate,enddate):
    start = mdates.date2num(startdate)
    end = mdates.date2num(enddate)
    width = end - start
    range_in_days = (enddate-startdate).days
    rect = Rectangle((start, ax.get_ylim()[0]), width, ax.get_ylim()[1] - ax.get_ylim()[0], color='yellow',alpha = 0.15)
    ax.add_patch(rect)

One specific date

yield_ = pd.read_pickle("yield_data.pkl")
considered_date = "2022-04-08"
considered_yield_data = yield_.loc[considered_date]
print("Proportion how many yield curves are inverted: " + str(calculate_matrix()[0]) + "%")
calculate_matrix()[1]
1Mo 2Mo 3Mo 6Mo 1Yr 2Yr 3Yr 5Yr 7Yr 10Yr 20Yr 30Yr
1Mo nan nan nan nan nan nan nan nan nan nan nan nan
2Mo 0.29 nan nan nan nan nan nan nan nan nan nan nan
3Mo 0.5 0.21 nan nan nan nan nan nan nan nan nan nan
6Mo 0.99 0.7 0.49 nan nan nan nan nan nan nan nan nan
1Yr 1.61 1.32 1.11 0.62 nan nan nan nan nan nan nan nan
2Yr 2.33 2.04 1.83 1.34 0.72 nan nan nan nan nan nan nan
3Yr 2.53 2.24 2.03 1.54 0.92 0.2 nan nan nan nan nan nan
5Yr 2.56 2.27 2.06 1.57 0.95 0.23 0.03 nan nan nan nan nan
7Yr 2.59 2.3 2.09 1.6 0.98 0.26 0.06 0.03 nan nan nan nan
10Yr 2.52 2.23 2.02 1.53 0.91 0.19 -0.01 -0.04 -0.07 nan nan nan
20Yr 2.74 2.45 2.24 1.75 1.13 0.41 0.21 0.18 0.15 0.22 nan nan
30Yr 2.56 2.27 2.06 1.57 0.95 0.23 0.03 0 -0.03 0.04 -0.18 nan

Time period

%%time
yield_ = pd.read_pickle("yield_data.pkl")
inverted_percentage = [] 
for u in range(0,(yield_.shape[0])):
    considered_yield_data = yield_.iloc[u]
    inverted_percentage.append(calculate_matrix()[0])
yield_["inverted_percentage"] = inverted_percentage
yield_
1Mo 2Mo 3Mo 6Mo 1Yr 2Yr 3Yr 5Yr 7Yr 10Yr 20Yr 30Yr inverted_percentage
1990-01-02 nan nan 7.83 7.89 7.81 7.87 7.9 7.87 7.98 7.94 nan 8 16.7
1990-01-03 nan nan 7.89 7.94 7.85 7.94 7.96 7.92 8.04 7.99 nan 8.04 16.7
1990-01-04 nan nan 7.84 7.9 7.82 7.92 7.93 7.91 8.02 7.98 nan 8.04 13.9
-- -- -- -- -- -- -- -- -- -- -- -- -- --
-- -- -- -- -- -- -- -- -- -- -- -- -- --
2022-04-06 0.21 0.44 0.67 1.15 1.79 2.5 2.67 2.7 2.69 2.61 2.81 2.63 12.1
2022-04-07 0.21 0.5 0.68 1.15 1.78 2.47 2.66 2.7 2.73 2.66 2.87 2.69 7.6
2022-04-08 0.2 0.49 0.7 1.19 1.81 2.53 2.73 2.76 2.79 2.72 2.94 2.76 7.6

Recessions

When did economic recessions take place

### https://en.wikipedia.org/wiki/List_of_recessions_in_the_United_States
from datetime import date
r1_start,r1_end = date(1990,7,1), date(1991,3,1)
r2_start,r2_end = date(2001,3,1), date(2001,11,1)
r3_start,r3_end = date(2007,12,1), date(2009,6,1)
r4_start,r4_end = date(2020,2,1), date(2020,4,1)

recessions = [[r1_start,r1_end],
              [r2_start,r2_end],
              [r3_start,r3_end],
              [r4_start,r4_end]]
fig,ax = plt.subplots(figsize=(35,10))
ax.plot(yield_.index, yield_["inverted_percentage"], label='Amount of inverted yield curves', linewidth=1.5, color = "blue")
ax.axhline(y=50, linewidth = 1.5, linestyle='--', color = "blue")
ax.set_ylim(-5,100)

# add recessions as a rectangle inside the figure
for i,_ in enumerate(recessions):
    add_patch(recessions[i][0],recessions[i][1])
    
# Annotation - current value
ax.annotate('%0.2f' % yield_["inverted_percentage"].iloc[-1], xy=(1, yield_["inverted_percentage"].iloc[-1]), xytext=(-80, 0), 
                     xycoords=('axes fraction', 'data'), textcoords='offset points')


## critical area
treshold = 50
ax.fill_between(yield_.index, yield_["inverted_percentage"], treshold,
                      where=(treshold < yield_["inverted_percentage"]),
                      facecolor='orange', edgecolor='orange', alpha=1)

legend = ax.legend(loc='upper right')

mloc = MonthLocator()
ax.xaxis.set_major_locator(mloc)
ax.grid(True,linewidth=2,alpha = 0.3)

# Labeling
plt.xticks(fontsize=4, rotation=90)
ax.set_title('Amount of inverse yield curves over time')
plt.ylabel('Percentage')
plt.xlabel("Date")

# save figure
pdf_name = "Yield.pdf"
plt.savefig(pdf_name)

Screenshot

Banks Tightening Standards for Commercial and Industrial Loans

Net Percentage of Domestic Banks Tightening Standards for Commercial and Industrial LoansNet Percentage of Domestic Banks Tightening Standards for Commercial and Industrial Loans

Number of regional banks in the U.S. that stop lending (Thightening Standards). If a bank fears bad times are coming, then it will lend less. If banks hold back to the maximum, then there will potentially a crash

# https://fred.stlouisfed.org/series/DRTSCILM
Tightening_Standards_csv = r"****/DRTSCILM.csv"
Tightening_Standards_df = pd.read_csv(Tightening_Standards_csv, sep=',')

Tightening_Standards_df.index = pd.to_datetime(Tightening_Standards_df["DATE"])
Tightening_Standards_df.drop(['DATE'], axis=1,inplace = True)
fig,ax = plt.subplots(figsize=(35,10))
ax.plot(Tightening_Standards_df.index, Tightening_Standards_df["DRTSCILM"], label='Tightening_Standards', linewidth=1.5)
ax.axhline(y=0, color='k', linewidth = 1.2, linestyle='--')
for i,_ in enumerate(recessions):
    add_patch(recessions[i][0],recessions[i][1])
    
plt.ylabel('Percentage')
plt.xlabel("Date")
legend = ax.legend(loc='upper right')

Screenshot

Combination of Yields and Banks Tightening Standards for Commercial and Industrial Loans

yield_part = pd.read_pickle("yield_data.pkl")

fig,ax = plt.subplots(figsize=(35,10))
ax.plot(yield_.index, yield_["inverted_percentage"], label='Amount of inverted yield curves', linewidth=1.5, color = "blue")
ax.axhline(y=50, linewidth = 1.5, linestyle='--', color = "blue")
ax.set_ylim(-30,100)

# add recessions as a rectangle inside the figure
for i,_ in enumerate(recessions):
    add_patch(recessions[i][0],recessions[i][1])
    
# Annotation - current value
ax.annotate('%0.2f' % yield_["inverted_percentage"].iloc[-1], xy=(1, yield_["inverted_percentage"].iloc[-1]), xytext=(-80, 0), 
                     xycoords=('axes fraction', 'data'), textcoords='offset points')

ax.plot(Tightening_Standards_df.index, Tightening_Standards_df["DRTSCILM"], label='Tightening_Standards', linewidth=1.5,color = "green")
ax.axhline(y=0, linewidth = 1.5, linestyle='--',color = "green")
## critical area
treshold = 50
ax.fill_between(yield_.index, yield_["inverted_percentage"], treshold,
                      where=(treshold < yield_["inverted_percentage"]),
                      facecolor='orange', edgecolor='orange', alpha=1)

legend = ax.legend(loc='upper right')

mloc = MonthLocator()
ax.xaxis.set_major_locator(mloc)
ax.grid(True,linewidth=2,alpha = 0.3)

# Labeling
plt.xticks(fontsize=4, rotation=90)
ax.set_title('Amount of inverse yield curves over time')
plt.ylabel('Percentage')
plt.xlabel("Date")

# save figure
pdf_name = "Yield.pdf"
plt.savefig(pdf_name)

Screenshot