Filtered Short-Term Reversal
Log in to collectAcademic paper
Filtered Market Statistics and Technical Trading Rules
Z. George Yang
- MUFlex (Mauritius)
- ?Flexible Plan Investments, Ltd
Strategy in a nutshell
The strategy trades an S&P 500 instrument (ETF, CFD, or future) based on consecutive daily gains or losses ("Runs"). The investor goes long at market close after two consecutive down days (including the current day) and switches to short after two consecutive up days. A dynamic filter removes noise by excluding days with absolute returns below 20% of the 60-day rolling standard deviation of daily SPX returns. This filter is updated daily to refine the signal calculation and improve trading accuracy. Positions are adjusted dynamically based on the filtered "Runs," capturing short-term market trends effectively.
Economic rationale
The academic paper explains that "nearly flat" days are typically information-light, contributing little to investment outcomes but significantly affecting market cycle predictions in technical rules. This impact is crucial for both short-term trading, where individual days can alter signals, and long-term rules, where "nearly flat" days constitute a large portion of look-back periods. Filtering out such days reduces noise, improves the accuracy of day-counting technical rules (e.g., only "up" or "down" days matter), and minimizes uncertainties in end-of-day trading execution. This approach refines signals, enhances decision-making, and optimizes trading outcomes by focusing on more relevant market movements.
Backtest performance
Full Python code
import numpy as np
from AlgorithmImports import *
class FilteredShortTermReversal(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbol: Symbol = self.AddEquity("SPY", Resolution.Minute).Symbol
# Setup consolidator.
self.spy_onsolidator = TradeBarConsolidator(timedelta(days=1))
self.spy_onsolidator.DataConsolidated += self.DailyData
self.SubscriptionManager.AddConsolidator(self.symbol, self.spy_onsolidator)
# SPY closes.
self.period: int = 61
self.data: RollingWindow = RollingWindow[float](self.period)
# Warmup.
history: DataFrame = self.History(self.symbol, self.period, Resolution.Daily)
if not history.empty:
closes = history.loc[self.symbol].close
for time, close in closes.items():
self.data.Add(close)
self.run_days: RollingWindow = RollingWindow[float](2)
# On daily data.
def DailyData(self, sender, consolidated) -> None:
self.data.Add(consolidated.Close)
if self.data.IsReady:
closes: np.ndarray = np.array([x for x in self.data])
return_data: np.ndarray = closes[:-1] / closes[1:] - 1
ret: float = return_data[0]
if ret >= 0:
self.run_days.Add(1)
else:
self.run_days.Add(0)
if len(return_data) == self.period - 1:
if self.run_days.IsReady:
mean: float = np.mean(return_data)
ret_std: float = np.std(return_data)
run_days: List[float] = [x for x in self.run_days]
# Positive run
if sum(run_days) == 2:
if ret >= mean + ret_std:
if self.Portfolio[self.symbol].IsLong:
self.Liquidate()
self.SetHoldings(self.symbol, -1)
else:
self.Liquidate()
# Negative run
elif sum(run_days) == 0:
if ret <= mean - ret_std:
if self.Portfolio[self.symbol].IsShort:
self.Liquidate()
self.SetHoldings(self.symbol, 1)
else:
self.Liquidate()
else:
self.Liquidate()