The strategy trades 10+ year bonds using seasonal return patterns, going long on high-return months and short on low, rebalancing monthly, with futures recommended for better liquidity and cost efficiency.

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

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

III. SOURCE PAPER

Cross-Sectional Seasonalities in International Government Bond Returns [Click to Open PDF]

Adam Zaremba.Montpellier Business School; Poznan University of Economics and Business; University of Cape Town (UCT)

<Abstract>

We are the first to document the cross-sectional return seasonality effect in international government bonds. Using a variety of tests, we examine fixed-income securities from 22 countries for the years 1980–2018. The bonds with high (low) returns in the same-calendar month in the past continue to overperform (underperform) in the future. The effect is robust to many considerations, including controlling for established predictors of bond returns. Our results support the behavioural story of the anomaly, demonstrating its highest profitability in the periods of elevated investor sentiment and in the market segments of strong limits to arbitrage. Nonetheless, investment application of bond seasonality might be challenging due to high trading costs and the required short holding periods.

IV. BACKTEST PERFORMANCE

Annualised Return5.41%
Volatility11.07%
Beta0.015
Sharpe Ratio0.49
Sortino Ratio-0.573
Maximum DrawdownN/A
Win Rate55%

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

Leave a Reply

Discover more from Quant Buffet

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

Continue reading