The strategy trades ETFs and futures from 66 countries, combining value (B/M ratios) and momentum (12-month performance), rebalancing monthly with equally weighted long and short positions.

I. STRATEGY IN A NUTSHELL

The strategy invests in ETFs and futures from 66 countries, going long on high B/M, high-performing countries and short on low B/M, poor-performing countries. The portfolio is equally weighted and rebalanced monthly.

II. ECONOMIC RATIONALE

Value and momentum effects arise from market mispricing and investor biases. By exploiting undervaluation and price continuation, the strategy captures global market inefficiencies while diversifying risk.

III. SOURCE PAPER

Value, Size and Momentum Across Countries [Click to Open PDF]

Zaremba, Montpellier Business School, Poznan University of Economics and Business; Konieczka, Montpellier Business School, Poznan University of Economics and Business

<Abstract>

The study investigates the characteristics of inter-country value, size and momentum premiums. We contribute to the asset-pricing literature in three ways. First, we provide fresh evidence for value, size and momentum premiums in country returns. Second, we show that these premiums are robust to the changes of functional currencies or countries’ representative indices. Third, we demonstrate, that the country-level value, size and momentum premiums tend to strengthen each other in double-sorted portfolios. We examine listings of stocks in 66 countries between 2000 and 2013.

IV. BACKTEST PERFORMANCE

Annualised Return24.16%
Volatility24.53%
Beta-0.055
Sharpe Ratio0.82
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate51%

V. FULL PYTHON CODE

from AlgorithmImports import *
#endregion
class MomentumCombinedwithValueEffectwithinCountries(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100000)
        self.symbols = {
            'Argentina' : 'ARGT',
            'Australia' : 'EWA',
            'Austria' : 'EWO',
            'Belgium' : 'EWK',
            'Brazil' : 'EWZ',
            'Canada' : 'EWC',
            'Chile' : 'ECH',
            'China' : 'FXI',
            'Egypt' : 'EGPT',
            'France' : 'EWQ',
            'Germany' : 'EWG',
            'Hong Kong' : 'EWH',
            'India' : 'INDA',
            'Indonesia' : 'EIDO',
            'Ireland' : 'EIRO',
            'Israel' : 'EIS',
            'Italy' : 'EWI',
            'Japan' : 'EWJ',
            'Malaysia' : 'EWM',
            'Mexico' : 'EWW',
            'Netherlands' : 'EWN',
            'New Zealand' : 'ENZL',
            'Norway' : 'NORW',
            'Philippines' : 'EPHE',
            'Poland' : 'EPOL',
            'Russia' : 'ERUS',
            'Saudi Arabia' : 'KSA',
            'Singapore' : 'EWS',
            'South Africa' : 'EZA',
            'South Korea' : 'EWY',
            'Spain' : 'EWS',
            'Sweden' : 'EWD',
            'Switzerland' : 'EWL',
            'Taiwan' : 'EWT',
            'Thailand' : 'THD',
            'Turkey' : 'TUR',
            'United Kingdom' : 'EWU',
            'United States' : 'SPY'
        }
        
        self.data:dict[str, RollingWindow] = {}
        self.period:int = 12 * 21
        self.SetWarmUp(self.period, Resolution.Daily)
        
        for symbol in self.symbols:
            data = self.AddEquity(self.symbols[symbol], Resolution.Daily)
            data.SetLeverage(5)
            
            self.data[symbol] = RollingWindow[float](self.period)
        self.recent_month:int = -1
        self.max_missing_days:int = 365
        self.quantile:int = 3
        self.country_pb_data:Symbol = self.AddData(CountryPB, 'CountryData').Symbol
    
    def OnData(self, data:Slice) -> None:
        # store daily data
        for symbol, etf in self.symbols.items():
            etf_symbol:Symbol = self.Symbol(etf)
            if etf_symbol in data and data[etf_symbol]:
                self.data[symbol].Add(data[etf_symbol].Value)
    
        # rebalance once a month
        if self.recent_month == self.Time.month:
            return
        self.recent_month = self.Time.month
        if self.Securities[self.country_pb_data].GetLastData() and (self.Time.date() - self.Securities[self.country_pb_data].GetLastData().Time.date()).days > self.max_missing_days:
            self.Liquidate()
            return
        bm_data:dict[str, float] = {}
        performance:dict[str, float] = {}
        country_pb_data = self.Securities[self.country_pb_data].GetLastData()
        if country_pb_data:
            for symbol in self.symbols:
                if self.data[symbol].IsReady:
                    pb:float = country_pb_data[symbol]
                    bm_data[symbol] = 1 / pb
                    closes:List[float] = list(self.data[symbol])
                    performance[symbol] = closes[0] / closes[-1] - 1
        long:List[str]= []
        short:List[str] = []
        if len(bm_data) >= self.quantile * 2:
            sorted_by_bm:List = sorted(bm_data.items(), key = lambda x: x[1], reverse = True)
            quantile:int = int(len(bm_data) / self.quantile)
            high_by_bm = [x[0] for x in sorted_by_bm[:quantile]]
            low_by_bm = [x[0] for x in sorted_by_bm[-quantile:]]
            
            high_by_perf:List = sorted(high_by_bm, key = lambda x: performance[x], reverse = True)
            quantile = int(len(high_by_perf) / self.quantile)
            long = [x for x in high_by_perf[:quantile]]
            
            low_by_perf:List = sorted(low_by_bm, key = lambda x: performance[x], reverse = True)
            quantile = int(len(low_by_perf) / self.quantile)
            short = [x for x in low_by_perf[-quantile:]]
        
        invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long + short:
                self.Liquidate(symbol)
        long_count:int = len(long)
        short_count:int = len(short)
        
        for symbol in long:
            traded_symbol:str = self.symbols[symbol]
            if traded_symbol in data and data[traded_symbol]:
                self.SetHoldings(traded_symbol, 1 / long_count)
        for symbol in short:
            traded_symbol:str = self.symbols[symbol]
            if traded_symbol in data and data[traded_symbol]:
                self.SetHoldings(traded_symbol, -1 / short_count)
# Country PB data
# NOTE: IMPORTANT: Data order must be ascending (date-wise)
from dateutil.relativedelta import relativedelta
class CountryPB(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/country_pb.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = CountryPB()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%Y") + relativedelta(years=1)
        self.symbols = ['Argentina','Australia','Austria','Belgium','Brazil','Canada','Chile','China','Egypt','France','Germany','Hong Kong','India','Indonesia','Ireland','Israel','Italy','Japan','Malaysia','Mexico','Netherlands','New Zealand','Norway','Philippines','Poland','Russia','Saudi Arabia','Singapore','South Africa','South Korea','Spain','Sweden','Switzerland','Taiwan','Thailand','Turkey','United Kingdom','United States']
        index = 1
        for symbol in self.symbols:
            data[symbol] = float(split[index])
            index += 1
            
        data.Value = float(split[1])
        return data

Leave a Reply

Discover more from Quant Buffet

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

Continue reading