
The strategy invests in 10-year bonds from 54 countries, longing top-return quintiles, shorting bottom-return quintiles, with equal weighting and monthly rebalancing based on past month’s performance.
ASSET CLASS: futures, swaps | REGION: Global | FREQUENCY:
Monthly | MARKET: bonds | KEYWORD: Momentum
I. STRATEGY IN A NUTSHELL
Trades 10-year government bonds from 54 countries, ranking by past month’s returns. Goes long on the top quintile and short on the bottom quintile, with equally weighted portfolios rebalanced monthly.
II. ECONOMIC RATIONALE
Last month’s bond returns predict future short-term performance across asset classes. This momentum is independent of traditional factors, robust across periods, and reflects a common, persistent short-term return pattern.
III. SOURCE PAPER
Short-Term Momentum (Almost) Everywhere [Click to Open PDF]
Adam Zaremba, Andreas Karathanasopoulos, Huaigang Long.Montpellier Business School; Poznan University of Economics and Business; University of Cape Town (UCT).University of Dubai.Zhejiang University; Zhejiang University of Finance and Economics (ZUFE)
<Abstract>
Is there a short-term reversal effect outside the universe of individual stocks? To answer this, we investigate a comprehensive dataset of more than two centuries of returns on five major asset classes: equity indices, government bonds, treasury bills, commodities, and currencies. Contrary to stock-level evidence, we find a striking short-term momentum pattern: the most recent month’s return positively predicts future performance. The effect is not explained by established return predictors — including the standard momentum — and is robust to many considerations. The short-term momentum is strongest among assets of high idiosyncratic volatility and in periods of elevated return dispersion. Also, the strategy payoffs display partial commonality across different asset classes.


IV. BACKTEST PERFORMANCE
| Annualised Return | 6.04% |
| Volatility | 9.69% |
| Beta | -0.037 |
| Sharpe Ratio | 0.62 |
| Sortino Ratio | -0.305 |
| Maximum Drawdown | N/A |
| Win Rate | 49% |
V. FULL PYTHON CODE
from AlgorithmImports import *
class OneMonthMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = [
"EUREX_FGBL1", # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
"CME_TY1", # 10 Yr Note Futures, Continuous Contract #1 (USA)
"MX_CGB1", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
"ASX_XT1", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
"SGX_JB1", # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
"LIFFE_R1", # Long Gilt Futures, Continuous Contract #1 (U.K.)
"EUREX_FBTP1" # Long-Term Euro-BTP Futures, Continuous Contract #1 (Italy)
]
self.period = 21
self.quantile = 5
self.SetWarmUp(self.period)
# Daily ROC data.
self.data = {}
for symbol in self.symbols:
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(5)
self.data[symbol] = self.ROC(symbol, self.period, Resolution.Daily)
self.rebalance_flag: bool = False
self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.At(0, 0), self.Rebalance)
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
def on_data(self, slice: Slice) -> None:
if not self.rebalance_flag:
return
self.rebalance_flag = False
# Return sorting.
sorted_by_return = sorted([x for x in self.data.items() if x[1].IsReady and self.Securities[x[0]].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[x[0]] and slice.contains_key(x[0]) and slice[x[0]]], key = lambda x: x[1].Current.Value, reverse = True)
long = []
short = []
if len(sorted_by_return) >= self.quantile:
quintile = int(len(sorted_by_return) / self.quantile)
long = [x[0] for x in sorted_by_return[:quintile]]
short = [x[0] for x in sorted_by_return[-quintile:]]
# Trade execution.
targets: List[PortfolioTarget] = []
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
if slice.contains_key(symbol) and slice[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
def Rebalance(self):
self.rebalance_flag = 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