SPY Flush-and-Recovery Backtest

Do prior day levels actually work? 25 years of data, 3 signal types, 6,692 trading days.

← Back to research studies

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.

Connect to Claude →

The prompt

You ask

"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.

Generated SQL
generated SQL
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

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.

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 →