Quant BuffetRelax, Not Over Thinking

Weighted Frequency of Losses

Log in to collect

Academic paper

Do Investors Care About Negative Returns?

AuthorsBorys Koval; Alina Steshkova

Institute
  • ?Vienna Graduate School of Finance (VGSF)
  • ATVienna University of Economics and Business

Strategy in a nutshell

The strategy trades all NYSE, AMEX, and NASDAQ-listed U.S. stocks above $1. Using CRSP data (winsorized at 0.5% and 99.5%), it computes each stock’s monthly weighted frequency of loss, where daily returns are exponentially weighted (r = 0.13) and only negative days are counted. At month-end, stocks are sorted into deciles by this loss frequency. The portfolio goes long the bottom decile (stocks with the highest weighted loss frequency, i.e., most recent losers) and short the top decile (stocks with the lowest weighted loss frequency, i.e., recent winners). Portfolios are value-weighted and rebalanced monthly.

Economic rationale

Investor behavior is driven by heuristics—focusing on whether returns are positive or negative, rather than their magnitude. A high frequency of recent losses induces excessive selling, leading to undervaluation and future outperformance. Exponential weighting improves results, consistent with investor emphasis on recent information. The effect is strongest in retail-dominated stocks, supporting the notion that retail investors rely on simplistic, binary decision-making.

Backtest performance

Annualised return11.9%
Volatility12.34%
Beta0.171
Sharpe ratio0.96
Sortino ratio0.237
Win rate50%

Full Python code

from AlgorithmImports import *
from pandas.core.frame import DataFrame
from typing import List, Dict
import numpy as np
from collections import deque
# endregion

class WeightedFrequencyofLosses(QCAlgorithm):

def Initialize(self) -> None:
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)  

self.market:Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.tickers_to_ignore:List[str] = ['CDLB']

self.weight:Dict[Symbol, float] = {}
self.data:Dict[Symbol, deque] = {}

self.r:float = 0.13
self.quantile:int = 10
self.leverage:int = 5
self.period = 30

self.fundamental_count:int = 3000
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.Settings.MinimumOrderMarginPortfolioPercentage = 0
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.AfterMarketOpen(self.market), self.Selection)

def OnSecuritiesChanged(self, changes:SecurityChanges) -> None:
for security in changes.AddedSecurities:
    security.SetLeverage(self.leverage)
    security.SetFeeModel(CustomFeeModel())

def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# update the rolling window every day
for stock in fundamental:
    symbol:Symbol = stock.Symbol

    # store monthly price
    if symbol in self.data:
        self.data[symbol].append((self.Time, stock.AdjustedPrice))

if not self.selection_flag:
    return Universe.Unchanged

selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Symbol.Value not in self.tickers_to_ignore and x.MarketCap != 0 and \
        (x.SecurityReference.ExchangeId == 'NYS') or (x.SecurityReference.ExchangeId == 'NAS') or (x.SecurityReference.ExchangeId == 'ASE')]
# selected:List[Fundamental] = [x
#     for x in sorted([x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Symbol.Value not in self.tickers_to_ignore],
#         key = lambda x: x.DollarVolume, reverse = True)[:self.fundamental_count]]

# warmup price rolling windows
for stock in selected:
    symbol:Symbol = stock.Symbol
    if symbol in self.data:
        continue
    
    self.data[symbol] = deque(maxlen=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].append((time, close))

# selected = [x for x in selected if x.MarketCap != 0 and \
#         (x.SecurityReference.ExchangeId == 'NYS') or (x.SecurityReference.ExchangeId == 'NAS') or (x.SecurityReference.ExchangeId == 'ASE')]

if len(selected) > self.fundamental_count:
    selected = sorted(selected, key=lambda x: x.MarketCap, reverse=True)[:self.fundamental_count]

selected:Dict[Symbol, Fundamental] = {x.Symbol: x for x in selected if len(self.data[x.Symbol]) == self.data[x.Symbol].maxlen}

if len(selected) != 0:
    # create dataframe from saved prices
    selected_stocks:Dict[Symbol, List[float]] = {symbol: [i[1] for i in value] for symbol, value in self.data.items() if symbol in selected.keys()}
    df_stocks:DataFrame = pd.DataFrame(selected_stocks, index=[i[0] for i in list(self.data.values())[0]])
    
    # trim dataframe to most recent month period
    last_month_start:datetime.date = (self.Time.date() - timedelta(self.Time.day + 1)).replace(day=1)
    df_stocks = df_stocks.pct_change()
    df_stocks = df_stocks[df_stocks.index.date >= last_month_start]
    
    # indicator function
    df_stocks[df_stocks >= 0.] = 0.
    df_stocks[df_stocks < 0.] = 1.
    
    # weighted frequency
    weights:np.ndarray = np.array([np.exp(self.r * (j - 1)) for j in range(1, len(df_stocks) + 1)])
    weights = weights / sum(weights)

    df_stocks = df_stocks.mul(weights, axis=0)
    df_stocks = df_stocks.sum(axis=0)

    VFL:Dict[Symbol, float] = df_stocks.to_dict()

    # sort and divide to upper decile and lower decile
    if len(VFL) >= self.quantile:
        sorted_VFL:List[Symbol] = sorted(VFL, key=VFL.get, reverse=True)
        quantile:int = int(len(sorted_VFL) / self.quantile)
        long:List[Symbol] = sorted_VFL[:quantile]
        short:List[Symbol] = sorted_VFL[-quantile:]

        # calculate weights based on marketcap
        for i, portfolio in enumerate([long, short]):
            mc_sum:float = sum([selected[x].MarketCap for x in portfolio])
            for symbol in portfolio:
                self.weight[symbol] = ((-1) ** i) * (selected[symbol].MarketCap / mc_sum)

return list(self.weight.keys())

def OnData(self, data: Slice) -> None:
# monthly rebalance
if not self.selection_flag:
    return
self.selection_flag = False

# trade execution
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)

self.weight.clear()

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

# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))