Quant BuffetRelax, Not Over Thinking

Reversal – Yield Change 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

The strategy trades 10-year government bond futures from Australia, Canada, Germany, the UK, and the US using a reversal factor based on yield changes over 3 months, 6 months, and 6 years. Allocations follow bottom, median, or top approaches. Portfolios buy/sell proportionally based on cross-sectional scores relative to the average, adjusted for dispersion, with equal-weighted allocations across countries to neutralize directional bias. Rebalanced monthly, the strategy systematically leverages reversal factors for bond decisions.

Economic rationale

Yield changes, typically a value indicator, serve here as a reversal factor for predicting bond futures. While no fundamental theory explains this, statistical and machine learning analyses confirm its reliability. Reversal is uncorrelated with carry, a traditional bond driver, making it a distinct and effective factor for portfolio construction and enhancing bond investment outcomes.

Backtest performance

Annualised return1.7%
Volatility10%
Beta0.004
Sharpe ratio0.17
Sortino ratio-1.317
Maximum drawdown-20%
Win rate48%

Full Python code

import numpy as np
from AlgorithmImports import *
import data_tools
class ReversalYieldChangeFactor(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)

self.symbols = {
                "ASX_XT1"     : "AU10YT",        # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
                "MX_CGB1"     : "CA10YT",        # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
                "EUREX_FGBL1" : "DE10YT",        # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
                "LIFFE_R1"    : "GB10YT",        # Long Gilt Futures, Continuous Contract #1 (U.K.)
                "CME_TY1"     : "US10YT"         # 10 Yr Note Futures, Continuous Contract #1 (USA)
                }
self.bond_yield = {}  # Bond yield data
self.period = 6*21
self.SetWarmUp(self.period)

for symbol in self.symbols:
    data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily)
    data.SetFeeModel(data_tools.CustomFeeModel())
    
    bond_yield_symbol = self.symbols[symbol]
    self.AddData(data_tools.QuantpediaBondYield, bond_yield_symbol, Resolution.Daily)
    
    self.bond_yield[symbol] = RollingWindow[float](self.period)
    
first_key = [x for x in self.symbols.keys()][0]
self.rebalance_flag: bool = False
self.Schedule.On(self.DateRules.MonthStart(first_key), self.TimeRules.At(0, 0), self.Rebalance)
self.settings.minimum_order_margin_portfolio_percentage = 0.

def OnData(self, data):
# store daily bond yield values
for symbol in self.symbols:
    yield_symbol = self.symbols[symbol]
    if yield_symbol in data and data[yield_symbol]:
        bond_yield = data[yield_symbol].Value
        self.bond_yield[symbol].Add(bond_yield)

if not self.rebalance_flag:
    return
self.rebalance_flag = False
yield_change = {
    symbol : self.bond_yield[symbol][self.period-1] - self.bond_yield[symbol][0] 
    for symbol, bond in self.symbols.items() 
    if self.bond_yield[symbol].IsReady 
    and all([self.Securities[x].GetLastData() and self.Time.date() < data_tools.LastDateHandler.get_last_update_date()[x]] for x in [symbol, bond])
}
if len(yield_change) <= 2: return

avg_change = np.average([x[1] for x in yield_change.items()])

diff = {
    symbol : avg_change - yield_change[symbol] for symbol in yield_change
}
total_diff = sum([abs(x[1]) for x in diff.items()])

targets: List[PortfolioTarget] = []
for symbol in diff:
    if symbol in data and data[symbol]:
        targets.append(PortfolioTarget(symbol, diff[symbol]/total_diff))

self.SetHoldings(targets, True)
def Rebalance(self):
self.rebalance_flag = True