Quant BuffetRelax, Not Over Thinking

Term Spread and Term Premium Predict US Government Bonds Returns

Log in to collect

Academic paper

Trading the Term Premium

AuthorsLuke van Schaik; Emlyn Flint; Florence Chikurunhe

Institute
  • ?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