We review the classical work of Campbell and Shiller [1] on excess returns from trading long-maturity bonds and funding it with a short-maturity bond. Subsequently we also look at the modern developments from the work of Cochrane and Piazzesi [2].
We define general expression for excess returns that involves taking long and short positions in two bonds of different maturities.
Let $P_t^k$ be the price of the $k$-maturity discount bound at time $t$.
At time $t=0$, we buy a $\frac{1}{P_0^N}$ units of $N$-maturity bond for \$1. To fund the \$1, we short $\frac{1}{P_0^n}$ units of $n$-maturity bond to get \$1.
So our portfolio at time $t=0$ is, $$ \mathbb{\Pi}_0(\tau, N, n) = \left(\frac{1}{P_0^N}\right)P_0^N -\left(\frac{1}{P_0^n}\right)P_0^n = 0 $$ where the quantities in the brackets are the notionals.
We then wait for a period of time $\tau$ to elapse, and at which time we unwind the positions. We have to sell the $N$-maturity bond we bought at time $t=0$ for price $P_{\tau}^{N-\tau}$, and buy back the short position for $P_{\tau}^{n-\tau}$. The time-$\tau$ portfolio will be worth,
$$ \mathbb{\Pi}_\tau(\tau, N, n) = \left(\frac{1}{P_0^N}\right)P_\tau^{N-\tau} -\left(\frac{1}{P_0^n}\right)P_\tau^{n-\tau} $$We therefore define the quantity $\mathbb{\Pi}_\tau-\mathbb{\Pi}_0 = \mathbb{\Pi}_\tau$ to be the $\tau$-period excess return from investing in the $N$-period bond and financing it with the $n$-period bond,
$$ \textit{xret}(\tau, N, n) =\mathbb{\Pi}_\tau (\tau, N, n) $$In this case we short a one year bond, use it to finance a $N$-maturity bond, and unwind it after 1 year. The excess return is,
$$ \begin{aligned} \textit{xret}(1, N, 1) &= \left(\frac{1}{P_0^N}\right)P_1^{N-1} -\left(\frac{1}{P_0^n}\right)P_1^0 = \left(\frac{1}{P_0^N}\right)P_1^{N-1} -\left(\frac{1}{P_0^n}\right)\\ &=\textit{xret}_{t+1} \end{aligned} $$We know that by definition, $P_t^T = e^{-y_t^T (T-t)} \approx 1 - y_t^T(T-t) + \ldots$, using this, we have,
$$ \begin{aligned} \textit{xret}_{t+1} &= \left(\frac{1}{P_0^N}\right)P_1^{N-1} -\left(\frac{1}{P_0^n}\right)\\ &=(1+ y_0^N(N-0))(1 - y_1^{N-1} ( N-1)) - (1+y_0^1 ( 1-0))\\ &=(1+ y_0^NN)(1 - y_1^{N-1} ( N-1)) - (1+y_0^1)\\ &=y_0^NN - y_1^{N-1} ( N-1) -y_0^1 \qquad \text{(retaining 1st order terms)} \end{aligned} $$import datetime
import quandl
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import numpy as np
from numpy.linalg import eig
from scipy.stats import linregress
from statsmodels.formula.api import ols
import warnings
from IPython.core.display import HTML, display
warnings.filterwarnings('ignore')
%matplotlib inline
plt.style.use(('bmh'))
# data = quandl.get("FED/SVENY") # get from quandl if we don't have a local copy
dataLoc = './FED-SVENY.csv'
data = pd.DataFrame.from_csv(dataLoc)
data.sort_index(inplace=True)
# find the index onwards from which we have data for all tenors
idx_mat = np.isnan(data).apply( np.nonzero, axis=0).apply(np.ravel)
first_idx = np.max( [ np.max(a) for a in idx_mat.values if len(a) > 0 ])
data_subset = data[first_idx+1:]
data_subset = data_subset.applymap( lambda x : x * 0.01 ) # convert from percentage to decimals
data_subset = data_subset.resample('M').copy() # down sample to monthly frequency
fig = plt.figure( figsize = ( 12, 6))
for n, col in enumerate( data_subset.columns ):
if n % 3 == 0:
# plot last 200 points
plt.plot( data_subset.index[-200:], data_subset[col][-200:], lw=2. )
We calculate the excess returns from long a 10-yr bond while funding it with a 1-yr bond, then unwinding the positions after 1yr.
# compute the excess returns. first we truncate the data and drop the last 1-yr (365days)
# since we won't be able to calculate returns from them.
data_1_10 = data_subset[['SVENY01','SVENY09','SVENY10']]
def excess_returns( data_set, N ):
xrets = []
time_points = []
for ts, series in data_set.iterrows():
#find the yield of the 9-yr bond, one yr from current date
one_yr = ts.date() + datetime.timedelta(days=365)
idx = data_set.index.searchsorted( one_yr )
if idx == len( data_set ):
break
if data_set.index[idx] == one_yr:
next_yr_bond = data_set.SVENY09.iloc[idx]
else:
next_yr_bond = data_set.SVENY09.iloc[idx-1]
xrets.append( series.SVENY10 * N - next_yr_bond * (N-1) - series.SVENY01 )
time_points.append( ts.date() )
return ( np.array(xrets), time_points )
ret, xs = excess_returns( data_1_10, 10 )
fig = plt.figure( figsize = ( 12, 6 ) )
plt.plot( xs, ret, lw='2.0')
plt.title('Excess returns from long 10-y and short 1-yr bond strategy. Sharpe ratio = %s'
% (ret.mean()/ret.std(), ) )
plt.show()
We now test the claim that the slope of the yield curve predicts excess returns. In the work of Campbell and Shiller (1991) [1], they regressed the differences between the time-$(t+1)$, $(n-1)$-maturity yield and the time-$t$, $n$-maturity yield (the right-hand variable) against the time-$t$ differences between the $n$-maturity yield and the funding rate, $y_t^1$, (the right-hand variables):
$$ y_{t+1}^{n-1} - y_t^n= \alpha_n + \beta_n \frac{y_t^n - y_t^1}{n-1}\qquad \text{(*)} $$What's the intuition behind the above?
Recall that previously we defined the excess return as,
$$ \begin{aligned} \textit{xret}_{t+1}&=y_0^NN - y_1^{N-1} ( N-1) -y_0^1 \\ &= \underbrace{(y_0^N - y_1^{N-1})}_{\text{roll-down term}} ( N-1) + \underbrace{y_0^N-y_0^1}_{\text{slope or carry}} \end{aligned} $$The above expression after rearrangement looks very similar to that in $(*)$. Suppose if we regress the carry ($\frac{y_0^N-y_0^1}{N-1}$) against the roll-down $y_0^N - y_1^{N-1}$ and if we find a relationship between the two, then we can use it to predict the excess return at any point in time.
As long as the yield curve is upward sloping, we get a boost from the positive carry term. The other component of the excess returns, the roll-down, it dependent on the behavior of the $(n − 1)$-year yield at time-$(t + 1)$ : $y_{t+1}^{n-1}$.
We shall see that indeed there is a relationship which is of statistical significance.
LHS, RHS = [], []
N=10
for ts, series in data_1_10.iterrows():
#find the yield of the 9-yr bond, one yr from current date
one_yr = ts.date() + datetime.timedelta(days=365)
idx = data_1_10.index.searchsorted( one_yr )
if idx == len( data_1_10):
break
if data_1_10.index[idx] == one_yr:
next_yr_bond = data_1_10.SVENY09.iloc[idx]
else:
next_yr_bond = data_1_10.SVENY09.iloc[idx-1]
LHS.append( next_yr_bond - series.SVENY10 )
RHS.append( ( series.SVENY10 - series.SVENY01 ) / ( N - 1 ) )
fig = plt.figure( figsize = ( 12, 6 ) )
plt.scatter(RHS, LHS, color='#EC7063', edgecolors='black')
plt.show()
res = linregress( RHS, LHS )
regression_data = pd.DataFrame( {'y':RHS, 'x':LHS} )
model = ols( 'y ~ x', regression_data ).fit()
model.summary()
As we can see from the above, the relationship is statistically significant.
We now explore the formulation of excess return in terms of forward rates.
We work in time intervals of 1-yr and consider the construction of the following portfolio.
At time $t=0$,
So the initila value of our portfolio is $0$.
At time $t=N$,
At time $t=N+1$,
So what we have done is that we have synthesized a contract today for borrowing $\$1$ at $t=N$ and paying back at $t=N+1$. This is essentially, a forward rate.
Let us now define log-prices and forward rates,
By convention, we use the forward rate definition whereby we borrow at time-$t+n-1$ and pay back at time-$t+n$,
$$ f_t^n = p_t^{t+n-1} - p_t^{t+n} $$Assume $p_t^{t+0}=0$, then we can write the following,
$$ \begin{aligned} f_t^1 &= p_t^{t+0} - p_t^{t+1}\\ &= 0 - p_t^{t+1}\\ f_t^2 &= p_t^{t+1} - p_t^{t+2}\\ \ldots\\ f_t^n &= p_t^{t+n-1} - p_t^{t+n} \end{aligned} $$Clearly, from the above we see that we have a telescopic sum. Adding up $f_t^1, f_t^2, \ldots f_t^n$, we get,
$$ p_t^{t+n} = -\left(\sum_{i=1}^n f_t^i \right) $$Given this, we can express the excess returns formula as,
$$ \begin{aligned} \textit{xret}_{t+1} &= y_t^nn - y_{t+1}^{n-1} ( n-1) -y_t^1 \\ &=p_{t+1}^{n-1} - p_t^n -y_t^1\\ &=\sum_{i=1}^{n} f_t^i - \sum_{i=1}^{n-1} f_{t+1 }^i + p_t^1\\ &=\sum_{i=2}^{n} f_t^i - \sum_{i=2}^{n} f_{t+1 }^{i-1} \qquad \text{(adjusting the index in 2nd sum and subtract $p_t^1$ from the 1st)} \\ &=\sum_{i=2}^{n} \left(f_t^i - f_{t+1 }^{i-1} \right) \end{aligned} $$This expression shows that the excess returns reaped by engaging in the 'carry' trade with the $n$-maturity bond can be expressed as the sum of $n-1$ terms. Each term is the difference between today's forward rate for maturity $i$ $\left(f_t^i\right)$ and the forward rate in one year's time with maturity decreased by 1 yr $\left(f_{t+1}^{i-1}\right)$.
In their work in [2], Cochrane and Piazzesi found that five forward rates predict excess returns with $R^2$ as high as $44\%$.
They define,
which we derived above.
They then regressed the excess return against the five forward rates : $f_t^{(1)}=y_t^{(1)}$, $f_t^{(2)}$, $f_t^{(3)}$, $f_t^{(4)}$, $f_t^{(5)}$.
We validate their claims below using a Python implementation and investigate how the excess returns look like as well as the shape of the return predicting factors.
Note: In their analysis, Cochrane and Piazzesi used CRSP data from Jan-1964 to Dec-2003.
st = datetime.date(1964, 1, 1)
et = datetime.date(2003, 12, 31)
CP_data = pd.DataFrame.from_csv(dataLoc)
CP_data.sort_index(inplace=True)
CP_data = CP_data.ix[st:et]
CP_data = CP_data.applymap( lambda x : x * 0.01 ) # convert from percentage to decimals
CP_data = CP_data.resample('M').copy() # down sample to monthly frequency
def compute_forward_rates( df ):
# compute the new columns for the forward rates
df['p0'] = 0.0 # for convenience later when computing the forward rates.
for n in xrange(1,6):
ns = str(n)
colname = 'SVENY0' + ns
df['p' + ns] = - df[colname] * float(n) # this is the log-price p(n)
for n in xrange(1,6):
ns = str(n)
nm1s = str(n-1)
df['f' + ns] = df[ 'p' + nm1s] - df[ 'p' + ns ]
compute_forward_rates( CP_data )
def excess_returns( df ):
xrets = {}
time_points = []
holding_period = 365
for n, (ts, series) in enumerate(df.iterrows()):
one_yr = ts.date() + datetime.timedelta( days = holding_period )
idx = df.index.searchsorted( one_yr )
if idx == len( df ):
break
for n in xrange(2, 6):
ns, nm1s = str(n), str(n - 1)
if df.index[idx] == one_yr:
next_yr_bond = df[ 'p' + nm1s ].iloc[ idx ]
else:
next_yr_bond = df[ 'p' + nm1s ].iloc[ idx - 1 ]
xrets.setdefault( n, [] ).append( next_yr_bond - series[ 'p' + ns ] - series['f1'] )
time_points.append( ts.date() )
return xrets, time_points
ret, xs = excess_returns( CP_data )
fig = plt.figure( figsize = ( 12, 20 ) )
for i in range(1, 5):
cur_rets = np.array( ret[i+1] )
sr = cur_rets.mean() / cur_rets.std()
plt.subplot(410 + i)
plt.plot( xs, ret[i+1], lw='2.0')
plt.title( 'Excess returns from investing in the %s-yr bond. Sharpe ratio = %s' % ( i+1, sr ) )
def make_regression_data( df, ret, timepts ):
d = dict( (x, df[x][timepts]) for x in ['f1', 'f2', 'f3', 'f4','f5'] )
for x in range( 2, 6 ):
d['rx' + str(x)] = ret[x]
return pd.DataFrame(d, index=xs)
regression_data = make_regression_data( CP_data, ret, xs )
regression_data.head()
models = {}
for x in range( 2, 6 ):
models[x] = ols( 'rx%s ~ f1 + f2 + f3 + f4 + f5' % x, regression_data ).fit()
models[2].summary()
Note the large value of 643 for the condition number above. This shows that there is multicollinearity in the data.
Now we show the results of multiple linear regression for the excess returns on 2, 3, 4, 5 yr bonds.
fig = plt.figure( figsize = ( 8, 6 ) )
for x in range( 2, 6 ):
plt.plot(range(1,6), models[x].params.values[1:], lw=2, label="%s-yr" % x )
plt.title('Regression coefficients for excess returns.')
plt.legend()
plt.show()
Rather surprisingly, we didn't get the tent shape as described by Cochrane and Piazzesi!
Instead, we have a 'bat' shaped diagram for the regression coefficients for the five forward rates. However, this is also not unexpected. It has been chronicled in Dai and Singleton [3] that slight differences in yield curve interpolation methods will produce very different shapes for the regression coefficients. In fact, we reproduced exactly the "bat" shape as described in their work in [3].
The reason for this is due to the multicollinearity issue in the forward rates used for the multiple linear regression. See [3] and [4] for a detailed discussion.
We now retry the above using CRSP data as that in Cochrane and Piazzesi.
CRSP_data_loc = 'CRSP_yield.csv'
CRSP_data = pd.DataFrame.from_csv(CRSP_data_loc)
CRSP_data.sort_index(inplace=True)
CRSP_data = CRSP_data.ix[st:et]
CRSP_data = CRSP_data.applymap( lambda x : x * 0.01 ) # convert from percentage to decimals
CRSP_data.head()
compute_forward_rates( CRSP_data )
ret, xs = excess_returns( CRSP_data )
fig = plt.figure( figsize = ( 12, 20 ) )
for i in range(1, 5):
cur_rets = np.array( ret[i+1] )
sr = cur_rets.mean() / cur_rets.std()
plt.subplot(410 + i)
plt.plot( xs, ret[i+1], lw='2.0')
plt.title( 'Excess returns from investing in the %s-yr bond. Sharpe ratio = %s' % ( i+1, sr ) )
regression_data = make_regression_data( CRSP_data, ret, xs )
display( HTML( regression_data.head().to_html() ) )
models = {}
for x in range( 2, 6 ):
models[x] = ols( 'rx%s ~ f1 + f2 + f3 + f4 + f5' % x, regression_data ).fit()
models[2].summary()
fig = plt.figure( figsize = ( 8, 6 ) )
for x in range( 2, 6 ):
plt.plot(range(1,6), models[x].params.values[1:], lw=2, label="%s-yr" % x )
plt.title('Regression coefficients for excess returns.')
plt.legend()
plt.show()
So indeed we have reproduced the 'tent' shape as in Cochrane and Piazzesi. This shows how sensitive the regression betas are due to the multicolinearity problem and the issue of the smoothing of the yield curve. In Cochrane and Paizzesi the yield curve data is unsmoothed, while the quandl SVENY data set uses the Nelson-Sigel smoothed yield curve construction.
[1] Campbell J, Shiller, R, (1991), Yield Spreads and Interest Rate Movements: A Bird’s Eye View, Review of Economic Studies, 58, 495-514.
[2] Cochrane J H, Piazzesi M, (2005), Bond Risk Premia, American Economic Review, Vol 95, no 1, 138-160 — see also https://www.aeaweb.org/aer/data/mar05_app_cochrane.pdf (last accessed 25 November, 2014) for technical details, and the regression coefficients.
[3] Dai, Q, Singhleton K J, YAng W, (2004), Predictability of Bond Risk Premia and Affine Term Structure Models, working paper, Stern School of Business.
[4] Rebonato Riccardo, Return-predicting factors for us treasuries : on the similarity of “tents” and “bats”, International Journal of Theoretical and Applied Finance.- River Edge, NJ [u.a.] : World Scientific, ISSN 0219-0249, ZDB-ID 14289829. - Vol. 18.2015, 4, p. 1-14