Mean Reversion Strategy with a Seasonal Filter
Buys SPY only on a Monday that extends a two-day pullback, then exits on the first close above the prior day's high — a Monday seasonal filter on a mean-reversion trigger.
Performance
Real internal-backtest statistics from the registry — Internal backtest · SPY daily bars (Yahoo Finance) · 308 trades · 1993–2026.
CAGR and exposure are not published for the library yet — we show “—” rather than invent them. Max drawdown, best year and worst year are undefined for the pooled Momentum universe backtest. Win rate is a share of trades, average return is per trade, and holding period is in trading days.
Historical pattern
Mean Reversion Strategy with a Seasonal Filter — average trade trajectory (entry → exit)
Percent of the trade's holding period (0% = entry, 100% = close crosses above the prior day's high) · Average cumulative return (%). Empirical average trade path computed across ALL 308 completed backtest windows on real SPY daily closes. The holding period is variable (1–16 sessions), so each trade is normalized to its own duration (0–100%) before averaging; every point therefore uses the same 308 trades (no survivorship bias) and the 100% value equals the mean per-trade return. Labelled basis:"derived" (computed from our own backtest); the frozen v1 enum has no "empirical" value.
The Mean Reversion Strategy with a Seasonal Filter fuses a calendar filter (the Monday effect) with a short-term mean-reversion trigger (a two-day down sequence). Both must be present:
- BUY at today's close when all of:
1. today is Monday (the seasonal filter), 2. today's close is lower than Friday's close (Monday weakness), and 3. Friday's close was lower than Thursday's close (prior weakness).
- SELL at today's close when today's close is higher than yesterday's high.
That's the entire strategy. It runs on SPY as a market-timing signal (/api/strategies) and, via the scanner, on individual liquid names (/api/scan). The production parameter is the seasonal weekday (Monday); the engine keeps it configurable internally, and the public strategy exposes the Monday default.
How "Friday" and "Thursday" are read. The seasonal gate keys off today's actual weekday (
getUTCDay() === 1). "Friday" is the most recent prior trading session and "Thursday" the session before that — on a normal week these are literally Friday and Thursday. The three conditions therefore require today (Monday) to sit at the bottom of a declining three-session sequence (Thursday → Friday → Monday), each close lower than the last.
Why the Monday effect exists
The Monday effect (a strand of the "weekend effect" / day-of-the-week anomaly) is one of the oldest documented calendar patterns in equities: average returns on Mondays have historically skewed weaker than other weekdays. The usual explanations are structural rather than magical:
- Weekend information flow. Bad news is disproportionately released after Friday's close (companies and regulators favour Friday evenings), so it is first priced on Monday.
- Settlement and financing effects. Historical settlement conventions meant buyers effectively financed positions over the weekend, nudging early-week pricing.
- Behavioural / positioning patterns. Risk-reduction into the weekend and slower re-engagement of participants early in the week leave Mondays thinner and more prone to follow-through weakness.
The effect has weakened over the decades as it became well known — which is exactly why this strategy does not trade Mondays blindly.
Why combining seasonality with mean reversion improves selectivity
A plain "buy every weak Monday" rule fires far too often and captures a diluted, fading edge. Requiring two consecutive lower closes into that Monday turns the seasonal soft spot into a confirmation filter:
- It demands the market already be pulling back. Monday's weakness must extend an existing down sequence (Friday already closed below Thursday), so the strategy only acts when a genuine short-term oversold condition and a seasonally weak day coincide.
- It sharply cuts the trade count. Two independent conditions (a specific weekday and a specific price sequence) intersect rarely, so each signal is far more selective than either alone — the strategy sits in cash ~88% of the time.
- It stacks two unrelated edges. A calendar anomaly and a mean-reversion tendency have different drivers; when both point the same way, the combined setup is more discriminating than doubling down on one.
The result is a low-exposure, high-hit-rate entry that buys a confirmed pullback at a seasonally favourable moment, then exits into the first sign of renewed strength.
One pure core (seasonalMrSignalFromBars) backs the live SPY state, the scanner, and the backtest adapter, so the maths lives in exactly one place. It takes an optional params object ({ entryWeekday }); the exported strategy uses the production default (entryWeekday = 1, Monday).
Entry
BUY at today's close when all hold:
1. Today is Monday —
getUTCDay()of today's bar is 1.2. Monday weakness — today's close is lower than Friday's (the prior session's) close.
3. Prior weakness — Friday's close is lower than Thursday's (the session-before-that) close.
Exit
SELL at today's close when today's close is higher than yesterday's high. This is the only exit.
Position management
- No stop loss, no profit target, no time stop, no trailing logic.
stopLoss,firstTarget,riskPercent, andrewardRiskRatioare reported asnull(never invented).entryPriceis the signal-bar close. - Risk is controlled solely by the short holding period and the mean-reversion exit — there is no protection against a market that keeps falling after a weak Monday.
Scanner
The same per-symbol logic runs across a universe via GET /api/scan?strategy=seasonal_mean_reversion, returning each match's signal, a 0–100 confidence score, the passed-rule checklist, and metrics (weekday, close, fridayClose, thursdayClose, prevHigh).
Deterministic, from the shared buildConfidence framework. confidenceScore is a setup-quality measure (0–100), not a probability of profit. All four components the strategy exposes are documented and their maxima sum to 100:
| Component | Max | Rewards | Formula (fraction of max, clamped 0–1) |
|---|---|---|---|
seasonalFilter | 40 | seasonal filter quality — today is the Monday filter day | 1 if today is Monday, else 0 |
setupCompleteness | 30 | overall setup completeness — all three entry conditions align (the BUY fires) | 1 if the BUY fires, else 0 |
mondayWeakness | 15 | Monday weakness — how far today closed below Friday | ((fridayClose − close) / fridayClose) / 0.01 (full at a ≥1% drop) |
previousWeakness | 15 | previous weakness — how far Friday closed below Thursday | ((thursdayClose − fridayClose) / thursdayClose) / 0.01 (full at a ≥1% drop) |
A bare BUY (a Monday completing the setup with tiny drops) scores ≈ 70 (Good) — the seasonal gate (40) plus completeness (30); two deep down legs add up to 100 (Exceptional). On /api/scan the score is populated per match; on /api/strategies it reflects SPY's current reading. Bands: <70 Weak · 70–79 Good · 80–89 Strong · 90–100 Exceptional.
The API returns the labels of the rules that currently pass:
- Today is Monday — the seasonal filter.
- Today's close below Friday's close — Monday weakness.
- Friday's close below Thursday's close — prior weakness.
- All three true → BUY at the signal-bar close.
- No stop, no target — hold until the exit condition.
- Close above yesterday's high → this is the SELL (exit).
- Very high hit-rate, strong expectancy — 79.2% win rate, 3.89 profit factor over 308 real trades / 33 years.
- Two independent edges stacked — a calendar anomaly (Monday effect) confirmed by a mean-reversion sequence, so each signal is highly selective.
- Very low exposure — in the market only ~12% of the time; capital is free for other setups and drawdown stays contained.
- Dead-simple and fully objective — a weekday check and two close comparisons; no indicators, nothing to curve-fit beyond the (fixed) weekday.
- Dual use — the same core powers a SPY timing signal and a full stock scanner.
- No stop — real tail risk. Worst trade −8.89%, max drawdown 11.39%. A weak Monday that begins a real decline is held until price reclaims the prior day's high.
- A fading anomaly. The Monday effect has weakened as it became widely known; the mean-reversion leg mitigates but does not eliminate this decay risk.
- Few signals. High selectivity means a small trade count per year — the edge is real but infrequent, demanding patience.
- No trend filter. It will buy a weak Monday even as a top forms; the setup assumes a mean-reverting context it does not verify.
- Close-to-close model — no commissions/slippage; fills at the signal-bar close.
Choppy or gently rising markets that dip into Monday and bounce, where the two-day pullback is bought back within a few sessions. Range-bound years with frequent shallow pullbacks are ideal (2002, the best year at +33.7%, came amid exactly that kind of volatile, mean-reverting tape).
Sustained downtrends and trending selloffs, where a weak Monday extending a pullback is the start of a real leg down and price is slow to reclaim the prior day's high. In those regimes the position sits in the red until the exit finally fires (2018, the worst year at −4.7%, is the archetype).
- Static snapshot.
historical.*and the pattern are baked as of 2026; the live/api/backtestalways recomputes from current data. - Single-vendor, close-to-close. Yahoo prices; SPY history from 1993; no costs modelled.
- Weekday semantics. "Friday"/"Thursday" are the two prior trading sessions; around holidays the prior session may not literally be Friday, but the rule's intent (a declining three-session sequence into Monday) is preserved.
- Pattern is normalized-time, not raw sessions (a deliberate, documented choice to avoid survivorship bias for a variable-holding strategy).
- Backtested on SPY. The scanner applies the same rules per-symbol; the published statistics are the SPY single-symbol backtest.
- No optimization. The Monday weekday and the close-comparison rules are fixed by design; the numbers above are not the product of a parameter search.
API examples
Real responses from the live endpoints for this strategy.
Live strategy
GET /api/strategies?id=seasonal_mean_reversion{
"strategies": [
{
"id": "seasonal_mean_reversion",
"name": "Mean Reversion Strategy with a Seasonal Filter",
"category": "Seasonality",
"asset": "SPY",
"description": "WHAT IT IS: The Mean Reversion Strategy with a Seasonal Filter is a HYBRID setup…",
"historical": {
"yearsTested": 33,
"winRate": 0.7922,
"averageReturn": 0.85,
"bestYear": 33.69,
"worstYear": -4.71,
"maxDrawdown": 11.39,
"averageHoldingPeriod": 3,
"profitFactor": 3.89,
"totalTrades": 308,
"source": "Internal backtest · SPY daily bars (Yahoo Finance) · 308 trades · 1993–2026"
},
"historicalPattern": {
"title": "Mean Reversion Strategy with a Seasonal Filter — average trade trajectory (entry → exit)",
"xLabel": "Percent of the trade's holding period (0% = entry, 100% = close crosses above the prior day's high)",
"yLabel": "Average cumulative return (%)",
"basis": "derived",
"source": "Internal backtest — 308 completed SPY trades (Yahoo Finance daily closes), 1993–2026",
"note": "Empirical average trade path… each trade normalized to its own duration so every point averages the same 308 trades (no survivorship bias); the 100% value equals the mean per-trade return.",
"series": [
{ "day": 0, "label": "Entry", "cumulativeReturn": 0.0 },
{ "day": 10, "label": "10%", "cumulativeReturn": 0.0 },
{ "day": 20, "label": "20%", "cumulativeReturn": -0.05 },
{ "day": 30, "label": "30%", "cumulativeReturn": -0.1 },
{ "day": 40, "label": "40%", "cumulativeReturn": -0.15 },
{ "day": 50, "label": "50%", "cumulativeReturn": -0.23 },
{ "day": 60, "label": "60%", "cumulativeReturn": -0.25 },
{ "day": 70, "label": "70%", "cumulativeReturn": -0.17 },
{ "day": 80, "label": "80%", "cumulativeReturn": 0.03 },
{ "day": 90, "label": "90%", "cumulativeReturn": 0.39 },
{ "day": 100, "label": "Exit", "cumulativeReturn": 0.85 }
]
},
"status": "Upcoming",
"entryDate": "",
"exitDate": "",
"daysUntilEntry": 0,
"entryPrice": 583.12,
"stopLoss": null,
"firstTarget": null,
"riskPercent": null,
"rewardRiskRatio": null,
"confidenceScore": 40,
"confidenceBreakdown": { "seasonalFilter": 40, "setupCompleteness": 0, "mondayWeakness": 0, "previousWeakness": 0 },
"explanation": "SPY does not currently trigger the Seasonal Mean Reversion setup — Monday did not close below Friday…",
"checklist": ["Today is Monday (the seasonal filter)"],
"score": 75,
"recommended": true,
"weight": "medium"
}
]
}Scanner
GET /api/scan?strategy=seasonal_mean_reversion{
"strategy": "seasonal_mean_reversion",
"supported": true,
"scanned": 500,
"matches": [
{
"symbol": "XYZ",
"signal": "BUY",
"score": 90,
"reasons": { "isMonday": true, "closeBelowFriday": true, "fridayBelowThursday": true, "closeAbovePriorHigh": false },
"metrics": { "weekday": 1, "close": 150.10, "fridayClose": 151.90, "thursdayClose": 153.20, "prevHigh": 152.60 },
"entryPrice": 150.10,
"stopLoss": null,
"firstTarget": null,
"riskPercent": null,
"rewardRiskRatio": null,
"confidenceScore": 96,
"confidenceBreakdown": { "seasonalFilter": 40, "setupCompleteness": 30, "mondayWeakness": 15, "previousWeakness": 11 },
"explanation": "XYZ rates 96/100 — an exceptional setup by the Mean Reversion Strategy with a Seasonal Filter rules…",
"checklist": [
"Today is Monday (the seasonal filter)",
"Today's close below Friday's close (Monday weakness)",
"Friday's close below Thursday's close (prior weakness)"
]
}
],
"skipped": []
}Backtest
GET /api/backtest?strategy=seasonal_mean_reversion{
"strategy": "seasonal_mean_reversion",
"symbol": "SPY",
"range": { "requestedFrom": null, "requestedTo": null, "testedFrom": "1993-01-29", "testedTo": "2026-08-05", "barsTested": 8436 },
"metrics": {
"totalTrades": 308, "winningTrades": 244, "losingTrades": 63,
"winRate": 0.7922, "averageReturn": 0.0085, "medianReturn": 0.0077,
"bestTrade": 0.1169, "worstTrade": -0.0889, "averageHoldingPeriod": 3.29,
"profitFactor": 3.8947, "totalReturn": 11.9385, "maxDrawdown": 0.1139
},
"sampleTrades": [
{ "entryDate": "2026-07-27", "exitDate": "2026-07-31", "entryPrice": 552.10, "exitPrice": 558.40, "return": 0.0114, "holdingDays": 4 }
],
"openTradeEntryDate": null
}References
- French, K. R. (1980). "Stock returns and the weekend effect." Journal of Financial Economics — the seminal day-of-the-week / Monday effect study.
- Gibbons, M. & Hess, P. (1981). "Day of the Week Effects and Asset Returns." Journal of Business.
- Larry Connors & Cesar Alvarez, Short Term Trading Strategies That Work — short-horizon mean-reversion systems that buy weakness and exit on strength (the mean-reversion lineage of this setup).
- Backtest data: SPY daily OHLCV via Yahoo Finance, 1993–2026 (
lib/market-data/prices.ts).