The strategy trades 10-year bond futures from six countries, using equity volatility-based factors to allocate positions, neutralizing directional bias with equal-weighted daily adjustments and rebalancing for precision.

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

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

III. SOURCE PAPER

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

Jérôme Gava, William Lefebvre, and Julien Turc — BNP Paribas; École Polytechnique; Laboratoire de Probabilités, Statistique et Modélisation (LPSM)

<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 Return2.8%
Volatility8.2%
Beta0.013
Sharpe Ratio0.34
Sortino Ratio-1.898
Maximum Drawdown-15%
Win Rate49%

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

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading