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

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 Return14.9%
Volatility13.93%
Beta0.218
Sharpe Ratio1.07
Sortino Ratio0.404
Maximum Drawdown-10.92%
Win Rate82%

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

Leave a Reply

Discover more from Quant Buffet

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

Continue reading