The strategy uses unemployment gap factors to allocate 10-year bond futures across five countries, neutralizing directional bias with equal-weighted adjustments and rebalancing monthly for systematic investment.

I. 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.

II. 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.

III. SOURCE PAPER

Beyond Carry and Momentum in Government Bonds [Click to Open PDF]

Jerome Gava, BNP Paribas, Ecole Polytechnique, Laboratoire de Probabilités, Statistique et Modélisation (LPSM); William Lefebvre, BNP Paribas; Julien Turc, BNP Paribas, Ecole Polytechnique

<Abstract>

This article revisits recent literature on factor investing in government bonds, in particular regarding the definition of value and defensive investing. Using techniques derived from machine learning, the authors identify the key drivers of government bond futures and the groups of factors that are most genuinely relevant. Beyond carry and momentum, they propose an approach to defensive investing that considers the safe-haven nature of government bonds. These two main styles may be complemented by value and a reversal factor in order to achieve returns independently from broad movements in interest rates.

IV. BACKTEST PERFORMANCE

Annualised Return1.3%
Volatility10.1%
Beta-0.002
Sharpe Ratio0.13
Sortino RatioN/A
Maximum Drawdown-40%
Win Rate53%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading