Strasmore Research
Deep Dives Matt ConnorBy Matt Connor · data as of September 27, 2026 · refreshed weekly

What Happens After a Big One-Day Gain

What happens after a big one-day gain? We measured the top 20 gainers in every session for three years: next-day medians, means, gap-and-fade rates.

What happens after a big one-day gain is a base-rate question, and it has a measured answer. Across three years of US sessions, the 20 largest daily gainers among liquid stocks went into the next session with a median close-to-close return near flat, inside a range several percent wide on both sides. This page builds that screen session by session, then reports what the next open and the next close actually did.

Two return measures carry the whole analysis, and they answer different questions. Close-to-close return runs from the gain day's closing price to the next day's closing price, which is what a reader who buys near the bell and holds for one day lives through. Open-to-close return covers only the next session, from its opening print to its closing print, which removes the overnight gap. The distance between the two figures IS the gap. A gainers list shows neither of them.

What happens after a big one-day gain, on average?

Every session in the window, the screen ranks stocks by close-to-close gain and keeps the top 20, requiring a prior close of at least $5 and at least $5 million of dollar volume on the gain day. That liquidity floor matters. Without it, a top-20 list fills with sub-dollar names whose next-day prints say more about a wide bid-ask spread than about follow-through. The panel below groups every one of those observations by how large the initial gain was.

QueryNext-session returns after a big one-day gain, by size of the gain
gain_bucketobservationsmedian_next_cc_pctmean_next_cc_pctmedian_next_oc_pct
under 10%639-0.27-0.15-0.23
10 to 15%2647-0.58-0.18-0.54
15 to 25%5689-1.1-0.64-0.82
25 to 50%4188-2.35-0.5-1.14
50% and up1817-9.66-5.85-2.32
The exact SQL behind every number
WITH bars AS
(
    SELECT
        ticker,
        date,
        max(toFloat64(open))   AS o,
        max(toFloat64(close))  AS c,
        max(toFloat64(volume)) AS vol
    FROM global_markets.stocks_daily_aggs
    WHERE date >= today() - 1095
      AND date <= today() - 2
      AND ifNull(otc, 0) = 0
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker, date
),
seq AS
(
    SELECT
        ticker,
        date,
        c,
        vol,
        lagInFrame(c)     OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c,
        leadInFrame(o)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_o,
        leadInFrame(c)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_c,
        leadInFrame(date) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_date
    FROM bars
),
movers AS
(
    SELECT
        date,
        ticker,
        100 * (c / prev_c - 1)      AS gain_pct,
        100 * (next_c / next_o - 1) AS next_oc_pct,
        100 * (next_c / c - 1)      AS next_cc_pct,
        row_number() OVER (PARTITION BY date ORDER BY c / prev_c DESC, ticker ASC) AS rnk
    FROM seq
    WHERE prev_c >= 5
      AND next_o > 0
      AND next_c > 0
      AND c * vol >= 5000000
      AND dateDiff('day', date, next_date) <= 6
)
SELECT
    multiIf(gain_pct < 10, 'under 10%',
            gain_pct < 15, '10 to 15%',
            gain_pct < 25, '15 to 25%',
            gain_pct < 50, '25 to 50%',
                           '50% and up')                                                  AS gain_bucket,
    count()                                                                               AS observations,
    round(quantileDeterministic(0.5)(next_cc_pct, cityHash64(ticker, toString(date))), 2)  AS median_next_cc_pct,
    round(avg(next_cc_pct), 2)                                                            AS mean_next_cc_pct,
    round(quantileDeterministic(0.5)(next_oc_pct, cityHash64(ticker, toString(date))), 2)  AS median_next_oc_pct
FROM movers
WHERE rnk <= 20
GROUP BY gain_bucket
ORDER BY min(gain_pct) ASC
Run this yourself

In the under 10% bucket, the median next-session close-to-close return measured -0.27% across 639 observations, while the mean over those same observations came in at -0.15%. In the 50% and up bucket the median measured -9.66%, on 1817 observations.

Read the median column and the mean column side by side in every row. They are computed from identical observations and they describe different things: the median is the middle outcome, and the mean is the middle outcome plus the pull of the tails. A handful of names that run hard again the next day lift an average that thousands of small declines cannot drag back. Any headline quoting only the average return after a big gain is quoting that tail, which is why both columns sit on the panel.

Notice also that the median open-to-close figure differs from the median close-to-close figure in the same row. That distance is the overnight gap, and it is where much of a mover's next-day outcome gets settled before anyone can trade the regular session. The mechanics behind it are the subject of why stocks gap overnight.

Does a bigger one-day gain mean more follow-through?

Medians answer "how much". A reader looking at today's list usually wants "how often" as well. The next panel converts the same observations into three hit rates per bucket: the share that opened the next session above the gain day's close, the share that opened higher and then closed below that opening print, and the share that closed the next session above the gain day's close.

QueryHow the next session split: gapped up, gapped up and faded, closed higher
gain_bucketobservationsgapped_up_pctgapped_up_then_faded_pctclosed_higher_pct
under 10%63949.324.647.1
10 to 15%26474827.344.6
15 to 25%568945.62643
25 to 50%418841.924.241.1
50% and up181730.318.532.9
The exact SQL behind every number
WITH bars AS
(
    SELECT
        ticker,
        date,
        max(toFloat64(open))   AS o,
        max(toFloat64(close))  AS c,
        max(toFloat64(volume)) AS vol
    FROM global_markets.stocks_daily_aggs
    WHERE date >= today() - 1095
      AND date <= today() - 2
      AND ifNull(otc, 0) = 0
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker, date
),
seq AS
(
    SELECT
        ticker,
        date,
        c,
        vol,
        lagInFrame(c)     OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c,
        leadInFrame(o)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_o,
        leadInFrame(c)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_c,
        leadInFrame(date) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_date
    FROM bars
),
movers AS
(
    SELECT
        date,
        ticker,
        100 * (c / prev_c - 1)      AS gain_pct,
        100 * (next_o / c - 1)      AS next_gap_pct,
        100 * (next_c / next_o - 1) AS next_oc_pct,
        100 * (next_c / c - 1)      AS next_cc_pct,
        row_number() OVER (PARTITION BY date ORDER BY c / prev_c DESC, ticker ASC) AS rnk
    FROM seq
    WHERE prev_c >= 5
      AND next_o > 0
      AND next_c > 0
      AND c * vol >= 5000000
      AND dateDiff('day', date, next_date) <= 6
)
SELECT
    multiIf(gain_pct < 10, 'under 10%',
            gain_pct < 15, '10 to 15%',
            gain_pct < 25, '15 to 25%',
            gain_pct < 50, '25 to 50%',
                           '50% and up')                                       AS gain_bucket,
    count()                                                                    AS observations,
    round(100 * countIf(next_gap_pct > 0) / count(), 1)                         AS gapped_up_pct,
    round(100 * countIf(next_gap_pct > 0 AND next_oc_pct < 0) / count(), 1)     AS gapped_up_then_faded_pct,
    round(100 * countIf(next_cc_pct > 0) / count(), 1)                          AS closed_higher_pct
FROM movers
WHERE rnk <= 20
GROUP BY gain_bucket
ORDER BY min(gain_pct) ASC
Run this yourself

In the under 10% bucket, 49.3% of observations opened the next session above the gain day's close, 24.6% opened higher and then closed beneath that opening print, and 47.1% finished the next session above the gain day's close. In the 50% and up bucket that last figure measured 32.9%.

The pattern across the buckets is the useful part. The share closing higher never moves far from half, in any bucket, while the gap-and-fade share stays substantial throughout. Bigger initial moves change the width of the next day's outcomes far more than they change the odds of a higher close. Whether those unfilled gaps eventually close is a separate measurement, and it has its own page: do stock gaps get filled.

Is an overnight gap different from an intraday squeeze?

A gainers list reports one number, the day's percentage change, and two very different sessions produce it. In the first, the stock gaps open far above the prior close and spends the day holding roughly that level, the shape that typically accompanies a scheduled release or a corporate announcement. In the second, the stock opens near the prior close and climbs through the session, the shape that coincides with steady intraday buying or with short covering. These are different populations, and the follow-through question is a different question for each.

The panel below splits the gain day itself by how much of the move was already in place at the open. Gap share is the opening gap divided by the full day's gain, so a value of 1.0 means the entire move happened before the opening bell and a value near 0 means all of it happened during the session. Only gains of 5% or more are included here, where that ratio is stable.

QueryOvernight gap versus intraday run: what the next session did
move_shapeobservationsmedian_next_cc_pctmedian_next_oc_pctmean_next_cc_pct
mostly intraday7551-1.6-1.07-0.56
mixed3790-0.89-0.83-0.61
mostly overnight gap3638-1.95-0.43-2.84
The exact SQL behind every number
WITH bars AS
(
    SELECT
        ticker,
        date,
        max(toFloat64(open))   AS o,
        max(toFloat64(close))  AS c,
        max(toFloat64(volume)) AS vol
    FROM global_markets.stocks_daily_aggs
    WHERE date >= today() - 1095
      AND date <= today() - 2
      AND ifNull(otc, 0) = 0
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker, date
),
seq AS
(
    SELECT
        ticker,
        date,
        o,
        c,
        vol,
        lagInFrame(c)     OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c,
        leadInFrame(o)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_o,
        leadInFrame(c)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_c,
        leadInFrame(date) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_date
    FROM bars
),
movers AS
(
    SELECT
        date,
        ticker,
        100 * (c / prev_c - 1)                        AS gain_pct,
        (o / prev_c - 1) / (c / prev_c - 1)           AS gap_share,
        100 * (next_c / next_o - 1)                   AS next_oc_pct,
        100 * (next_c / c - 1)                        AS next_cc_pct,
        row_number() OVER (PARTITION BY date ORDER BY c / prev_c DESC, ticker ASC) AS rnk
    FROM seq
    WHERE prev_c >= 5
      AND next_o > 0
      AND next_c > 0
      AND c * vol >= 5000000
      AND dateDiff('day', date, next_date) <= 6
)
SELECT
    multiIf(gap_share >= 0.7, 'mostly overnight gap',
            gap_share >= 0.3, 'mixed',
                              'mostly intraday')                                     AS move_shape,
    count()                                                                          AS observations,
    round(quantileDeterministic(0.5)(next_cc_pct, cityHash64(ticker, toString(date))), 2) AS median_next_cc_pct,
    round(quantileDeterministic(0.5)(next_oc_pct, cityHash64(ticker, toString(date))), 2) AS median_next_oc_pct,
    round(avg(next_cc_pct), 2)                                                       AS mean_next_cc_pct
FROM movers
WHERE rnk <= 20
  AND gain_pct >= 5
GROUP BY move_shape
ORDER BY min(gap_share) ASC
Run this yourself

The mostly intraday group posted a median next-session close-to-close return of -1.6% across 7551 observations. The mostly overnight gap group measured -1.95%, alongside a median next-session open-to-close of -0.43%.

The instructive comparison is each row's two medians against one another, not one row against another. A group whose close-to-close median sits far from its open-to-close median is one where the overnight gap is doing the work and the regular session is adding little on top. A group where the two medians sit close together is one where the next day's outcome was decided during trading hours, when a reader could actually respond to it.

How wide is the range of next-day outcomes?

A median and a mean both compress a distribution into a single figure, and the shape around the center is what a reader deciding about tomorrow is really facing. The panel reports the same top-20 sample at five percentiles, on both measures.

QueryThe full spread of next-session returns for top-20 daily gainers
percentileclose_to_close_pctopen_to_close_pct
p05-26.28-18.68
p25-8.05-6.2
p50-1.6-0.9
p754.213.85
p9521.818.8
The exact SQL behind every number
WITH bars AS
(
    SELECT
        ticker,
        date,
        max(toFloat64(open))   AS o,
        max(toFloat64(close))  AS c,
        max(toFloat64(volume)) AS vol
    FROM global_markets.stocks_daily_aggs
    WHERE date >= today() - 1095
      AND date <= today() - 2
      AND ifNull(otc, 0) = 0
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker, date
),
seq AS
(
    SELECT
        ticker,
        date,
        c,
        vol,
        lagInFrame(c)     OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c,
        leadInFrame(o)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_o,
        leadInFrame(c)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_c,
        leadInFrame(date) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_date
    FROM bars
),
movers AS
(
    SELECT
        date,
        ticker,
        100 * (next_c / next_o - 1) AS next_oc_pct,
        100 * (next_c / c - 1)      AS next_cc_pct,
        row_number() OVER (PARTITION BY date ORDER BY c / prev_c DESC, ticker ASC) AS rnk
    FROM seq
    WHERE prev_c >= 5
      AND next_o > 0
      AND next_c > 0
      AND c * vol >= 5000000
      AND dateDiff('day', date, next_date) <= 6
),
picks AS
(
    SELECT
        date,
        ticker,
        next_cc_pct,
        next_oc_pct,
        arrayJoin(['p05', 'p25', 'p50', 'p75', 'p95']) AS percentile
    FROM movers
    WHERE rnk <= 20
)
SELECT
    percentile,
    round(multiIf(percentile = 'p05', quantileDeterministic(0.05)(next_cc_pct, cityHash64(ticker, toString(date))),
                  percentile = 'p25', quantileDeterministic(0.25)(next_cc_pct, cityHash64(ticker, toString(date))),
                  percentile = 'p50', quantileDeterministic(0.5)(next_cc_pct, cityHash64(ticker, toString(date))),
                  percentile = 'p75', quantileDeterministic(0.75)(next_cc_pct, cityHash64(ticker, toString(date))),
                                      quantileDeterministic(0.95)(next_cc_pct, cityHash64(ticker, toString(date)))), 2) AS close_to_close_pct,
    round(multiIf(percentile = 'p05', quantileDeterministic(0.05)(next_oc_pct, cityHash64(ticker, toString(date))),
                  percentile = 'p25', quantileDeterministic(0.25)(next_oc_pct, cityHash64(ticker, toString(date))),
                  percentile = 'p50', quantileDeterministic(0.5)(next_oc_pct, cityHash64(ticker, toString(date))),
                  percentile = 'p75', quantileDeterministic(0.75)(next_oc_pct, cityHash64(ticker, toString(date))),
                                      quantileDeterministic(0.95)(next_oc_pct, cityHash64(ticker, toString(date)))), 2) AS open_to_close_pct
FROM picks
GROUP BY percentile
ORDER BY percentile ASC
Run this yourself

The 5th percentile of next-session close-to-close returns measured -26.28% and the 95th measured 21.8%, with the middle row at -1.6%. One observation in twenty landed at or past each of those ends. The open-to-close column gives the same percentiles for the regular session alone, and the distance between the two columns at each end measures how much of the tail lived in the overnight gap rather than in trading hours.

That width is the finding. A near-flat median describes a distribution whose edges are several percent from the center in both directions, so "nothing usually happens" and "large moves are common" are both accurate readings of the same table.

What this base rate does and does not tell you

A base rate is a statement about a population over a window. This one says that across the last three years, among liquid US stocks that had just posted one of the 20 largest gains of a session, the next session's median outcome sat near flat, roughly half closed higher, and the spread around that center was wide on both sides. That is a fact about thousands of observations, and it is worth knowing before opening any gainers list.

It does not describe the next case. The median row and the 95th-percentile row come from exactly the same sample, and a base rate carries no information about which one a particular name will draw tomorrow. It also holds nothing about the fill a real order would get on a heavily traded mover at the open, or about borrow and halt risk on the short side, neither of which appears anywhere in a daily bar. Fixed-percentage exit rules such as the 7 percent sell rule address a different decision from the one measured here, and they come with their own base rates.

What the measurement earns is the right to replace a story with a number. When the next list of large gainers arrives, "what usually happens next" already has an answer attached, and that answer is a distribution rather than a direction.

Data notes and year-by-year coverage

Coverage is uneven across the window: the first and last calendar years in view are partial. The panel counts sessions and observations per year, next to the same median, so a reader can see whether one year is carrying the result.

QuerySample coverage and next-session medians, year by year
yearsessionsmover_rowsmedian_next_cc_pctmean_next_cc_pct
2023641280-0.360.14
20242525040-1.25-0.96
20252505000-2.25-1.62
20261833660-1.44-1.13
The exact SQL behind every number
WITH bars AS
(
    SELECT
        ticker,
        date,
        max(toFloat64(open))   AS o,
        max(toFloat64(close))  AS c,
        max(toFloat64(volume)) AS vol
    FROM global_markets.stocks_daily_aggs
    WHERE date >= today() - 1095
      AND date <= today() - 2
      AND ifNull(otc, 0) = 0
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker, date
),
seq AS
(
    SELECT
        ticker,
        date,
        c,
        vol,
        lagInFrame(c)     OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c,
        leadInFrame(o)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_o,
        leadInFrame(c)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_c,
        leadInFrame(date) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_date
    FROM bars
),
movers AS
(
    SELECT
        date,
        ticker,
        100 * (next_c / c - 1) AS next_cc_pct,
        row_number() OVER (PARTITION BY date ORDER BY c / prev_c DESC, ticker ASC) AS rnk
    FROM seq
    WHERE prev_c >= 5
      AND next_o > 0
      AND next_c > 0
      AND c * vol >= 5000000
      AND dateDiff('day', date, next_date) <= 6
)
SELECT
    toString(toYear(date))                                                                AS year,
    countDistinct(date)                                                                   AS sessions,
    count()                                                                               AS mover_rows,
    round(quantileDeterministic(0.5)(next_cc_pct, cityHash64(ticker, toString(date))), 2)  AS median_next_cc_pct,
    round(avg(next_cc_pct), 2)                                                            AS mean_next_cc_pct
FROM movers
WHERE rnk <= 20
GROUP BY year
ORDER BY year ASC
Run this yourself

In 2026 the screen produced 3660 observations over 183 sessions, with a median next-session close-to-close return of -1.44%. The window opens in 2023.

  • The window spans the last three years of daily bars and stops a couple of sessions short of today, where the front edge of the daily data is still filling in.
  • Universe filters on the gain day: a prior close at or above $5, and at least $5 million of dollar volume. OTC rows are excluded.
  • Dollar volume is measured on the gain day itself, which the move inflates. Treat it as a liquidity floor, not as a reading of normal turnover.
  • A name with no following session inside six calendar days is dropped before the ranking runs, so a halt or a delisting never occupies one of the 20 slots. This makes the sample slightly tidier than a live gainers list, which shows those names and then goes quiet on them.
  • Daily bars cover the regular session, so an overnight move lands in the next day's open rather than in the prior day's close. That is exactly why the open-to-close and close-to-close columns disagree.
  • Medians use a deterministic quantile, which keeps two runs of the same query in agreement.
  • On an unusually weak session, fewer than 20 names may post a gain at all, and the lowest-ranked rows of that session can sit near zero. Those rows land in the smallest bucket.

FAQ

Do stocks usually keep going up the day after a big one-day gain?

Not reliably in either direction. In this three-year sample the median next-day close-to-close return for a top-20 daily gainer measured -0.27% in the under 10% bucket, close to flat, and the share of observations that closed higher stayed near half in every gain bucket. The spread around that center is wide, which is the part a single average hides.

What is the difference between open-to-close and close-to-close after a gain day?

Close-to-close runs from the gain day's closing price to the next day's closing price, so it includes the overnight gap. Open-to-close covers the next session only, from its opening print to its closing print. Compare the two and you have isolated how much of the outcome happened while the market was shut.

What does it mean when a stock gaps up and fades?

A stock gaps up when it opens above the previous close, and it fades when it then trades down from that opening print over the session. In the under 10% bucket, 24.6% of top-20 observations did both on the session after their big gain.

Does the size of the one-day gain change what happens next?

It changes the width of the outcome more than the odds. The panels group observations from under 10% up to 50% and up and report each bucket's median separately, and the share closing higher stays near half throughout. Observation counts fall sharply in the largest buckets, so those rows carry more uncertainty.

Why report medians instead of averages after a big gain?

A small number of names that continue sharply can lift an average well above the typical outcome. The median describes the middle observation, and publishing both columns makes that skew visible instead of burying it. Both sit on the first panel above.


Every panel here ships with the SQL that produced it. Change the ranking depth or the liquidity floor and ask the same question again on the Strasmore terminal. For the current crop of large moves, see the biggest stock movers this month, and for the volume side of the same screen, unusual volume stocks this week.

#movers#momentum#mean reversion#gaps#base rates