Quant BuffetRelax, Not Over Thinking

Momentum Combined with Value Effect within Countries

Log in to collect

Academic paper

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.

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.

Backtest performance

Annualised return24.16%
Volatility24.53%
Beta-0.055
Sharpe ratio0.82
Win rate51%

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