Quant BuffetRelax, Not Over Thinking

Filtered Short-Term Reversal

Log in to collect

Academic paper

Filtered Market Statistics and Technical Trading Rules

AuthorsZ. George Yang

Institute
  • 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

Annualised return9.77%
Volatility18.6%
Beta0.113
Sharpe ratio0.31
Sortino ratio-0.03
Maximum drawdown-26.7%
Win rate49%

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()