The strategy involves 64 country ETFs, going long the top 10% and short the bottom 10% based on monthly Market Breadth (MBR). The portfolio is equally weighted and rebalanced monthly.

I. STRATEGY IN A NUTSHELL

The strategy trades country ETFs from 64 countries. Each month, Market Breadth (MBR)—the scaled difference between rising and falling stocks—is calculated. The top 10% of countries by MBR are bought, and the bottom 10% are sold. Portfolios are equally weighted and rebalanced monthly to exploit strong breadth and avoid weak breadth.

II. ECONOMIC RATIONALE

Market breadth effects persist even after controlling for size, style, volatility, skewness, momentum, and trend-following. The effect is strongest in markets with high limits to arbitrage, following bullish periods, and in collectivistic societies, consistent with behavioral explanations.

III. SOURCE PAPER

 Herding for Profits: Market Breadth and the Cross-Section of Global Equity Returns [Click to Open PDF]

Zaremba, Poznan University of Economics and Business; Szyszka, University of Dubai; Karathanasopoulos, Warsaw School of Economics; Mikutowski, University of Dubai; [Name Missing], Poznan University of Economics and Business

<Abstract>

This paper shows that market breadth, i.e. the difference between the average number of rising stocks and the average number of falling stocks within a portfolio, is a robust predictor of future stock returns on market and industry portfolios for 64 countries for the period between 1973 and 2018. We link the market breadth with herd behavior and show that high market breadth portfolios significantly outperform low market breadth portfolios, and that this effect is robust to effects such as size, style, volatility, skewness, momentum, and trend-following signals. In addition, the role of market breadth is particularly strong among markets characterized by high limits to arbitrage, following bullish periods, and in collectivistic societies, supporting behavioral explanations of the phenomenon. We also examine practical implications of the effect and our results indicate that the effect may be employed for equity allocation and market timing, although frequent portfolio rebalancing can lead to higher transaction costs that may affect profitability.

IV. BACKTEST PERFORMANCE

Annualised Return20.76%
Volatility21.69%
Beta-0.08
Sharpe Ratio0.96
Sortino Ratio-0.152
Maximum DrawdownN/A
Win Rate51%

V. FULL PYTHON CODE

from AlgorithmImports import *
from pandas.core.frame import dataframe
class MarketBreadthInGlobalEquities(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        etf_tickers: List[Tuple[str, str]] = [
            # (CountryId, ticker)
            ("AUS", "EWA"), # iShares MSCI Australia Index ETF
            ("BRA", "EWZ"),  # iShares MSCI Brazil Index ETF
            ("CAN", "EWC"),  # iShares MSCI Canada Index ETF
            ("CHN", "FXI"),  # iShares China Large-Cap ETF
            ("FRA", "EWQ"),  # iShares MSCI France Index ETF
            ("DEU", "EWG"),  # iShares MSCI Germany ETF
            ("HKG", "EWH"),  # iShares MSCI Hong Kong Index ETF
            ("JPN", "EWJ"),  # iShares MSCI Japan Index ETF
            ("MEX", "EWW"),  # iShares MSCI Mexico Inv. Mt. Idx
            ("NLD", "EWN"),  # iShares MSCI Netherlands Index ETF
            ("SGP", "EWS"),  # iShares MSCI Singapore Index ETF
            ("KOR", "EWY"),  # iShares MSCI South Korea ETF
            ("CHE", "EWL"),  # iShares MSCI Switzerland Index ETF
            ("GBR", "EWU"),  # iShares MSCI United Kingdom Index ETF
            ("USA", "SPY"),  # SPDR S&P 500 ETF
            ("IRL", "EIRL"),  # iShares MSCI Ireland ETF
            ("ISR", "EIS"),  # iShares MSCI Israel ETF    
        ]
        
        self.data: Dict[Symbol, SymbolData] = {}
        self.etf_by_country: Dict[str, Symbol] = {}
        
        self.long: Dict[Symbol] = []
        self.short: Dict[Symbol] = []
        
        self.period: int = 2 * 12 * 21 # 2 years of daily closes
        self.count_to_invest: int = 2 # We go 2 etfs long and 2 etfs short, because our universe consists of country ETF from 17 countries.
        self.leverage: int = 3
        
        for country_id, ticker in etf_tickers:
            security: Security = self.AddEquity(ticker, Resolution.Daily)
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
            
            self.etf_by_country[country_id] = security.Symbol
            
        market: Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        self.selection_flag: bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
        self.settings.daily_precise_end_time = False
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        for stock in fundamental:
            symbol: Symbol = stock.Symbol
            
            if symbol in self.data:
                self.data[symbol].update(stock.AdjustedPrice)
        
        if not self.selection_flag:
            return Universe.Unchanged
        
        selected: List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.CompanyReference.BusinessCountryID in self.etf_by_country]
        
        country_stocks: Dict[str, CountryStocks] = {}
        for stock in selected:
            symbol: Symbol = stock.Symbol
            if symbol not in self.data:
                self.data[symbol] = SymbolData(self.period)
                history: dataframe = self.History(symbol, self.period, Resolution.Daily)
                if history.empty:
                    self.Log(f"Not enough data for {symbol} yet")
                    continue
                closes: pd.Series = history.loc[symbol].close
                for time, close in closes.items():
                    self.data[symbol].update(close)
            
            if self.data[symbol].is_ready():
                country_id: str = stock.CompanyReference.BusinessCountryID
                performance: float = self.data[symbol].performance()
            
                if country_id not in country_stocks:
                    country_stocks[country_id] = CountryStocks()
                
                if performance > 0.:
                    country_stocks[country_id].increase_rising()
                else:
                    country_stocks[country_id].increase_falling()
                
        if len(country_stocks) == 0:
            return Universe.Unchanged
            
        sorted_by_market_breadth: List[str] = [x[0] for x in sorted(country_stocks.items(), key=lambda item: item[1].market_breadth())]
        
        self.long = [self.etf_by_country[country_id] for country_id in sorted_by_market_breadth[-self.count_to_invest:]]
        self.short = [self.etf_by_country[country_id] for country_id in sorted_by_market_breadth[:self.count_to_invest]]
            
        return self.long + self.short
        
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # order execution
        targets:List[PortfolioTarget] = []
        for i, portfolio in enumerate([self.long, self.short]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)
        self.long.clear()
        self.short.clear()
        
    def Selection(self) -> None:
        self.selection_flag = True
        
class SymbolData():
    def __init__(self, period: int):
        self._closes:RollingWindow = RollingWindow[float](period)
        
    def update(self, close: float) -> None:
        self._closes.Add(close)
        
    def is_ready(self) -> bool:
        return self._closes.IsReady
        
    def performance(self) -> float:
        return self._closes[0] / self._closes[self._closes.Count - 1] - 1
        
class CountryStocks():
    def __init__(self):
        self._rising_stocks_count: int = 0
        self._falling_stocks_count: int = 0
        
    def increase_rising(self) -> None:
        self._rising_stocks_count += 1
        
    def increase_falling(self) -> None:
        self._falling_stocks_count += 1
        
    def market_breadth(self) -> float:
        result: float = (self._rising_stocks_count - self._falling_stocks_count) / (self._rising_stocks_count + self._falling_stocks_count)
        return result 
        
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

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

Continue reading