Term Spread and Term Premium Predict US Government Bonds Returns
Log in to collectAcademic paper
Luke van Schaik; Emlyn Flint; Florence Chikurunhe
- ?Peresec
- ZAUniversity of Cape Town
- Peregrine Power (United States)
- ?Peregrine Securities
Strategy in a nutshell
Monthly bond strategy on US Treasuries: invest in 10-year T-bonds when term premium is below its 40-month moving average; otherwise, invest in 1-month T-bills. Fully weighted; rebalanced monthly.
Economic rationale
The term premium predicts regimes with higher risk-adjusted returns. Using moving-average signals, the strategy times duration risk effectively, improving returns relative to constant-duration exposure in US and SA markets.
Backtest performance
Annualised return8.08%
Volatility7.45%
Beta-0.053
Sharpe ratio1.08
Sortino ratio-0.067
Maximum drawdown-17.8%
Win rate45%
Full Python code
from AlgorithmImports import *
from pandas.core.frame import DataFrame
# endregion
class TermSpreadandTermPremiumPredictUSGovernmentBondsReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2007, 6, 1) # BIL inception
self.SetCash(100000)
self.long_duration_bond:Symbol = self.AddEquity('IEF', Resolution.Daily).Symbol
self.short_duration_bond:Symbol = self.AddEquity('BIL', Resolution.Daily).Symbol
self.term_spread:Symbol = self.AddData(TermSpread, 'T10Y3M', Resolution.Daily).Symbol
self.period:int = 40 * 21
self.rebalance_flag:bool = False
self.term_spread_sma = self.SMA(self.term_spread, self.period, Resolution.Daily)
self.SetWarmup(self.period, Resolution.Daily)
self.recent_month:int = -1
def OnData(self, data: Slice) -> None:
if self.IsWarmingUp:
return
t10y3m_last_update_date:datetime.date = TermSpread._last_update_date
# check if custom data is still arriving
if self.Securities[self.term_spread].GetLastData() and self.Time.date() >= t10y3m_last_update_date:
self.Liquidate()
return
# rebalance monthly
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
# compare latest value with 40-month moving average
traded_asset:Symbol|None = None
risk_flag:bool = False
if self.term_spread in data and data[self.term_spread]:
if data[self.term_spread].Price < self.term_spread_sma.Current.Value:
traded_asset = self.long_duration_bond
else:
traded_asset = self.short_duration_bond
# trade execution
if all(x in data and data[x] for x in [self.long_duration_bond, self.short_duration_bond]):
if not self.Portfolio[traded_asset].Invested:
self.Liquidate()
self.SetHoldings(traded_asset, 1)
# Source: https://fred.stlouisfed.org/series/T10Y3M
class TermSpread(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource('data.quantpedia.com/backtesting_data/economic/T10Y3M.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
_last_update_date:datetime.date = datetime(1,1,1).date()
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return TermSpread._last_update_date
def Reader(self, config, line, date, isLiveMode):
data = TermSpread()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split = line.split(';')
# Parse the CSV file's columns into the custom data class
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
if split[1] != '.':
data.Value = float(split[1])
if data.Time.date() > TermSpread._last_update_date:
TermSpread._last_update_date = data.Time.date()
return data