Flight to Quality Factor in Fixed Income
Log in to collectAcademic paper
Beyond Carry and Momentum in Government Bonds
Jérôme Gava; William Lefebvre; Julien Turc
- É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
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