Unemployment Gap 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
Invests in 10-year government bond futures (Australia, Canada, Germany, UK, US) using an unemployment gap factor (1-month, 9-month, or 3-year). Allocation can follow bottom, median, or top approaches, or single-factor. Portfolios buy/sell proportionally based on cross-sectional scores, with equal-weighted country allocations to neutralize directional bias. Rebalanced monthly, leveraging unemployment gap trends for systematic bond decisions.
Economic rationale
Unemployment influences bonds as central banks adjust rates to control output and unemployment. Long-term rates reflect debt/GDP; unemployment indirectly ties to GDP. While no direct theory links the unemployment gap to bond futures, statistical and machine learning evidence shows it predicts returns reliably. It outperforms raw unemployment, yielding higher information ratios, making it effective for bond strategies.
Backtest performance
Full Python code
import numpy as np
from AlgorithmImports import *
import data_tools
class UnemploymentGapFactorinFixedIncome(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = {
"ASX_XT1" : "RBA/H05_GLFSURSA", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
"MX_CGB1" : "UKONS/ZXDZ_M", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
"EUREX_FGBL1" : "UKONS/ZXDK_M", # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
"LIFFE_R1" : "UKONS/YCNO_M", # Long Gilt Futures, Continuous Contract #1 (U.K.)
"CME_TY1" : "UKONS/ZXDX_M" # 10 Yr Note Futures, Continuous Contract #1 (USA)
}
# Monthly unemployment data.
self.data = {}
self.period = 3 * 12
for symbol in self.symbols:
data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(5)
unempl_symbol = self.symbols[symbol]
if unempl_symbol == 'ASX_XT1':
data = self.AddData(data_tools.UnemploymentDataAUD, unempl_symbol, Resolution.Daily)
else:
data = self.AddData(data_tools.UnemploymentData, unempl_symbol, Resolution.Daily)
self.data[symbol] = RollingWindow[float](self.period)
first_key = [x for x in self.symbols.keys()][0]
self.Schedule.On(self.DateRules.MonthStart(self.symbols[first_key]), self.TimeRules.At(0, 0), self.Rebalance)
def OnData(self, data):
# store monthly rates
for symbol in self.symbols:
unempl_symbol = self.symbols[symbol]
if unempl_symbol in data and data[unempl_symbol]:
unempl_rate = data[unempl_symbol].Value
if unempl_rate != 0:
self.data[symbol].Add(unempl_rate)
def Rebalance(self):
# Difference from MA.
ma_diff = {}
for symbol in self.symbols:
if self.Securities[symbol].GetLastData() and (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days < 5:
# Unemployment data is ready to calculate MA.
if self.data[symbol].IsReady:
# Calculate difference from MA.
ma = np.mean([x for x in self.data[symbol]])
if ma != 0:
current_value = self.data[symbol][0]
ma_diff[symbol] = current_value - ma
if len(ma_diff) != 0:
# Difference weighting.
avg_diff = np.mean([x[1] for x in ma_diff.items()])
diff_from_avg = { symbol: diff - avg_diff for symbol, diff in ma_diff.items() }
total_diff = sum([abs(x[1]) for x in diff_from_avg.items()])
weight = { symbol: diff / total_diff for symbol, diff in diff_from_avg.items() }
for symbol, w in weight.items():
self.SetHoldings(symbol, w)
else:
self.Liquidate()