Idea in brief
Rob Hanna gives an example of the simplest mean reversion idea: buy every 5-day low and sell when price closes above the 5-day average. In Hanna's words, such a system can have about 75% winning trades, but the equity curve goes up, up, and then drops, and the drops can be sharp.
Hanna guards against this with position size and with confirmation from the market. Hanna does not buy pullbacks in stocks if the market has just rocketed and is itself due for a pullback, or if the market is breaking down. The decision comes from the Aggregator indicator, which combines all of Hanna's market research into a forecast for several days. The indicator's composition is not public. Hanna trades such models only on large companies: S&P 500, S&P 100, NASDAQ 100.
In another episode, Laurens Bensdorp describes a similar setup with numbers. The stock must be in a trend, for example above the 150-day EMA. If the S&P 500 closed below its 40-day average, there are no new entries. The exit is often by time: if the stock has not returned to the average within three days, sell. There is always a stop, and it is wide.
The two authors' filters are not mixed in this card. There is one signal from Hanna and two separate market filters for it: A after Hanna in our formalization, and B after Bensdorp with the numbers Bensdorp gives. Stock selection is a separate rule.
Why it might work
Hanna explains mean reversion through the ownership structure. A large company has many institutional shareholders, and during a heavy sell-off some of them buy more at a good price. A small cap has one or two such holders, and if one of them decides to exit, the stock can fall for a long time. Bankruptcy is also less likely in the S&P 500. Hanna explains the market filter this way: when the market is on your side, a rising tide lifts all boats, and stock selection can be less precise.
Bensdorp does not use the market filter to improve the result of an individual system. The starting point is that in a fast decline all of Bensdorp's long systems lose at the same time. The 40-day average filter automatically reduces long exposure when the market is against you, and on a rebound it switches back on earlier than long-term trend filters. This conclusion comes from Bensdorp's own experience: after 2008 Bensdorp believed that long mean reversion makes money in a bear market, and in 2011 learned the opposite.
A caveat: Hanna's 75% is an example of what you might see in a test, not a result Hanna showed. Bensdorp presents the numbers only as examples.
Rules
Signal and exit (Rob Hanna)
// daily bars; a stock from the S&P 500, S&P 100 or NASDAQ 100, or an index ETF
FiveDayLow = Close <= Lowest(Close, 5) // author: buy the 5-day low
// Finetiq: low of closes, current bar included in the window. Variant: Low <= Lowest(Low, 5)
IF FiveDayLow AND MarketOK AND NOT InPosition
BUY AT NEXT BAR OPEN // Finetiq: execution; variant BUY AT CLOSE
IF Close > SMA(Close, 5)
EXIT AT NEXT BAR OPEN // author: price closed above the 5-day average
// the author's simple version has no stop
Market filter A: Hanna (Finetiq formalization)
Hanna names two situations in which Hanna does not buy, but gives no numbers for them. Hanna's Aggregator is not available, so below is a simple substitute.
SPX = close of the S&P 500 (second symbol)
Rocket = SPX >= Highest(SPX, 10) // author: the market has rocketed and is due for a pullback; Finetiq: 10-day high
BreakDown = SPX <= Lowest(SPX, 20) // author: the market is breaking down; Finetiq: 20-day low
MarketOK = NOT Rocket AND NOT BreakDown
// author: the filter only blocks new entries, it does not close open positions
If you trade the index itself, filter A turns into the rule "buy 5-day lows that are not 20-day lows". That is a different system, and it should be tested separately.
Market filter B: Bensdorp (author's numbers)
Bensdorp does not name a pullback trigger. To make the filters comparable, the entry is the same as Hanna's.
MarketOK = SPX > SMA(SPX, 40) // author: S&P 500 above the 40-day average; Finetiq: simple average
StockTrend = Close > EMA(Close, 150) // author: "for example" a 150-day EMA
IF FiveDayLow AND StockTrend AND MarketOK AND NOT InPosition
BUY AT NEXT BAR OPEN // Finetiq: Hanna's trigger
IF Close > SMA(Close, 5) THEN EXIT AT NEXT BAR OPEN // Finetiq: returned to the average = above MA5
IF BarsSinceEntry >= 3 THEN EXIT AT NEXT BAR OPEN // author: did not return within 3 days
StopLoss = EntryPrice * 0.80 // author: always a stop, a wide one, 20% in the author's example
Shares = Equity * 1% / (EntryPrice - StopLoss) // author: size from the distance to the stop; Finetiq: 1% risk
Shares = Min(Shares, Equity * 10% / EntryPrice) // author: cap on the share of capital; Finetiq: 10%
Stock selection (separate rule)
// Hanna: large companies only
UniverseA = S&P 500 members on the signal date // Finetiq: membership on the date, not today's constituents
// Bensdorp: all stocks, including delisted ones,
// with liquidity, price and volatility filters, thresholds not named
UniverseB = US stocks AND AvgVolume > X AND Close > Y AND ATRpct > Z // Finetiq: choose X, Y, Z
Bensdorp explains the volatility filter: with a holding period of 1-5 days, a stock that moves 0.2% a day gives you nothing. On a broad universe some days bring more signals than there are slots in the portfolio. Neither author named a rule for choosing among them for this system.
Parameters
| Parameter | Value | Source |
|---|---|---|
| Signal | close at a 5-day low | author (Hanna) |
| Low basis | closes, current bar in the window | Finetiq |
| Exit | close above SMA 5 | author (Hanna) |
| Universe A | S&P 500, S&P 100, NASDAQ 100, no small caps | author (Hanna) |
| Filter A: market has rocketed | S&P 500 at a 10-day high | Finetiq |
| Filter A: market breaks down | S&P 500 at a 20-day low | Finetiq |
| Filter B: market | S&P 500 above the 40-day average | author (Bensdorp), average type Finetiq |
| Filter B: stock trend | above EMA 150 | author (Bensdorp) |
| B: time exit | 3 days | author (Bensdorp) |
| B: stop | 20% | author (Bensdorp, example) |
| B: risk and position cap | 1% and 10% of capital | Finetiq |
| Holding period | 1-5 days | author (Bensdorp) |
| Entry | next day's open | Finetiq |
What to test
- Is the filter needed. One signal on one universe: no filter, with A, with B. Look at drawdown and the number of simultaneous positions in 2008, 2011, 2020 and 2022. Bensdorp uses the filter to control exposure, so returns alone are not enough for the comparison.
- The 75% claim. The simple version without a filter and a stop on large companies. Besides the win rate, look at the worst losing streak and the depth of the drops.
- Large versus small. The same rules on small caps. Hanna says mean reversion is unreliable there. If there is a difference, it supports Hanna's explanation.
- Stop. No stop, 20% and 10%. Bensdorp expects a stop to lower the win rate and accepts that for the sake of the worst case. Measure what this insurance costs.
- Parameter neighborhood. Low over 4, 5, 7 days. Exit above SMA 3, 5, 7. Filter B: average 30, 40, 50, trend EMA 100, 150, 200. If the result holds only on the original numbers, it is overfitting.
- Low basis. Closes versus bar lows.
- Costs. Holding lasts several days, and a broad universe produces many trades. Commission and spread on market entries noticeably reduce the average trade, so run the test with them.
Platform notes
TradingView (Pine Script)
- A strategy trades only the chart symbol. A portfolio of S&P 500 stocks cannot be assembled in one test: you can check the signal on individual stocks and combine the results outside the platform.
- The S&P 500 is added via
request.securitywith the index ticker and the"D"timeframe. Withoutlookahead_onthere is no future data on history, but check that the bar dates of the stock and the index match. ta.lowest(close, 5)includes the current bar, so the conditionclose <= ta.lowest(close, 5)is exactly the 5-day low of closes.- Pine has no index membership by date. A test on today's index members overstates the result.
MultiCharts and TradeStation (EasyLanguage)
- The index is added as a second data stream:
Close of Data2,Average(Close of Data2, 40). Orders go only toData1. - A portfolio over a universe: Portfolio Maestro in TradeStation, Portfolio Trader in MultiCharts. A fair test needs data that includes delisted stocks, otherwise the result is overstated.
- EMA 150:
XAverage(Close, 150). The average needs a warm-up of 450-600 bars, so discard the first years of history. - Time exit:
If BarsSinceEntry >= 3 then Sell next bar at market. Stop as a price:Sell next bar at EntryPrice * 0.8 stop.
MetaTrader 5 (MQL5)
- Stocks are traded as CFDs, the broker's list of stocks is incomplete, and there is no history of delisted stocks. A test in MT5 almost always contains survivorship bias.
- The index is read via
iCloseon an S&P 500 CFD; the symbol name differs between brokers. A CFD daily bar is built on server time and does not close at 16:00 ET, so the 40-day average will be slightly different. - The tester's multi-symbol mode lets one EA handle several stocks, but all symbols must be in Market Watch.
- Include swap for holding periods of up to five nights in the test.
Where the idea can break
- Neither Hanna nor Bensdorp showed a test of this setup. 75% and 60% are their estimates.
- Negative asymmetry: many small wins and rare sharp drops. A short history without 2008 or 2020 will show a curve that is too smooth.
- Filter A is entirely ours. Hanna's real filter is private and relies on a large body of Hanna's market research.
- Bensdorp calls these numbers (EMA 150, MA 40) examples, and they work for Bensdorp inside a portfolio of dozens of systems with different exits.
- A test on current S&P 500 members without delisted stocks overstates the result, especially when buying falling stocks.
- A version on a single index produces few trades and behaves differently from a portfolio of hundreds of stocks.