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.
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.
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:
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).
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.
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:
The effect has weakened over the decades as it became well known — which is exactly why this strategy does not trade Mondays blindly.
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:
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).
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.
SELL at today's close when today's close is higher than yesterday's high. This is the only exit.
stopLoss, firstTarget, riskPercent, and rewardRiskRatio are reported as null (never invented). entryPrice is the signal-bar close.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:
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).
historical.* and the pattern are baked as of 2026; the live /api/backtest always recomputes from current data.Real responses from the live endpoints for this 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"
}
]
}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": []
}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
}lib/market-data/prices.ts).