
The strategy involves trading 10-year interest rate swaps from G10 countries, going long when the current rate exceeds the previous year’s rate, short when it is lower, with daily rebalancing and 10:1 leverage.
ASSET CLASS: futures, swaps | REGION: Global | FREQUENCY:
Daily | MARKET: bonds | KEYWORD: Interest Rate, Momentum
I. STRATEGY IN A NUTSHELL
The strategy trades 10-year interest rate swaps in G10 countries. It goes long when the current swap rate exceeds the rate from 262 trading days ago and short when lower. The portfolio is equally weighted, rebalanced daily, and levered 10:1 to capture long-term rate trends.
II. ECONOMIC RATIONALE
Momentum in interest rate swaps arises from persistent monetary policy trends. Rising or falling rates tend to continue, with higher returns typically during rate declines. Effectiveness may wane in near-zero-rate environments but remains stronger in markets with higher nominal rates, such as emerging economies.
III. SOURCE PAPER
Interest Rate Momentum Everywhere Across Global Yield Curves [Click to Open PDF]
Jonathan Hartley, Harvard Kennedy School
<Abstract>
This paper explores time series momentum in fixed income securities. Almost all countries in our large sample of 28 advanced and emerging markets have statistically significant positive time series momentum strategy returns. Shorter maturity fixed income securities have greater momentum returns compared to longer maturity securities arguably as a result of investor underreaction to monetary policy cycles. A significantly greater share of positive momentum returns occurs during falling rate environments versus rising rate environments as a result of the secular decline in interest rates experienced in recent decades suggesting that if low interest rates persist, momentum returns could be lower.


IV. BACKTEST PERFORMANCE
| Annualised Return | 2.42% |
| Volatility | 4.18% |
| Beta | -0.048 |
| Sharpe Ratio | 0.58 |
| Sortino Ratio | -0.351 |
| Maximum Drawdown | N/A |
| Win Rate | 55% |
V. FULL PYTHON CODE
from AlgorithmImports import *
class InterestRateMomentuminGlobalYieldCurves(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = [
"ASX_XT1", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
"MX_CGB1", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
"EUREX_FGBL1", # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
"LIFFE_R1", # Long Gilt Futures, Continuous Contract #1 (U.K.)
"EUREX_FBTP1", # Long-Term Euro-BTP Futures, Continuous Contract #1 (Italy)
"SGX_JB1", # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
"CME_TY1" # 10 Yr Note Futures, Continuous Contract #1 (USA)
]
# Daily ROC data.
self.data = {}
self.period = 252
self.SetWarmUp(self.period)
self.leverage = 5
for symbol in self.symbols:
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(self.leverage)
self.data[symbol] = self.ROC(symbol, self.period, Resolution.Daily)
def OnData(self, data):
# Custom data is still coming.
if any([self.securities[symbol].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[symbol] for symbol in self.symbols]):
self.liquidate()
return
# Momentum sorting.
long = [x[0] for x in self.data.items() if x[1].IsReady and x[1].Current.Value > 0 and x[0] in data and data[x[0]]]
short = [x[0] for x in self.data.items() if x[1].IsReady and x[1].Current.Value < 0 and x[0] in data and data[x[0]]]
# Trade execution.
targets: List[PortfolioTarget] = []
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
if symbol in data and data[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
# 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
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
VI. Backtest Performance