Quant BuffetRelax, Not Over Thinking

Country ETF Momentum Strategy Selecting Top 5 with 10-12 Month Momentum

Log in to collect

Academic paper

Strategy in a nutshell

The investment strategy targets ETFs linked to national equity indexes. Based on investor preference, studies suggest selecting countries with the strongest momentum over 10-12 months yields the best results. The top 5 countries demonstrating this optimal momentum are included in the investment portfolio, which is then rebalanced monthly to maintain alignment with these momentum trends. This approach aims to capitalize on the momentum effect by dynamically adjusting to changes in market performance.

Economic rationale

Academic consensus supports the momentum anomaly's effectiveness, attributing its persistence to behavioral biases like herding, overreaction, and confirmation bias. Bhorjaj and Swaminathan’s research on international stock indices reveals strong momentum influenced by macroeconomic news misreactions, rather than corporate earnings, supporting behavioral theories. Further evidence from Andreu, Swinkels, and Tjong-A-Tjoe shows that exploiting country and industry momentum through Exchange-Traded Funds (ETFs) yields about 5% annual excess returns. Their findings highlight the feasibility of using ETFs for momentum strategies, offering a practical option for investors unable to trade individual stocks directly, thus validating the presence and exploitability of momentum effects in global markets.

Backtest performance

Annualised return17.7%
Beta0.91
Sharpe ratio0.12
Sortino ratio0.13
Maximum drawdown65.4%
Win rate62%

Full Python code

from AlgoLib import *

class GlobalIndexMomentumStrategy(XXX):
'''
This strategy implements a global index momentum-based trading strategy.
It invests in the top-performing country equity indexes based on their
momentum, which is calculated using the Rate of Change (ROC) indicator
over a predefined analysis period.
'''

def Initialize(self):
'''
Initializes the trading algorithm settings, including the start date,
initial cash, list of index symbols to be tracked, and the setup for
the Rate of Change (ROC) indicator for each symbol.
'''
self.SetStartDate(2000, 1, 1)  # Set the start date of the algorithm.
self.SetCash(100000)  # Set the initial cash for the portfolio.

# Initialize performance tracking dictionary.
self.performance = {}

# Analysis period set for 6 months, approximated by trading days.
self.analysis_period = 6 * 21
self.SetWarmUp(self.analysis_period, Resolution.Daily)  # Warm-up period for indicators.

# List of ETFs representing various country equity indexes.
self.index_symbols = [
    "EWA",  # Australia
    "EWO",  # Austria
    "EWK",  # Belgium
    "EWZ",  # Brazil
    "EWC",  # Canada
    "FXI",  # China
    "EWQ",  # France
    "EWG",  # Germany
    "EWH",  # Hong Kong
    "EWI",  # Italy
    "EWJ",  # Japan
    "EWM",  # Malaysia
    "EWW",  # Mexico
    "EWN",  # Netherlands
    "EWS",  # Singapore
    "EZA",  # South Africa
    "EWY",  # South Korea
    "EWP",  # Spain
    "EWD",  # Sweden
    "EWL",  # Switzerland
    "EWT",  # Taiwan
    "THD",  # Thailand
    "EWU",  # United Kingdom
    "SPY",  # USA
]

self.top_performers_count = 5  # Number of top performers to invest in.

for symbol in self.index_symbols:
    equity = self.AddEquity(symbol, Resolution.Daily)  # Add equity for tracking.
    equity.SetFeeModel(StandardFeeModel())  # Set the fee model.
    equity.SetLeverage(5)  # Set leverage for each equity.
    
    # Initialize and track the Rate of Change (ROC) indicator for each symbol.
    self.performance[symbol] = self.ROC(symbol, self.analysis_period, Resolution.Daily)
    
self.last_month_processed = -1  # Track the last processed month.

def OnData(self, data: Slice):
'''
This method is called at the start of each trading day. It checks for
the top-performing indexes based on momentum and adjusts the portfolio
accordingly by liquidating positions not in the top performers and
investing in the new top performers.
'''

if self.IsWarmingUp:
    return  # Do nothing if the algorithm is still warming up.

# Execute trades at the start of the trading day after warm-up.
if self.Time.hour == 9 and self.Time.minute == 30:
    if self.Time.month == self.last_month_processed:
        return  # Do nothing if we have already processed this month.
    
    self.last_month_processed = self.Time.month  # Update the last processed month.
    
    # Sort symbols by their momentum in descending order.
    momentum_sorted = sorted([x for x in self.performance.items() if x[1].IsReady and x[0] in data], key=lambda x: x[1].Current.Value, reverse=True)
    to_invest = []

    # Select the top performers to invest in.
    if len(momentum_sorted) >= self.top_performers_count:
        to_invest = [x[0] for x in momentum_sorted[:self.top_performers_count]]
        
    # Adjust portfolio based on the selected top performers.
    for symbol in self.Portfolio:
        if symbol not in to_invest:
            self.Liquidate(symbol)  # Liquidate positions not in the top performers.
    
    for symbol in to_invest:
        self.SetHoldings(symbol, 1 / len(to_invest))  # Invest in the top performers evenly.
    
class StandardFeeModel(FeeModel):
'''
This class represents a customized fee model where the order fee is calculated
as a percentage of the transaction amount, with a custom fee rate specified.
'''

def GetOrderFee(self, parameters: OrderFeeParameters):
'''
Calculates the fee for an order based on the custom fee rate.

:param parameters: Contains details about the order for which the fee is calculated.
:return: The calculated fee for the order.
'''

fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.0001  # Calculate the fee at the custom rate.
return OrderFee(CashAmount(fee, "USD"))  # Return the calculated fee.