Quant BuffetRelax, Not Over Thinking

Unemployment Gap 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

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

Annualised return1.3%
Volatility10.1%
Beta-0.002
Sharpe ratio0.13
Maximum drawdown-40%
Win rate53%

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()