Quant Buffet放轻松,别过度思虑

股票中的均线距离策略

登录后收藏

学术论文

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

策略概要

投资范围包括在纽约证券交易所、美国证券交易所和纳斯达克上市的美国公司,不包括价格低于5美元的股票、非活跃股票以及缺乏回报观察值或预测所需特征的股票。该策略通过将21天移动平均线(MA21)除以200天移动平均线(MA200)来构建移动平均线偏差(MAD)。MAD ≥ 1.2的股票做多,MAD ≤ 0.8的股票做空。投资组合采用等权重,策略每月重新平衡。这种方法识别出短期价格相对于长期趋势有显著变动的股票。

II. 策略合理性

基于21天移动平均线与200天移动平均线之比的MAD策略,由于投资者锚定偏差导致的反应不足,显示出显著的长期盈利能力。投资者通过锚定200天移动平均线而高估股票价格的回报潜力,导致对好消息或坏消息反应不足。这种效应持续长达两年,即使在控制了其他已知异常(包括动量和盈利修正)后,MAD策略仍然有利可图。该策略的盈利能力不仅来自多头头寸,也来自空头头寸。从2001年到2016年,即使考虑到交易成本,回报仍然显著,使其在各种市场条件下都具有鲁棒性。

回测表现

波动率18.31%
夏普比率0.46
索提诺比率0.36
胜率60%

完整 Python 代码

import numpy as np
from AlgorithmImports import *
from typing import List, Dict
class 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"))