Antonacci’s Dual Momentum
Log in to collectAcademic paper
Strategy in a nutshell
Construct long-only, equal-weighted portfolios across four modules: (1) U.S./Foreign equities, (2) High-yield/credit bonds, (3) Equity/mortgage REITs, and (4) Gold/Treasuries. Within each module, select the better-performing asset using 12-month relative momentum. If no asset shows positive absolute momentum versus Treasury bills, allocate to BIL. Portfolios are equally weighted across modules and rebalanced monthly.
Economic rationale
Behavioral biases and trend persistence drive momentum. Absolute momentum captures asset trends, while relative momentum enhances diversification. This approach mitigates specific risk factors, reduces portfolio volatility, and maintains consistent returns across market regimes.
Backtest performance
Annualised return14.9%
Volatility13.93%
Beta0.218
Sharpe ratio1.07
Sortino ratio0.404
Maximum drawdown-10.92%
Win rate82%
Full Python code
from AlgorithmImports import *
#endregion
class AntonaccisDualMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
period:int = 12 * 21
self.SetWarmUp(period, Resolution.Daily)
self.treasury_bill:Symbol = self.AddEquity("BIL", Resolution.Daily).Symbol
# subscribe assets for each module
self.equities:List[Symbol] = [self.AddEquity(x, Resolution.Daily).Symbol for x in ["SPY", "EFA"]]
self.bonds:List[Symbol] = [self.AddEquity(x, Resolution.Daily).Symbol for x in ["HYG", "LQD"]]
self.reits:List[Symbol] = [self.AddEquity(x, Resolution.Daily).Symbol for x in ["MBB", "VNQ"]]
self.commodities:List[Symbol] = [self.AddEquity(x, Resolution.Daily).Symbol for x in ["TLT", "GLD"]]
# traded modules
self.modules:List[List[Symbol]] = [self.equities, self.bonds, self.reits, self.commodities]
self.momentum:dict[Symbol, RateOfChange] = {}
self.momentum_treshold:float = 0.
for module in self.modules:
for asset in module:
# subscribe ROC indicator
self.momentum[asset] = self.ROC(asset, period, Resolution.Daily)
self.recent_month:int = -1
def OnData(self, data:Slice) -> None:
if self.IsWarmingUp: return
# monthly rebalance
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
tbill_allocation_count:int = 0
long:List[Symbol] = []
for module_index, module in enumerate(self.modules):
if all(self.momentum[x].IsReady for x in module):
# select and buy the better-performing asset as representative of the module
module_perf_values:List[float] = [self.momentum[x].Current.Value for x in module]
max_module_perf:float = max(module_perf_values)
best_performing_asset:Symbol = module[module_perf_values.index(max_module_perf)]
if max_module_perf > self.momentum_treshold:
long.append(best_performing_asset)
else:
tbill_allocation_count += 1
# tbill allocation weight relative to total number of modules with ROC data ready
traded_asset_count:int = (tbill_allocation_count + len(long))
tbill_allocation:float = tbill_allocation_count / traded_asset_count if traded_asset_count != 0 else 0
# liquidate
invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long + [self.treasury_bill]:
self.Liquidate(symbol)
# trade etfs
for symbol in long:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, 1 / traded_asset_count)
# trade tbills
if self.treasury_bill in data and data[self.treasury_bill]:
self.SetHoldings(self.treasury_bill, tbill_allocation)