Quant BuffetRelax, Not Over Thinking

Realized Skewness Predicts Equity Returns

Log in to collect

Academic paper

Do Realized Skewness and Kurtosis Predict the Cross-Section of Equity Returns?

AuthorsDiego Amaya; Peter Christoffersen; Kris Jacobs; Aurelio Vasquez

Institute
  • CAWilfrid Laurier University
  • CAUniversity of Toronto
  • DKCopenhagen Business School
  • DKAarhus University
  • ?Aarhus University - CREATES
  • ?University of Toronto - Rotman School of Management
  • University of Houston
  • ?University of Houston - C.T. Bauer College of Business
  • MXInstituto Tecnológico Autónomo de México
  • ?Instituto Tecnológico Autónomo de México (ITAM) - Department of Business Administration

Strategy in a nutshell

The strategy trades U.S. stocks from NYSE, AMEX, and Nasdaq priced above $5, focusing on the largest quintile by market capitalization. Using 5-minute intraday data, weekly realized skewness is computed for each stock. Stocks are sorted into deciles by skewness, with the investor buying the top decile (right-skewed) and selling the bottom decile (left-skewed). Portfolios are value-weighted and rebalanced weekly, exploiting skewness-driven return anomalies.

Economic rationale

Empirical evidence shows that stocks with higher skewness tend to have lower returns due to investors’ preference for lottery-like payoffs or heterogeneous skewness preferences under cumulative prospect theory. This creates a predictable risk–return pattern that the strategy systematically captures.

Backtest performance

Annualised return9.69%
Volatility11.53%
Beta0.556
Sharpe ratio0.84
Sortino ratio0.523
Win rate100%

Full Python code

from AlgorithmImports import *
from collections import deque
from scipy.stats import skew
import numpy as np
from pandas.core.frame import DataFrame
#endregion
class RealizedSkewnessPredictsEquityReturns(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
market:Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
# 5-minute price data.
self.data:Dict[Symbol, deque] = {}
self.period:int = 5 * 78
self.rebalance_month:int = 12
self.leverage:int = 5
self.quantile:int = 10
self.min_share_price:float = 5.

self.fundamental_count:int = 100
self.fundamental_sorting_key = lambda x: x.MarketCap
# Yearly selected universe with symbol and market cap data.
self.selected_universe:List[Fundamental] = []

self.month:int = 12
self.days:int = 5
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Minute
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection)

def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
    symbol = security.Symbol
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(self.leverage)
    
    if symbol not in self.data:
        history:DataFrame = self.History(symbol, self.period * 5, Resolution.Minute)
        if len(history) == self.period and 'close' in history:
            closes_1M:List[float] = list(history['close'])
            closes_5M:List[float] = closes_1M[::5]
            self.data[symbol] = deque(closes_5M, maxlen = self.period)

# Remove old stocks from selected universe data.
for security in changes.RemovedSecurities:
    if security.Symbol in self.data:
        del self.data[security.Symbol]
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag: 
    return Universe.Unchanged
self.selection_flag = False
selected:List[Fundamental] = [
    x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price >= self.min_share_price
]
if len(selected) > self.fundamental_count:
    selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]

self.selected_universe = selected

return list(map(lambda x: x.Symbol, selected))

def OnData(self, data: Slice) -> None:
if self.Time.minute % 5 == 0:
    # Store 5 minute data.
    for symbol in self.data:
        if symbol in data and data[symbol]:
            price = data[symbol].Value
            self.data[symbol].append(price)
if not (self.Time.hour == 16 and self.Time.minute == 0):
    return
if self.days == 5:
    aggregate_skewness:Dict[Symbol, float] = {}
    for stock in self.selected_universe:
        symbol:Symbol = stock.Symbol
        # 5 Minute data is ready.
        if symbol in self.data and len(self.data[symbol]) == self.data[symbol].maxlen:
            closes_5M:np.ndarray = np.array(self.data[symbol])
            returns_5M:np.ndarray = (closes_5M[1:] - closes_5M[:-1]) / closes_5M[:-1]
            skewness:float = skew(returns_5M)
            aggregate_skewness[stock] = skewness
                    
    if len(aggregate_skewness) != 0:
        # Aggregate skewness sorting.
        sorted_by_aggregate_skewness:List = sorted(aggregate_skewness.items(), key = lambda x: x[1], reverse = True)
        quantile:int = int(len(sorted_by_aggregate_skewness) / self.quantile)
        long:List[Fundamental] = [x[0] for x in sorted_by_aggregate_skewness[-quantile:]]
        short:List[Fundamental] = [x[0] for x in sorted_by_aggregate_skewness[:quantile]]
    
        weight:Dict[Symbol, float] = {}
        # Market cap weighting.
        for i, portfolio in enumerate([long, short]):
            mc_sum:float = sum(map(lambda x: x.MarketCap, portfolio))
            for stock in portfolio:
                weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
                
        # Trade execution.
        portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in weight.items() if symbol in data and data[symbol]]
        self.SetHoldings(portfolio, True)

self.days += 1
if self.days > 5:
    self.days = 1      

def Selection(self) -> None:
if self.month == self.rebalance_month:
    self.selection_flag = True

self.month += 1
if self.month > 12:
    self.month = 1
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))