Quant BuffetRelax, Not Over Thinking

Skewness Effect in Country Equity Indexes

Log in to collect

Academic paper

Skewness Preference Across Countries

AuthorsAdam Zaremba; Andrzej Nowak

Institute
  • Poznań University of Economics and Business
  • Montpellier Business School
  • ?Poznan University of Economics and Business
  • ?Poznan University of Economics

Strategy in a nutshell

This strategy trades 39 MSCI USD-denominated markets using 24-month historical skewness. Each month, the most negatively skewed quintile is bought and the most positively skewed quintile is sold, with markets equally weighted and the portfolio rebalanced monthly to exploit skewness-driven inefficiencies.

Economic rationale

Investors overvalue small-probability, high-payoff outcomes, favoring positively skewed markets. This effect is stronger in globally integrated, liquid markets with international investors, while illiquid or local markets show a weaker skewness influence, confirming skewness as a driver of returns.

Backtest performance

Annualised return13.76%
Volatility25.66%
Beta-0.014
Sharpe ratio0.54
Sortino ratio0.372
Win rate52%

Full Python code

import numpy as np
from AlgorithmImports import *
from scipy.stats import skew
class SkewnessEffectEquities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)

self.symbols = ["EWJ",  # iShares MSCI Japan Index ETF
                "EZU",  # iShares MSCI Eurozone ETF
                "EFNL", # iShares MSCI Finland Capped Investable Market Index ETF
                "EWW",  # iShares MSCI Mexico Inv. Mt. Idx
                "ERUS", # iShares MSCI Russia ETF
                "IVV",  # iShares S&P 500 Index
                "ICOL", # Consumer Discretionary Select Sector SPDR Fund
                "AAXJ", # iShares MSCI All Country Asia ex Japan Index ETF
                "AUD",  # Australia Bond Index Fund
                "EWQ",  # iShares MSCI France Index ETF
                "BUND", # Pimco Germany Bond Index Fund
                "EWH",  # iShares MSCI Hong Kong Index ETF
                "EPI",  # WisdomTree India Earnings ETF
                "EIDO"  # iShares MSCI Indonesia Investable Market Index ETF
                "EWI",  # iShares MSCI Italy Index ETF
                "GAF",  # SPDR S&P Emerging Middle East & Africa ETF
                "ENZL", # iShares MSCI New Zealand Investable Market Index Fund
                "NORW"  # Global X FTSE Norway 30 ETF
                "EWY",  # iShares MSCI South Korea Index ETF
                "EWP",  # iShares MSCI Spain Index ETF
                "EWD",  # iShares MSCI Sweden Index ETF
                "EWL",  # iShares MSCI Switzerland Index ETF
                "GXC",  # SPDR S&P China ETF
                "EWC",  # iShares MSCI Canada Index ETF
                "EWZ",  # iShares MSCI Brazil Index ETF
                "ARGT", # Global X FTSE Argentina 20 ETF
                "AND",  # Global X FTSE Andean 40 ETF
                "AIA",  # iShares S&P Asia 50 Index ETF
                "EWO",  # iShares MSCI Austria Investable Mkt Index ETF
                "EWK",  # iShares MSCI Belgium Investable Market Index ETF
                "BRAQ", # Global X Brazil Consumer ETF
                "ECH",  # iShares MSCI Chile Investable Market Index ETF
                "CHIB", # Global X China Technology ETF
                "EGPT", # Market Vectors Egypt Index ETF
                "ADRU"  # BLDRS Europe 100 ADR Index ETF
                ]

self.lookup_period = 24 * 21
self.SetWarmup(self.lookup_period)
self.data = {}

for symbol in self.symbols:
    data = self.AddEquity(symbol, Resolution.Daily)
    data.SetLeverage(10)
    self.data[symbol] = RollingWindow[float](self.lookup_period)
    
self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.AfterMarketOpen(self.symbols[0]), self.Rebalance)

def OnData(self, data):
for symbol in self.data:
    if symbol in data and data[symbol]:
        price = data[symbol].Value
        if price != 0:
            self.data[symbol].Add(price)
        
def Rebalance(self):
if self.IsWarmingUp: return

# Skewness calculation
skewness_data = {}
for symbol in self.symbols:
    if self.data[symbol].IsReady and self.Securities[symbol].IsTradable and (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days < 5:
        prices = np.array([x for x in self.data[symbol]])
        returns = (prices[:-1]-prices[1:])/prices[1:]
        if len(returns) == self.lookup_period-1:
            # NOTE: Manual skewness calculation example
            # avg = np.average(returns)
            # std = np.std(returns)
            # skewness = (sum(np.power((x - avg), 3) for x in returns)) / ((self.return_history[symbol].maxlen-1) * np.power(std, 3))
            skewness_data[symbol] = skew(returns)
        
# Skewness sorting
sorted_by_skewness = sorted(skewness_data.items(), key = lambda x: x[1], reverse = True)
quintile = int(len(sorted_by_skewness)/5)
long = [x[0] for x in sorted_by_skewness[-quintile:]]
short = [x[0] for x in sorted_by_skewness[:quintile]]

# Trade execution
invested = [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 = len(long)
short_count = len(short)

for symbol in long:
    self.SetHoldings(symbol, 1 / long_count)
for symbol in short:
    self.SetHoldings(symbol, -1 / short_count)