Prior-day-high and prior-day-low levels are a staple of day-trading education. The idea is simple: if price dips below yesterday's low and recovers, that is a bullish signal. If price retests yesterday's high as support, momentum should continue. These patterns show up in trading courses, YouTube tutorials, and chart rooms every day. But does the data actually support them?
This study tests three flush-and-recovery signal types on SPY from 2000 to present, measuring forward returns at 1, 5, and 10 days against a baseline of any random SPY trading day. The entire study was run with a single prompt.
The prompt
"Backtest SPY flush-and-recovery at prior day low and prior day high levels from 2000 to present. Test three patterns: price dips below yesterday's low then recovers above it, price retests yesterday's high as support, and price breaks above yesterday's high from below. Show forward returns at 1, 5, and 10 days with win rates compared to any random SPY day."
The generated SQL
Claude generated this query automatically from the prompt above. This is the raw output, unedited.
WITH spy_full AS (
SELECT
sq.date, sq.open, sq.high, sq.low, sq.close,
LAG(sq.high) OVER (ORDER BY sq.date) AS prev_high,
LAG(sq.low) OVER (ORDER BY sq.date) AS prev_low,
LAG(sq.close) OVER (ORDER BY sq.date) AS prev_close,
LEAD(sq.close, 1) OVER (ORDER BY sq.date) AS fwd_1d,
LEAD(sq.close, 5) OVER (ORDER BY sq.date) AS fwd_5d,
LEAD(sq.close, 10) OVER (ORDER BY sq.date) AS fwd_10d
FROM shibui.stock_quotes sq
WHERE sq.symbol = 'SPY.NYSE'
AND sq.date >= '2000-01-01'
),
signals AS (
SELECT *,
ROUND((close - open) / NULLIF(open, 0) * 100, 3) AS intraday_ret,
ROUND((fwd_1d - close) / NULLIF(close, 0) * 100, 3) AS ret_1d,
ROUND((fwd_5d - close) / NULLIF(close, 0) * 100, 3) AS ret_5d,
ROUND((fwd_10d - close) / NULLIF(close, 0) * 100, 3) AS ret_10d,
CASE WHEN low < prev_low AND close > prev_low
THEN 1 ELSE 0 END AS pdl_signal,
CASE WHEN open > prev_high
AND low < prev_high
AND close > prev_high
THEN 1 ELSE 0 END AS pdh_support_signal,
CASE WHEN prev_close < prev_high
AND open < prev_high
AND high >= prev_high
AND close > prev_high
THEN 1 ELSE 0 END AS pdh_breakout_signal
FROM spy_full
WHERE prev_high IS NOT NULL
),
bounds AS (
SELECT
PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY ret_1d) AS p01_1d,
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY ret_1d) AS p99_1d,
PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY ret_5d) AS p01_5d,
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY ret_5d) AS p99_5d,
PERCENTILE_CONT(0.01) WITHIN GROUP (ORDER BY ret_10d) AS p01_10d,
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY ret_10d) AS p99_10d
FROM signals WHERE ret_1d IS NOT NULL
)
SELECT group_label, signal_days,
ROUND(avg_ret_1d, 2) AS avg_ret_1d_pct,
ROUND(win_rate_1d, 1) AS win_rate_1d_pct,
ROUND(avg_ret_5d, 2) AS avg_ret_5d_pct,
ROUND(win_rate_5d, 1) AS win_rate_5d_pct,
ROUND(avg_ret_10d, 2) AS avg_ret_10d_pct,
ROUND(win_rate_10d, 1) AS win_rate_10d_pct
FROM (
SELECT 'Baseline (All SPY Days)' AS group_label,
COUNT(*) AS signal_days,
AVG(GREATEST(LEAST(ret_1d, b.p99_1d), b.p01_1d)) AS avg_ret_1d,
COUNT(*) FILTER (WHERE fwd_1d > close) * 100.0
/ NULLIF(COUNT(fwd_1d), 0) AS win_rate_1d,
AVG(GREATEST(LEAST(ret_5d, b.p99_5d), b.p01_5d)) AS avg_ret_5d,
COUNT(*) FILTER (WHERE fwd_5d > close) * 100.0
/ NULLIF(COUNT(fwd_5d), 0) AS win_rate_5d,
AVG(GREATEST(LEAST(ret_10d, b.p99_10d), b.p01_10d)) AS avg_ret_10d,
COUNT(*) FILTER (WHERE fwd_10d > close) * 100.0
/ NULLIF(COUNT(fwd_10d), 0) AS win_rate_10d
FROM signals CROSS JOIN bounds b
UNION ALL
SELECT 'PDL Flush and Recovery',
COUNT(*) FILTER (WHERE pdl_signal=1),
AVG(GREATEST(LEAST(ret_1d, b.p99_1d), b.p01_1d))
FILTER (WHERE pdl_signal=1),
COUNT(*) FILTER (WHERE pdl_signal=1 AND fwd_1d > close) * 100.0
/ NULLIF(COUNT(fwd_1d) FILTER (WHERE pdl_signal=1), 0),
AVG(GREATEST(LEAST(ret_5d, b.p99_5d), b.p01_5d))
FILTER (WHERE pdl_signal=1),
COUNT(*) FILTER (WHERE pdl_signal=1 AND fwd_5d > close) * 100.0
/ NULLIF(COUNT(fwd_5d) FILTER (WHERE pdl_signal=1), 0),
AVG(GREATEST(LEAST(ret_10d, b.p99_10d), b.p01_10d))
FILTER (WHERE pdl_signal=1),
COUNT(*) FILTER (WHERE pdl_signal=1 AND fwd_10d > close) * 100.0
/ NULLIF(COUNT(fwd_10d) FILTER (WHERE pdl_signal=1), 0)
FROM signals CROSS JOIN bounds b
UNION ALL
SELECT 'PDH Support',
COUNT(*) FILTER (WHERE pdh_support_signal=1),
AVG(GREATEST(LEAST(ret_1d, b.p99_1d), b.p01_1d))
FILTER (WHERE pdh_support_signal=1),
COUNT(*) FILTER (WHERE pdh_support_signal=1 AND fwd_1d > close) * 100.0
/ NULLIF(COUNT(fwd_1d) FILTER (WHERE pdh_support_signal=1), 0),
AVG(GREATEST(LEAST(ret_5d, b.p99_5d), b.p01_5d))
FILTER (WHERE pdh_support_signal=1),
COUNT(*) FILTER (WHERE pdh_support_signal=1 AND fwd_5d > close) * 100.0
/ NULLIF(COUNT(fwd_5d) FILTER (WHERE pdh_support_signal=1), 0),
AVG(GREATEST(LEAST(ret_10d, b.p99_10d), b.p01_10d))
FILTER (WHERE pdh_support_signal=1),
COUNT(*) FILTER (WHERE pdh_support_signal=1 AND fwd_10d > close) * 100.0
/ NULLIF(COUNT(fwd_10d) FILTER (WHERE pdh_support_signal=1), 0)
FROM signals CROSS JOIN bounds b
UNION ALL
SELECT 'PDH Breakout',
COUNT(*) FILTER (WHERE pdh_breakout_signal=1),
AVG(GREATEST(LEAST(ret_1d, b.p99_1d), b.p01_1d))
FILTER (WHERE pdh_breakout_signal=1),
COUNT(*) FILTER (WHERE pdh_breakout_signal=1 AND fwd_1d > close) * 100.0
/ NULLIF(COUNT(fwd_1d) FILTER (WHERE pdh_breakout_signal=1), 0),
AVG(GREATEST(LEAST(ret_5d, b.p99_5d), b.p01_5d))
FILTER (WHERE pdh_breakout_signal=1),
COUNT(*) FILTER (WHERE pdh_breakout_signal=1 AND fwd_5d > close) * 100.0
/ NULLIF(COUNT(fwd_5d) FILTER (WHERE pdh_breakout_signal=1), 0),
AVG(GREATEST(LEAST(ret_10d, b.p99_10d), b.p01_10d))
FILTER (WHERE pdh_breakout_signal=1),
COUNT(*) FILTER (WHERE pdh_breakout_signal=1 AND fwd_10d > close) * 100.0
/ NULLIF(COUNT(fwd_10d) FILTER (WHERE pdh_breakout_signal=1), 0)
FROM signals CROSS JOIN bounds b
) results
LIMIT 10
The results
Ran in 0.1 seconds on 25 years of daily SPY data.
| Signal | Days | Avg 1d | Win% 1d | Avg 5d | Win% 5d | Avg 10d | Win% 10d |
|---|---|---|---|---|---|---|---|
| Baseline (All SPY Days) | 6,692 | +0.03% | 54.1% | +0.17% | 57.4% | +0.34% | 60.0% |
| PDL Flush and Recovery | 1,441 | +0.04% | 54.0% | +0.14% | 57.1% | +0.25% | 58.2% |
| PDH Support | 612 | 0.00% | 53.3% | +0.15% | 57.5% | +0.28% | 62.3% |
| PDH Breakout | 1,071 | -0.02% | 52.7% | +0.01% | 55.5% | +0.14% | 58.7% |
What the results show
None of the three flush-and-recovery patterns produce meaningfully different returns than simply holding SPY on any random day. The prior-day-low recovery, often taught as a high-probability day-trading setup, shows essentially the same 1-day return (+0.04% vs +0.03% baseline) and a marginally lower 10-day win rate (58.2% vs 60.0%).
The prior-day-high tested as support is the most frequently cited of the three, and it does show a slightly higher 10-day win rate (62.3%) than baseline. But the average return is lower (+0.28% vs +0.34%), meaning the wins are smaller. The breakout variant is the weakest: below-baseline returns at every horizon.
The sample sizes are large enough to be meaningful: 1,441 PDL signals and 1,071 breakout signals across 25 years. The returns are winsorized at the 1st and 99th percentiles to prevent outliers from distorting the averages. Prior-day levels are not a statistical edge in SPY. Traders who rely on these patterns are likely attributing random variation to a signal that does not exist.
Methodology
- Data: SPY (S&P 500 ETF) daily OHLC, January 2000 to present
- PDL Flush and Recovery: Intraday low pierces yesterday's low, close recovers above it
- PDH Support: Opens above yesterday's high, dips below it, closes back above
- PDH Breakout: Was below yesterday's high, touches it during the day, closes above
- Winsorization: Returns capped at 1st/99th percentiles to prevent extreme outlier distortion
- Forward returns: Measured from close on signal day
Frequently asked questions
How was this study run?
A single prompt to Claude with the Shibui MCP connector. Claude generated the SQL and returned the results in under 0.1 seconds.
Why test SPY specifically?
SPY is the most liquid equity product in the world. If prior-day levels work anywhere, they should work here. Testing on a single instrument also eliminates cross-sectional noise.
What is winsorization?
Capping extreme values at the 1st and 99th percentiles so that a handful of crash or melt-up days do not dominate the average. It is a standard technique in financial backtesting.
Can I reproduce this?
Yes. Connect Shibui to Claude (free), paste the prompt, and Claude will generate and run the query. Results update daily.
Is this financial advice?
No. This is a statistical study using historical data. Past performance does not predict future results. Consult a licensed advisor before making investment decisions.
Related studies:
Data note: Results shown on this page are from a specific date and will change as new trading days are added. This is a screening tool, not financial advice.
Run your own backtest
Connect Shibui to Claude in 2 minutes. Ask any question about price patterns, technical signals, or market structure and get real data back.
Connect to Claude →