Quant BuffetRelax, Not Over Thinking

Flight to Quality Factor in Fixed Income

Log in to collect

Academic paper

Beyond Carry and Momentum in Government Bonds

AuthorsJérôme Gava; William Lefebvre; Julien Turc

Institute
  • École Polytechnique
  • BNP Paribas (France)
  • ?BNP Paribas
  • ?Ecole Polytechnique
  • ?Laboratoire de Probabilités, Statistique et Modélisation
  • ?Laboratoire de Probabilités, Statistique et Modélisation (LPSM)

Strategy in a nutshell

The strategy focuses on 10-year government bond futures from six countries: Australia, Canada, Germany, Japan, the UK, and the US. It constructs a factor based on equity volatility (three years, one year, or one month), where high volatility signals positive bond allocation and vice versa. Allocation can follow bottom, median, or top strategies, or a combined approach across versions. Factor portfolios are constructed by proportionally buying or selling bond futures, using cross-sectional scores derived from comparing indicators against the adjusted cross-sectional average. Directional bias is neutralized by equal-weighted allocation across all countries, adjusted daily. The strategy is rebalanced daily for precision.

Economic rationale

Defensive bond investing aligns with bonds' reputation as safe-haven assets, supported by research showing stock market volatility guides defensive bond strategies. When confidence drops, investors shift from equities to bonds, a phenomenon linked to flight-to-quality effects during equity market shocks. Conversely, bond market shocks impact both bonds and equities negatively. Equity market volatility is a useful indicator for bond investments, as it reflects investor sentiment and risk-aversion behaviors, making it a valuable tool for timing defensive bond strategies. This insight explains why equity market volatility can be effectively utilized in bond market decision-making.

Backtest performance

Annualised return2.8%
Volatility8.2%
Beta0.013
Sharpe ratio0.34
Sortino ratio-1.898
Maximum drawdown-15%
Win rate49%

Full Python code

import numpy as np
from AlgorithmImports import *
class FlighttoQualityFactor(QCAlgorithm):
def Initialize(self):
 self.SetStartDate(2000, 1, 1)
 self.SetCash(100000)
 
 self.symbols = {
                 "EWA" : "ASX_XT1",        # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
                 "EWC" : "MX_CGB1",        # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
                 "EWG" : "EUREX_FGBL1",    # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
                 "EWJ" : "SGX_JB1",        # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
                 "EWU" : "LIFFE_R1",       # Long Gilt Futures, Continuous Contract #1 (U.K.)
                 "SPY" : "CME_TY1"         # 10 Yr Note Futures, Continuous Contract #1 (USA)
                 }
 self.data = {}
 self.period = 12 * 21
 leverage: int = 2
 self.SetWarmUp(self.period)
 for symbol in self.symbols:
     bond = self.symbols[symbol]
     
     self.AddEquity(symbol, Resolution.Daily)
     self.data[symbol] = RollingWindow[float](self.period)
     data = self.AddData(QuantpediaFutures, bond, Resolution.Daily)
     data.set_leverage(leverage)
     data.SetFeeModel(CustomFeeModel())
     
def OnData(self, data):
 for symbol, bond in self.symbols.items():
     if self.securities[bond].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[bond]:
         self.liquidate(bond)
         self.data[symbol].reset()
         continue
     if symbol in data and data[symbol]:
         price = data[symbol].Value
         self.data[symbol].Add(price)
 if self.IsWarmingUp: return

 volatility = {}
 for symbol in self.symbols:
     if self.data[symbol].IsReady:
         prices = np.array([x for x in self.data[symbol]])
         returns = prices[:-1] / prices[1:] - 1
         volatility[symbol] = np.std(returns)
 
 if len(volatility) <= 1: return
 
 avg_volatility = np.mean([x[1] for x in volatility.items()])
 
 diff = {symbol : (volatility[symbol] - avg_volatility) for symbol in volatility}
 total_diff = sum([abs(x[1]) for x in diff.items()])
 
 for symbol in diff:
     bond = self.symbols[symbol]
     if data.contains_key(bond) and data[bond]:
         self.SetHoldings(bond, diff[symbol] / total_diff)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
 fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
 return OrderFee(CashAmount(fee, "USD"))
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
_last_update_date:Dict[Symbol, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[Symbol, datetime.date]:
return QuantpediaFutures._last_update_date
def GetSource(self, config, date, isLiveMode):
 return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
 data = QuantpediaFutures()
 data.Symbol = config.Symbol
 
 if not line[0].isdigit(): return None
 split = line.split(';')
 
 data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
 data['back_adjusted'] = float(split[1])
 data['spliced'] = float(split[2])
 data.Value = float(split[1])
 if config.Symbol.Value not in QuantpediaFutures._last_update_date:
     QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
 if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
     QuantpediaFutures._last_update_date[config.Symbol.Value] = data.Time.date()
 return data