Cross-Sectional Seasonalities in International Government Bond Returns
Log in to collectAcademic paper
Cross-Sectional Seasonalities in International Government Bond Returns
Adam Zaremba
- Montpellier Business School
- Poznań University of Economics and Business
- ?Poznan University of Economics and Business
Strategy in a nutshell
The strategy trades 10+ year government bonds across 22 developed and emerging markets using a seasonal momentum signal based on average returns for the same calendar month over the past 20 years. A zero-cost portfolio goes long the top 20% and short the bottom 20% of bonds, rebalanced monthly. Futures are used to improve liquidity and minimize transaction costs.
Economic rationale
Seasonal patterns in bond returns arise from cyclical investor behavior rather than fundamentals. These sentiment-driven anomalies persist due to limited arbitrage and high turnover. Trading through bond futures enhances liquidity and implementation efficiency, making the strategy both practical and profitable.
Backtest performance
Full Python code
from AlgorithmImports import *
import data_tools
from collections import deque
class SeasonalitiesBondReturns(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_FOAT1", # Euro-OAT Futures, Continuous Contract #1 (France)
"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)
"CME_TY1", # 10 Yr Note Futures, Continuous Contract #1 (USA)
"SGX_JB1" # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
}
# daily price data
self.data = {}
# monthly returns
self.monthly_return = {}
self.daily_period = 21
self.monthly_period = 20 * 12
self.traded_count = 1
for symbol in self.symbols:
# Bond future data.
data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(5)
self.data[symbol] = RollingWindow[float](self.daily_period)
self.monthly_return[symbol] = deque(maxlen=self.monthly_period)
self.settings.minimum_order_margin_portfolio_percentage = 0.
self.settings.daily_precise_end_time = False
self.rebalance_flag: bool = False
self.Schedule.On(self.DateRules.MonthEnd('ASX_XT1'), self.TimeRules.At(0, 0), self.Rebalance)
def OnData(self, data):
# store monthly future returns
for symbol in self.symbols:
if symbol in data and data[symbol]:
price = data[symbol].Value
self.data[symbol].Add(price)
if not self.rebalance_flag:
return
self.rebalance_flag = False
curr_month = self.Time.month
SAME = {}
# store monthly returns
for symbol in self.symbols:
if self.Securities[symbol].GetLastData() and self.Time.date() < data_tools.QuantpediaFutures.get_last_update_date()[symbol]:
if self.data[symbol].IsReady:
monthly_ret = self.data[symbol][0] / self.data[symbol][self.daily_period - 1] - 1
self.monthly_return[symbol].append((monthly_ret, curr_month))
# monthly returns are ready
if len(self.monthly_return[symbol]) >= self.monthly_period / 2:
next_month = curr_month+1 if curr_month < 12 else 1
same_month_returns = [x[0] for x in self.monthly_return[symbol] if x[1] == next_month]
SAME[symbol] = np.mean(same_month_returns)
else:
self.liquidate(symbol)
continue
long = []
short = []
if len(SAME) >= self.traded_count * 2:
# decile = int(len(SAME) / self.quantile)
# count = decile
# sorting by SAME
sorted_by_SAME = sorted(SAME.items(), key = lambda x: x[1], reverse = True)
long = [x[0] for x in sorted_by_SAME[:self.traded_count]]
short = [x[0] for x in sorted_by_SAME[-self.traded_count:]]
# order 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) / self.traded_count))
self.SetHoldings(targets, True)
def Rebalance(self):
self.rebalance_flag = True