Pullback Strategy for the S&P 500
Buys a new 10-day high that closes weak (IBS < 0.30 — sold into the high), then exits on the first close above the prior day's high. A pullback into strength.
Performance
Real internal-backtest statistics from the registry — Internal backtest · SPY daily bars (Yahoo Finance) · 318 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
Pullback Strategy for the S&P 500 — 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 318 completed backtest windows on real SPY daily closes. The holding period is variable (1–19 sessions), so each trade is normalized to its own duration (0–100%) before averaging; every point therefore uses the same 318 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 Pullback Strategy for the S&P 500 reduces "buy the dip inside strength" to two objective conditions and one objective exit:
- BUY at today's close when both:
1. today makes a new 10-day high (today's high tops the highest high of the prior 10 sessions), and 2. today's IBS < 0.30 (the close sits in the bottom third of the day's range — the session was sold into).
- 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 parameters are a 10-day high window and a 0.30 IBS threshold; the engine keeps them configurable internally, and the public strategy exposes these defaults.
Market rationale
A market pushing to a new 10-day high is, by definition, in demand — buyers have taken price above everything the prior two weeks printed. But a weak close on that same day (a low IBS) means the demand was absorbed intraday: the day was sold into and finished near its low. That combination — new high, weak close — is a one-day pullback inside an up-move, not a breakdown. Historically, in a rising market these intraday shakeouts resolve back up: price soon closes above the prior day's high again, and the strategy exits into that renewed strength.
Why pullbacks into strength work
Chasing a strong close means paying up after the move; buying a weak close on a new-high day does the opposite — it enters strength on weakness:
- Demand is already proven. The new 10-day high shows the instrument is being accumulated over a meaningful window, so you are buying a pullback within an uptrend, where mean reversion works with the trend rather than against it.
- The entry is a discount. A close in the bottom 30% of the range is a better price than the day's average — you are systematically buying the intraday flush, not the euphoria.
- The exit is objective and quick. Requiring a close above the prior day's high waits for buyers to reassert control, then banks the snap-back. Most trades resolve in a few sessions (see Historical performance), which limits how long capital is exposed to a setup that has failed.
The edge is the recurring tendency of shakeouts inside strength to be bought back — captured mechanically, with no discretion.
One pure core (pullbackSignalFromBars) 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 ({ highLookback, ibsThreshold }); the exported strategy uses the production defaults (highLookback = 10, ibsThreshold = 0.30).
Entry
BUY at today's close when both hold:
1. New 10-day high — today's high is strictly greater than the highest high of the previous 10 sessions.
2. Weak close — today's IBS < 0.30.
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 new-high close.
Scanner
The same per-symbol logic runs across a universe via GET /api/scan?strategy=pullback_sp500, returning each match's signal, a 0–100 confidence score, the passed-rule checklist, and metrics (ibs, priorHigh, prevHigh, high, close).
Internal Bar Strength locates the close inside the day's range:
IBS = (Close − Low) / (High − Low)- IBS = 0 → closed exactly on the low (maximum weakness).
- IBS = 1 → closed exactly on the high (maximum strength).
- IBS = 0.5 → closed mid-range.
The entry requires IBS < 0.30 — the close is in the bottom third of the range, i.e. the day was sold into. This is the objective definition of the "weak close" that turns a plain new-high day into a pullback setup. A zero-range day (High === Low) has an undefined IBS; the implementation returns 0.5 (neutral) for it, so such a bar can never trigger the weak-close entry.
Deterministic, from the shared buildConfidence framework. confidenceScore is a setup-quality measure (0–100), not a probability of profit. Every component is documented and its maxima sum to 100:
| Component | Max | Rewards | Formula (fraction of max, clamped 0–1) |
|---|---|---|---|
entrySetup | 70 | the required condition — a new 10-day high and IBS < 0.30 | 1 if the BUY fires, else 0 |
breakoutQuality | 12 | quality of the breakout — how far today's high extends above the prior 10-day high | ((high − priorHigh) / priorHigh) / 0.02 (full at a ≥2% breakout) |
ibsDepth | 12 | IBS depth — how far below the 0.30 threshold the close sits | (0.30 − IBS) / 0.30 (full at IBS 0, closed on the low) |
distanceFromHigh | 6 | distance from the 10-day high — how far the close pulled back below today's high | ((high − close) / high) / 0.02 (full at ≥2% below the high) |
A bare BUY (a marginal breakout with an IBS right under 0.30) scores ≈ 70 (Good); a strong breakout with a deeply-sold close → 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:
- New 10-day high — today's high tops the prior 10 sessions.
- IBS below 0.30 — weak close, sold into the high.
- Both 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).
- High hit-rate, positive expectancy — 72.0% win rate, 2.29 profit factor over 318 real trades / 33 years.
- Buys strength on weakness — enters a proven-demand instrument at an intraday discount instead of chasing a strong close.
- Dead-simple and fully objective — one range metric (IBS) and two price conditions; no discretion, nothing to curve-fit beyond two parameters.
- Low exposure — in the market only ~14% of the time, so capital is free for other setups and drawdown stays shallow.
- Dual use — the same core powers a SPY timing signal and a full stock scanner.
- No stop — real tail risk. Worst trade −7.56%. A weak new-high close that turns into a genuine reversal is held until price reclaims the prior day's high, which may take a sustained drawdown first.
- No trend filter. It will buy a new-high-then-weak-close day even as a top forms; the setup assumes an up-market context it does not verify.
- Modest CAGR. Low exposure means low compounding (~3.9%); it is an additive, selective edge, not a standalone equity strategy.
- High win rate masks the loss shape — the occasional −5% to −7% trade must be respected in sizing.
- Close-to-close model — no commissions/slippage; fills at the signal-bar close.
Rising or gently trending markets that make frequent new highs and shake out intraday before resuming. Choppy uptrends are ideal: new highs are plentiful, weak closes are bought back quickly, and the exit triggers within a few sessions (2020, the best year at +24.6%, is the archetype).
Sustained downtrends and sharp reversals from a high, where a weak close after a new high is the start of a real turn and price does not promptly 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 −5.3%, 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.
- 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 10-day window and 0.30 IBS threshold 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=pullback_sp500{
"strategies": [
{
"id": "pullback_sp500",
"name": "Pullback Strategy for the S&P 500",
"category": "Technical",
"asset": "SPY",
"description": "WHAT IT IS: The Pullback Strategy for the S&P 500 is a pure 'pullback into strength' setup…",
"historical": {
"yearsTested": 33,
"winRate": 0.7201,
"averageReturn": 0.41,
"bestYear": 24.59,
"worstYear": -5.31,
"maxDrawdown": 7.56,
"averageHoldingPeriod": 4,
"profitFactor": 2.29,
"totalTrades": 318,
"source": "Internal backtest · SPY daily bars (Yahoo Finance) · 318 trades · 1993–2026"
},
"historicalPattern": {
"title": "Pullback Strategy for the S&P 500 — 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 — 318 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 318 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.03 },
{ "day": 20, "label": "20%", "cumulativeReturn": -0.09 },
{ "day": 30, "label": "30%", "cumulativeReturn": -0.18 },
{ "day": 40, "label": "40%", "cumulativeReturn": -0.24 },
{ "day": 50, "label": "50%", "cumulativeReturn": -0.31 },
{ "day": 60, "label": "60%", "cumulativeReturn": -0.32 },
{ "day": 70, "label": "70%", "cumulativeReturn": -0.3 },
{ "day": 80, "label": "80%", "cumulativeReturn": -0.21 },
{ "day": 90, "label": "90%", "cumulativeReturn": 0.02 },
{ "day": 100, "label": "Exit", "cumulativeReturn": 0.41 }
]
},
"status": "Upcoming",
"entryDate": "",
"exitDate": "",
"daysUntilEntry": 0,
"entryPrice": 583.12,
"stopLoss": null,
"firstTarget": null,
"riskPercent": null,
"rewardRiskRatio": null,
"confidenceScore": 6,
"confidenceBreakdown": { "entrySetup": 0, "breakoutQuality": 0, "ibsDepth": 0, "distanceFromHigh": 6 },
"explanation": "SPY does not currently trigger the Pullback Strategy — no new 10-day high today…",
"checklist": [],
"score": 75,
"recommended": true,
"weight": "medium"
}
]
}Scanner
GET /api/scan?strategy=pullback_sp500{
"strategy": "pullback_sp500",
"supported": true,
"scanned": 500,
"matches": [
{
"symbol": "XYZ",
"signal": "BUY",
"score": 92,
"reasons": { "newTenDayHigh": true, "ibsBelowThreshold": true, "closeAbovePriorHigh": false },
"metrics": { "ibs": 0.12, "priorHigh": 149.80, "prevHigh": 149.80, "high": 152.60, "close": 150.10 },
"entryPrice": 150.10,
"stopLoss": null,
"firstTarget": null,
"riskPercent": null,
"rewardRiskRatio": null,
"confidenceScore": 95,
"confidenceBreakdown": { "entrySetup": 70, "breakoutQuality": 12, "ibsDepth": 8, "distanceFromHigh": 5 },
"explanation": "XYZ rates 95/100 — an exceptional setup by the Pullback Strategy for the S&P 500 rules…",
"checklist": [
"New 10-day high (today's high tops the prior 10 sessions)",
"IBS below 0.3 (weak close — sold into the high)"
]
}
],
"skipped": []
}Backtest
GET /api/backtest?strategy=pullback_sp500{
"strategy": "pullback_sp500",
"symbol": "SPY",
"range": { "requestedFrom": null, "requestedTo": null, "testedFrom": "1993-01-29", "testedTo": "2026-08-05", "barsTested": 8436 },
"metrics": {
"totalTrades": 318, "winningTrades": 229, "losingTrades": 87,
"winRate": 0.7201, "averageReturn": 0.0041, "medianReturn": 0.0057,
"bestTrade": 0.0493, "worstTrade": -0.0756, "averageHoldingPeriod": 3.72,
"profitFactor": 2.2897, "totalReturn": 2.6089, "maxDrawdown": 0.0756
},
"sampleTrades": [
{ "entryDate": "2026-04-24", "exitDate": "2026-04-29", "entryPrice": 545.10, "exitPrice": 552.30, "return": 0.0132, "holdingDays": 3 }
],
"openTradeEntryDate": null
}References
- Larry Connors & Cesar Alvarez, Short Term Trading Strategies That Work — IBS and short-horizon "buy the weak close in an uptrend" mean-reversion systems (the lineage of this setup).
- Nick Radge and others on the Internal Bar Strength (IBS) filter for index mean reversion.
- Backtest data: SPY daily OHLCV via Yahoo Finance, 1993–2026 (
lib/market-data/prices.ts).