Quant BuffetRelax, Not Over Thinking

Top 3 ETF Momentum Strategy Selecting from SPY, EFA, BND, VNQ, GSG

Log in to collect

Academic paper

Relative Strength Strategies for Investing

AuthorsMeb Faber; Cambria Investment Management

Institute
  • Institut Mines-Télécom Business School
  • ?Cambria Investment Management

Strategy in a nutshell

In an investment strategy selecting from five ETFs—SPY, EFA, BND, VNQ, GSG—focus on identifying the three with the best 12-month performance. Allocate your investment equally among these chosen ETFs, ensuring each one has a third of the total investment. Maintain this allocation for one month. Afterward, assess the performance of all five ETFs again, selecting the top three performers for the next month. This cycle of equal weighting, holding, and monthly rebalancing adheres to a dynamic investment approach, adapting to market trends and optimizing portfolio performance.

Economic rationale

Momentum investing is recognized by scholars as a potent factor for generating returns, continuing to draw academic interest. This leads to numerous momentum-based strategies available to investors, though their future effectiveness remains a consideration. The strategy's foundation lies in rotating among asset classes with varying sensitivities to business cycles, aiming to select those with the highest return potential and lowest loss risk. Kessler and Scherer highlight in “Macro Momentum and the Economy” that rotational strategy success stems from exploiting predictable shifts in investment opportunities, offering rational payoffs to investors. Today's investors have access to a vast selection of mutual funds, ETFs, and closed-end funds, many of which offer low-cost or commission-free trading options, making this strategy increasingly accessible.

Backtest performance

Annualised return11.5%
Volatility11.0%
Beta0.3
Sharpe ratio0.25
Sortino ratio0.23
Maximum drawdown39.8%
Win rate88%

Full Python code

from AlgoLib import *

class AssetMomentumStrategy(XXX):
'''
This class implements a momentum-based trading strategy, inheriting from a base trading algorithm class.
It selects a subset of assets based on their momentum and rebalances the portfolio monthly, aiming to hold positions in assets demonstrating the strongest momentum.
'''

def Initialize(self):
'''
Initializes the trading strategy by setting the start date, initial cash, the momentum period for calculating rate of change (ROC), 
the assets to be traded, and setting up the necessary indicators and parameters for the trading algorithm.
'''

self.SetStartDate(2000, 1, 1)  # Set the start date of the backtest.
self.SetCash(100000)  # Set the initial capital for the backtest.

self.momentum_data = {}  # Initialize a dictionary to store Rate of Change (ROC) indicators for each asset.
momentum_period = 252  # Define the momentum period (typically 252 trading days, equivalent to 12 months).
self.SetWarmUp(momentum_period, Resolution.Daily)  # Set the warm-up period for the algorithm.

self.assets_to_trade = 3  # Define the number of assets to hold positions in simultaneously.

self.asset_symbols = ["SPY", "EFA", "IEF", "VNQ", "GSG"]  # List of asset symbols to consider for trading.

# Loop through each asset symbol, adding them to the algorithm and initializing their ROC indicators.
for asset in self.asset_symbols:
    self.AddEquity(asset, Resolution.Minute)  # Add the equity data for the asset.
    self.momentum_data[asset] = self.ROC(asset, momentum_period, Resolution.Daily)  # Initialize and store the ROC indicator for the asset.

self.last_rebalance_month = None  # Variable to track the last month the portfolio was rebalanced.

def OnData(self, data):
'''
The event handler for new data points. This function checks for the right conditions to rebalance the portfolio based on asset momentum.
'''

if self.IsWarmingUp:  # Check if the algorithm is still in the warm-up period.
    return

# Check if the current time is the market open time (9:30 AM).
if self.Time.hour != 9 or self.Time.minute != 30:
    return

self.Debug(f"Market open at: {self.Time}")  # Debug log for market opening time.

# Check if it's time to rebalance the portfolio (done monthly).
current_month = self.Time.month
if current_month == self.last_rebalance_month:  # If already rebalanced this month, do nothing.
    return
self.last_rebalance_month = current_month  # Update the last rebalance month.

self.Debug("Initiating monthly rebalance...")  # Debug log for rebalancing.

# Filter the assets that are ready for trading based on their ROC being ready.
ready_assets = {
    symbol: roc for symbol, roc in self.momentum_data.items() 
    if symbol in data and data[symbol] and roc.IsReady
}

# Sort the assets by their momentum in descending order.
assets_sorted_by_momentum = sorted(ready_assets.items(), key=lambda item: item[1].Current.Value, reverse=True)
self.Debug(f"Assets sorted for trading: {len(assets_sorted_by_momentum)} required: {self.assets_to_trade}")

# Select the top assets for trading based on momentum.
assets_to_long = [asset for asset, _ in assets_sorted_by_momentum[:self.assets_to_trade]]

# Liquidate positions not in the top assets list.
for asset in self.Portfolio:
    if asset not in assets_to_long and self.Portfolio[asset].Invested:
        self.Liquidate(asset)

self.Debug(f"Assets selected for long positions: {assets_to_long}")  # Debug log for selected assets.

# Allocate equal weight to each selected asset in the portfolio.
for asset in assets_to_long:
    self.SetHoldings(asset, 1 / len(assets_to_long))