Across 7 symbols and 882 strict gaps over ten years, roughly four in five gaps eventually fill. The data vindicates the folk wisdom. But a naive strategy that trades the signal loses money — Sharpe of -0.61. This page walks through why.
A strict gap up on day t is when the day's low is
higher than the previous day's high — price opened above yesterday's
entire range and never came back down during the session. A
strict gap down is the mirror image.
The gap is filled the first time price subsequently trades back through the prior bar's extreme (prior high for a gap up, prior low for a gap down). We measure days to fill for every gap in the universe and ask: what fraction fill within 1, 5, 20, 60 trading days?
Universe: 7 symbols over 2016–2026 (10 years). 882 strict gaps total, filtered to ≥ 0.5% of prior close.
The bar on the left (1 day) is the informative one. Roughly one-in-six gaps fill the very next session. By five days the number rises to one-in-two. By three months, four-in-five. Given enough patience, the gap usually closes.
The thesis is mostly symmetric, with one interesting asymmetry: gap-downs fill faster than gap-ups. Fear closes quicker than greed. And unsurprisingly, small gaps fill fastest — a 0.5–1% gap usually closes within a week.
Read the heatmap as: "of every gap in this size bucket, what fraction closed within this horizon?" Notice the top row — large gaps (≥5%, often news- or earnings-driven) only reach a 36% fill rate even at 60 days. That's a first hint that the gaps that are easy to trade aren't the same gaps that fill.
The naive translation of the thesis: fade every qualifying gap at the next open. Short the gap-ups, long the gap-downs, target the prior extreme (the gap-fill level), 2% stop-loss, 20-day time stop. Each trade a fixed $10K notional. No portfolio constraints, just stack every independent trade in parallel.
Three structural reasons, each visible in the numbers below.
The target is the gap fill — for the median gap, that's about 1.08% from the gap-day close. The stop-loss is fixed at 2.00%. So before you even place the trade, the nominal reward:risk is 0.54:1 — you're risking roughly twice what you stand to make. A setup like that demands a high hit rate just to break even. The signal only delivers 37.0%.
Per trade, your winners are actually bigger than your losers. Targets, when they hit, pay +2.50% on average. Stops, when they hit, cost -1.84%. If you hit each equally often, you'd print money.
But stops hit 60.6% of the time and targets only 39.2%. Why? Because the thesis promises that eventually price trades back to the prior extreme — it says nothing about how violently the path gets there. A 2% stop is narrower than the typical run-up that precedes a gap-fill. The gap will close — you'll just have been stopped out three days ago.
This is the most tractable of the three problems. Widening the stop (or scaling it to realized volatility) lets the thesis actually play out. The tradeoff is larger tail losses on the gaps that don't fill — which puts the spotlight on problem #3.
A "2% stop" only holds if the market gives you the chance to exit at your stop price. When a gap-up is followed by another overnight gap-up on fresh earnings news, the next session opens past your stop and you exit at the open — which might be 5%, 8%, or more against you. The five worst trades below all blew through the nominal stop. Each one of them takes four or five clean target-hits to offset.
This is the tail that a mean-return summary hides: the distribution is close to symmetric around the middle but the left wing is fatter than the right, and you're living there a few times a year.
| Symbol | Side | Entry | Exit | Reason | Return |
|---|
Cumulative net PnL across every trade, stamped at its exit date. Each trade is independent fixed notional, so this is additive PnL rather than a compounding portfolio curve. The 2020 drawdown is the COVID earnings-gap cluster — many large gaps that didn't revert quickly.
The thesis is real; the execution is leaky. Each leak is a tunable parameter in this repo — no new code required, just a new YAML:
Adding a new strategy is a ten-line subclass of
Strategy — the backtester and this dashboard pick it
up automatically.
| Symbol | Date | Dir | Gap % | Gap $ | Prev Close | Target | Days to fill |
|---|
| Symbol | Side | Entry | Entry $ | Exit | Exit $ | Reason | Bars | Return |
|---|