
The investment universe consists of securities following previously mentioned investment instruments available to one individual investor. Investors construct long-only equal-weighted composite dual momentum module-based portfolios.”
ASSET CLASS: ETFs | REGION: Global | FREQUENCY:
Monthly | MARKET: bonds, commodities, equities, REITs | KEYWORD: Momentum
I. 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.
II. 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.
III. SOURCE PAPER
Risk Premia Harvesting Through Dual Momentum [Click to Open PDF]
Gary Antonacci, Portfolio Management Consultants
<Abstract>
TMomentum is the premier market anomaly. It is nearly universal in its applicability. This paper examines multi-asset momentum with respect to what can make it most effective for momentum investors. We show that both absolute and relative momentum can enhance returns, but that absolute momentum does far more to lessen volatility and drawdown. We see that combining absolute and relative momentum gives the best results.he


IV. BACKTEST PERFORMANCE
| Annualised Return | 14.9% |
| Volatility | 13.93% |
| Beta | 0.218 |
| Sharpe Ratio | 1.07 |
| Sortino Ratio | 0.404 |
| Maximum Drawdown | -10.92% |
| Win Rate | 82% |
V. 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)
VI. Backtest Performance