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.
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.
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:
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).
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.
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.
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:
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).
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.
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=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)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:
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).
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=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"
}
]
}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": []
}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
}lib/market-data/prices.ts).