Moving Averages Distance Strategy in Equities
Log in to collectAcademic paper
The Predictability of Equity Returns from Past Returns: A New Moving Average-Based Perspective
Doron Avramov; Guy Kaplanski; Avanidhar Subrahmanyam
- ILReichman University
- ?Interdisciplinary Center (IDC) Herzliyah
- ILBar-Ilan University
- ?Bar-Ilan University - Graduate School of Business Administration
- University of California, Los Angeles
- Research Network (United States)
- ?Financial Research Network (FIRN)
- ?University of California, Los Angeles (UCLA) - Finance Area
Strategy in a nutshell
Trades U.S. stocks using the Moving Average Deviation (MAD) ratio (MA21 ÷ MA200). Long when MAD ≥ 1.2, short when MAD ≤ 0.8. Equally weighted, rebalanced monthly, capturing short-term deviations from long-term trends.
Economic rationale
Profits arise from investor anchoring and underreaction to new information. The MAD strategy exploits this behavioral bias, showing persistent profitability even after controlling for momentum, earnings revisions, and trading costs.
Backtest performance
Annualised return8.35%
Volatility18.31%
Beta-0.452
Sharpe ratio0.46
Sortino ratio0.36
Win rate60%
Full Python code
import numpy as npfrom AlgorithmImports import *from typing import List, Dictclass MovingAveragesDistance(QCAlgorithm): def Initialize(self): self.SetStartDate(2000, 1, 1) self.SetCash(100000) self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE'] self.fundamental_sorting_key = lambda x: x.DollarVolume self.fundamental_count:int = 500 self.min_share_price:int = 5 self.leverage:int = 10 self.period:int = 200 self.month_period:int = 21 self.data:Dict[Symbol, SymbolData] = {} self.long:List[Symbol] = [] self.short:List[Symbol] = [] self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol self.selection_flag:bool = False self.Settings.MinimumOrderMarginPortfolioPercentage = 0. self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.FundamentalSelectionFunction) self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection) self.settings.daily_precise_end_time = False def OnSecuritiesChanged(self, changes: SecurityChanges) -> None: for security in changes.AddedSecurities: security.SetFeeModel(CustomFeeModel()) security.SetLeverage(self.leverage) def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]: # Update the rolling window every day. for stock in fundamental: symbol = stock.Symbol # Store daily price. if symbol in self.data: self.data[symbol].update(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.Price > self.min_share_price \ and x.SecurityReference.ExchangeId in self.exchange_codes ] if len(selected) > self.fundamental_count: selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]] MAD:Dict[Symbol, float] = {} # Warmup price rolling windows. for stock in selected: symbol:Symbol = stock.Symbol if symbol not in self.data: self.data[symbol] = SymbolData(symbol, 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:Series = history.loc[symbol].close for time, close in closes.items(): self.data[symbol].update(close) if not self.data[symbol].is_ready(): continue prices:List[float] = self.data[symbol].return_prices() ma21:float = np.average(prices[:self.month_period]) ma200:float = np.average(prices) MAD[symbol] = ma21 / ma200 self.long = [x[0] for x in MAD.items() if x[1] >= 1.2] self.short = [x[0] for x in MAD.items() if x[1] <= 0.8] return self.long + self.short def OnData(self, data: Slice) -> None: if not self.selection_flag: return self.selection_flag = False # order execution targets:List[PortfolioTarget] = [] for i, portfolio in enumerate([self.long, self.short]): for symbol in portfolio: if symbol in data and data[symbol]: targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio))) self.SetHoldings(targets, True) self.long.clear() self.short.clear() def Selection(self) -> None: self.selection_flag = True class SymbolData(): def __init__(self, symbol:Symbol, period:int): self.Symbol:Symbol = symbol self.Prices:RollingWindow = RollingWindow[float](period) def update(self, price:float): self.Prices.Add(price) def is_ready(self) -> bool: return self.Prices.IsReady def return_prices(self) -> List[float]: return [x for x in self.Prices] class CustomFeeModel(FeeModel): def GetOrderFee(self, parameters): fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005 return OrderFee(CashAmount(fee, "USD"))