{"slug":"do-stock-gaps-get-filled","qid":"fill_by_horizon","label":"Share of 1%+ gaps filled, by how long you wait","post_title":"Do Stock Gaps Always Get Filled? The Data","post_url":"/blog/do-stock-gaps-get-filled#q-fill_by_horizon","columns":["horizon","gaps_measured","filled_pct"],"rows":[{"horizon":"1 session","gaps_measured":2191,"filled_pct":34},{"horizon":"5 sessions","gaps_measured":2191,"filled_pct":65.8},{"horizon":"20 sessions","gaps_measured":2191,"filled_pct":81.1},{"horizon":"60 sessions","gaps_measured":2191,"filled_pct":88.7}],"shape":"ranking","sql":"WITH sessions AS\n(\n    SELECT\n        ticker,\n        toDate(toTimeZone(window_start, 'America/New_York')) AS d,\n        argMin(toFloat64(open), window_start)                AS session_open,\n        argMax(toFloat64(close), window_start)               AS session_close,\n        toFloat64(max(high))                                 AS session_high,\n        toFloat64(min(low))                                  AS session_low\n    FROM global_markets.delayed_stocks_minute_aggs\n    WHERE ticker IN ('AAPL', 'MSFT', 'NVDA', 'AMZN', 'JPM', 'KO', 'WMT', 'XOM')\n      AND window_start >= '2021-01-01'\n      AND window_start <  '2026-07-01'\n      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60\n           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570\n      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60\n           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960\n    GROUP BY ticker, d\n),\npaths AS\n(\n    SELECT\n        d,\n        session_open,\n        session_high,\n        session_low,\n        lagInFrame(session_close) OVER (PARTITION BY ticker ORDER BY d ASC\n            ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)                                                          AS prior_close,\n        min(session_low)  OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING)   AS low_5,\n        max(session_high) OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING)   AS high_5,\n        min(session_low)  OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN CURRENT ROW AND 19 FOLLOWING)  AS low_20,\n        max(session_high) OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN CURRENT ROW AND 19 FOLLOWING)  AS high_20,\n        min(session_low)  OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN CURRENT ROW AND 59 FOLLOWING)  AS low_60,\n        max(session_high) OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN CURRENT ROW AND 59 FOLLOWING)  AS high_60\n    FROM sessions\n),\ngaps AS\n(\n    SELECT\n        session_open > prior_close AS gap_up,\n        prior_close,\n        session_low,\n        session_high,\n        low_5,\n        high_5,\n        low_20,\n        high_20,\n        low_60,\n        high_60\n    FROM paths\n    WHERE prior_close > 0\n      AND d <= toDate('2026-02-28')\n      AND abs(100 * (session_open / prior_close - 1)) >= 1\n),\nflags AS\n(\n    SELECT arrayJoin([\n        (1, '1 session',   toUInt8(if(gap_up, session_low <= prior_close, session_high >= prior_close))),\n        (2, '5 sessions',  toUInt8(if(gap_up, low_5  <= prior_close, high_5  >= prior_close))),\n        (3, '20 sessions', toUInt8(if(gap_up, low_20 <= prior_close, high_20 >= prior_close))),\n        (4, '60 sessions', toUInt8(if(gap_up, low_60 <= prior_close, high_60 >= prior_close)))\n    ]) AS f\n    FROM gaps\n)\nSELECT\n    tupleElement(f, 2)                        AS horizon,\n    count()                                   AS gaps_measured,\n    round(100 * avg(tupleElement(f, 3)), 1)   AS filled_pct\nFROM flags\nGROUP BY horizon\nORDER BY min(tupleElement(f, 1)) ASC","computed_at":"2026-08-06T05:38:12.189253+00:00","elapsed":27.930943335}