How Monthly Stock Returns Are Measured
A monthly stock return depends on which two prices you pick, whether dividends count, and whether the series is split-adjusted. Each one measured on real data.
Ask what a stock "did in June" and you will get different numbers from different sources, all of them correct. A monthly stock return is defined by choices most people never see: which two prices mark the month's edges, whether dividends count, and whether the price series was adjusted for splits. This page runs the competing conventions on the real tape, side by side, so the differences stop being mysterious.
Close-to-close: the standard convention
The convention used by index providers, fund fact sheets, and most quote pages: the month's return runs from the prior month's final regular-session closing price to this month's final close. May's last close is the starting line for June. The arithmetic is one line, ending price minus starting price, divided by starting price. A toy version first: a stock that ends May at $50.00 and ends June at $53.50 returned (53.50 − 50.00) ÷ 50.00 = 0.07, or 7%, for the month. Every convention on this page feeds different inputs into that same formula. Here it is on the real June 2026 tape, alongside the main alternative:
| ticker | may_final_close | june_first_open | june_final_close | close_to_close_pct | open_to_close_pct | convention_gap_points |
|---|---|---|---|---|---|---|
| NVDA | 211.15 | 215.73 | 199.76 | -5.39 | -7.4 | 2.01 |
| SPY | 756.4 | 755.36 | 746.32 | -1.33 | -1.2 | 0.14 |
The exact SQL behind every number
SELECT ticker,
round(argMaxIf(cl, day, day <= '2026-05-31'), 2) AS may_final_close,
round(argMinIf(op, day, day >= '2026-06-01'), 2) AS june_first_open,
round(argMaxIf(cl, day, day <= '2026-06-30'), 2) AS june_final_close,
round(100 * (argMaxIf(cl, day, day <= '2026-06-30') - argMaxIf(cl, day, day <= '2026-05-31'))
/ argMaxIf(cl, day, day <= '2026-05-31'), 2) AS close_to_close_pct,
round(100 * (argMaxIf(cl, day, day <= '2026-06-30') - argMinIf(op, day, day >= '2026-06-01'))
/ argMinIf(op, day, day >= '2026-06-01'), 2) AS open_to_close_pct,
round(abs(100 * (argMaxIf(cl, day, day <= '2026-06-30') - argMinIf(op, day, day >= '2026-06-01'))
/ argMinIf(op, day, day >= '2026-06-01')
- 100 * (argMaxIf(cl, day, day <= '2026-06-30') - argMaxIf(cl, day, day <= '2026-05-31'))
/ argMaxIf(cl, day, day <= '2026-05-31')), 2) AS convention_gap_points
FROM (
SELECT ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS day,
argMinIf(toFloat64(open), window_start, rth) AS op,
argMaxIf(toFloat64(close), window_start, rth) AS cl
FROM (
SELECT ticker, window_start, open, close,
toTimeZone(window_start, 'America/New_York') >= toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 09:30:00'), 'America/New_York')
AND toTimeZone(window_start, 'America/New_York') < toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 16:00:00'), 'America/New_York') AS rth
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('NVDA', 'SPY')
AND window_start >= '2026-05-28 04:00:00'
AND window_start < '2026-07-01 08:00:00'
)
GROUP BY ticker, day
)
GROUP BY ticker
ORDER BY tickerNVDA's June was -5.39% close-to-close, but -7.4% measured from June's first regular-session open to its last close. Same stock, same month, 2.01 percentage points between the two honest answers. The entire difference is one overnight+weekend window: NVDA closed May at $211.15 and opened June at $215.73, that jump belongs to "June" in the open-to-close convention and to the boundary in close-to-close. SPY shows the same anatomy more gently: -1.33% close-to-close versus -1.2% open-to-close.
Neither number is wrong. Close-to-close answers "what did a continuing holder experience across the month boundary?" Open-to-close answers "what did the month's own trading deliver to someone who bought at the June open?" They differ whenever the market gaps across the boundary, and overnight gaps are routine, not rare.
Dividends: price return vs total return
The second choice: does the cash a stock paid out during the month count? A price return ignores it; a total return adds it back. For most single months the gap is small, a quarterly payer distributes roughly a quarter of its yield per quarter, but it compounds into a chasm over years, and June 2026 happened to be an SPY payment month:
| may_final_close | june_final_close | june_dividend_per_share | price_return_pct | dividend_contribution_pct | total_return_pct |
|---|---|---|---|---|---|
| 756.4 | 746.32 | 1.9035 | -1.33 | 0.25 | -1.08 |
The exact SQL behind every number
WITH px AS (
SELECT argMaxIf(cl, day, day <= '2026-05-31') AS may_close,
argMaxIf(cl, day, day <= '2026-06-30') AS jun_close
FROM (
SELECT ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS day,
argMinIf(toFloat64(open), window_start, rth) AS op,
argMaxIf(toFloat64(close), window_start, rth) AS cl
FROM (
SELECT ticker, window_start, open, close,
toTimeZone(window_start, 'America/New_York') >= toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 09:30:00'), 'America/New_York')
AND toTimeZone(window_start, 'America/New_York') < toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 16:00:00'), 'America/New_York') AS rth
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('NVDA', 'SPY')
AND window_start >= '2026-05-28 04:00:00'
AND window_start < '2026-07-01 08:00:00'
)
GROUP BY ticker, day
)
WHERE ticker = 'SPY'
),
dv AS (
SELECT sum(cash_amount) AS div_per_share
FROM global_markets.stocks_dividends
WHERE ticker = 'SPY'
AND ex_dividend_date >= '2026-06-01'
AND ex_dividend_date <= '2026-06-30'
)
SELECT round(may_close, 2) AS may_final_close,
round(jun_close, 2) AS june_final_close,
round(div_per_share, 4) AS june_dividend_per_share,
round(100 * (jun_close - may_close) / may_close, 2) AS price_return_pct,
round(100 * div_per_share / may_close, 2) AS dividend_contribution_pct,
round(100 * ((jun_close + div_per_share) - may_close) / may_close, 2) AS total_return_pct
FROM px CROSS JOIN dvSPY's June price return was -1.33%; add the $1.9035 per-share distribution that went ex-dividend during the month and the total return improves to -1.08%, a 0.25% contribution from cash. Index families publish both flavors (the "S&P 500" you see quoted is usually the price index), fund performance pages are required to show total return, and screeners mix the two freely. When two sources disagree by a few tenths on a dividend payer's month, the dividend is the first suspect, the ex-dividend date determines which month a payment belongs to.
A single June is tenths of a percent. Horizon is what separates the two conventions, here is the same arithmetic stretched across five years:
| june_2021_close | june_2026_close | dividends_paid_per_share | dividend_payments | five_year_price_return_pct | five_year_return_with_divs_pct | dividend_points_added |
|---|---|---|---|---|---|---|
| 428.07 | 746.32 | 34.07 | 20 | 74.35 | 82.3 | 7.96 |
The exact SQL behind every number
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS day,
argMaxIf(toFloat64(close), window_start,
toTimeZone(window_start, 'America/New_York') >= toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 09:30:00'), 'America/New_York')
AND toTimeZone(window_start, 'America/New_York') < toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 16:00:00'), 'America/New_York')) AS cl
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND ((window_start >= '2021-06-28 04:00:00' AND window_start < '2021-07-01 08:00:00')
OR (window_start >= '2026-06-26 04:00:00' AND window_start < '2026-07-01 08:00:00'))
GROUP BY day
),
px AS (
SELECT argMaxIf(cl, day, day <= '2021-06-30') AS start_close,
argMaxIf(cl, day, day <= '2026-06-30') AS end_close
FROM daily
),
dv AS (
SELECT round(sum(cash_amount), 2) AS divs_per_share,
count() AS payments
FROM global_markets.stocks_dividends
WHERE ticker = 'SPY'
AND ex_dividend_date >= '2021-07-01'
AND ex_dividend_date <= '2026-06-30'
)
SELECT round(start_close, 2) AS june_2021_close,
round(end_close, 2) AS june_2026_close,
divs_per_share AS dividends_paid_per_share,
payments AS dividend_payments,
round(100 * (end_close - start_close) / start_close, 2) AS five_year_price_return_pct,
round(100 * ((end_close + divs_per_share) - start_close) / start_close, 2) AS five_year_return_with_divs_pct,
round(100 * divs_per_share / start_close, 2) AS dividend_points_added
FROM px CROSS JOIN dvFrom its June 2021 close of $428.07 to its June 2026 close of $746.32, SPY's five-year price return measures 74.35%. Over the same span it paid 20 distributions totaling $34.07 per share; counting that cash lifts the figure to 82.3%, 7.96 points of gap before a single dividend is reinvested. A true total-return index reinvests each payment on its ex-date, and the reinvested shares push the gap wider still. On a mature payer, "large margins" is not a figure of speech; it is the default outcome of years of quarterly cash.
Adjusted close vs. raw close: the split trap
Most chart tools, Yahoo Finance, Google Finance, most broker apps, quote an adjusted close: a historical price series where past prints have been divided down for splits (and, on some platforms, adjusted for dividends too) so the line is comparable through time. Our warehouse stores the tape as it actually printed. In an ordinary month the two agree. In a month containing a stock split they tell wildly different stories, NVDA's 10-for-1 split executed on June 10, 2024, inside the month:
| may_final_close_raw | june_final_close_raw | shares_after_split_per_old_share | raw_price_change_pct | may_close_split_adjusted | adjusted_return_pct |
|---|---|---|---|---|---|
| 1097.46 | 123.41 | 10 | -88.75 | 109.75 | 12.45 |
The exact SQL behind every number
WITH px AS (
SELECT argMaxIf(cl, day, day <= '2024-05-31') AS may_close,
argMaxIf(cl, day, day <= '2024-06-30') AS jun_close
FROM (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS day,
argMaxIf(toFloat64(close), window_start,
toTimeZone(window_start, 'America/New_York') >= toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 09:30:00'), 'America/New_York')
AND toTimeZone(window_start, 'America/New_York') < toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 16:00:00'), 'America/New_York')) AS cl
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'NVDA'
AND window_start >= '2024-05-29 04:00:00'
AND window_start < '2024-07-01 08:00:00'
GROUP BY day
)
),
sp AS (
SELECT split_to / split_from AS ratio
FROM global_markets.stocks_splits
WHERE ticker = 'NVDA'
AND execution_date >= '2024-06-01'
AND execution_date <= '2024-06-30'
)
SELECT round(may_close, 2) AS may_final_close_raw,
round(jun_close, 2) AS june_final_close_raw,
toInt32(ratio) AS shares_after_split_per_old_share,
round(100 * (jun_close - may_close) / may_close, 2) AS raw_price_change_pct,
round(may_close / ratio, 2) AS may_close_split_adjusted,
round(100 * (jun_close - may_close / ratio) / (may_close / ratio), 2) AS adjusted_return_pct
FROM px CROSS JOIN spRead raw, NVDA "fell" from $1097.46 to $123.41 that June, a -88.75% collapse. Nothing of the sort happened: on the execution date each old share became 10 new ones and the price divided to match, leaving every holder's dollar value where it was. Divide the May close by the same ratio, $109.75 on a split-adjusted basis, and June 2024 measures 12.45%, an up month. Any source quoting adjusted data reports something near the second figure; a naive calculation on raw prints reports the first. Adjusted close is the single most common reason two sources disagree wildly on a historical monthly return, when a figure looks impossibly large in either direction, check the split calendar before anything else.
Which "close" is the close?
One more boundary hides inside the conventions: the closing price itself. The number every fact sheet uses is the official closing-auction print, the single price set by the closing auction at 4:00 PM ET. But trading continues after hours, and the last print of the extended session can drift from the official close:
| regular_session_close | last_extended_print | after_hours_drift_pct |
|---|---|---|
| 746.32 | 746.3 | -0.003 |
The exact SQL behind every number
SELECT round(argMaxIf(toFloat64(close), window_start, rth), 2) AS regular_session_close,
round(argMax(toFloat64(close), window_start), 2) AS last_extended_print,
round(100 * (argMax(toFloat64(close), window_start) - argMaxIf(toFloat64(close), window_start, rth))
/ argMaxIf(toFloat64(close), window_start, rth), 3) AS after_hours_drift_pct
FROM (
SELECT window_start, close,
toTimeZone(window_start, 'America/New_York') >= toDateTime('2026-06-30 09:30:00', 'America/New_York')
AND toTimeZone(window_start, 'America/New_York') < toDateTime('2026-06-30 16:00:00', 'America/New_York') AS rth
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2026-06-30 04:00:00'
AND window_start < '2026-07-01 08:00:00'
)On June 30 the two differed by -0.003%, small that evening, but on an earnings night in a single stock the extended session can move percent-scale amounts that the official monthly return will assign to the next month. After-hours and premarket trading covers that boundary in detail. One related edge case: month boundaries occasionally land on a holiday-shortened session, where the regular day ends at 1:00 PM ET and the official close prints in an early closing auction, the market holiday schedule lists them.
Compounding twelve months into a year
A yearly return is not the sum of twelve monthly returns, it is their product. The linking formula: convert each month into a growth factor (1 + return), multiply the twelve factors together, subtract 1. A −5% month followed by a +5% month sums to zero but compounds to 0.95 × 1.05 − 1 = −0.25%. Here is SPY's actual monthly path over the twelve months ending June 2026:
| month | month_final_close | simple_return_pct | log_return_pct |
|---|---|---|---|
| 2025-07 | 631.97 | 2.28 | 2.25 |
| 2025-08 | 645.04 | 2.07 | 2.05 |
| 2025-09 | 666.11 | 3.27 | 3.21 |
| 2025-10 | 681.99 | 2.38 | 2.36 |
| 2025-11 | 683.39 | 0.21 | 0.21 |
| 2025-12 | 681.84 | -0.23 | -0.23 |
| 2026-01 | 691.85 | 1.47 | 1.46 |
| 2026-02 | 686.23 | -0.81 | -0.82 |
| 2026-03 | 650.24 | -5.25 | -5.39 |
| 2026-04 | 718.43 | 10.49 | 9.97 |
| 2026-05 | 756.4 | 5.29 | 5.15 |
| 2026-06 | 746.32 | -1.33 | -1.34 |
The exact SQL behind every number
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS day,
argMaxIf(toFloat64(close), window_start,
toTimeZone(window_start, 'America/New_York') >= toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 09:30:00'), 'America/New_York')
AND toTimeZone(window_start, 'America/New_York') < toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 16:00:00'), 'America/New_York')) AS cl
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2025-06-24 04:00:00'
AND window_start < '2026-07-01 08:00:00'
GROUP BY day
),
monthly AS (
SELECT toStartOfMonth(day) AS month_start,
argMax(cl, day) AS month_close
FROM daily
GROUP BY month_start
),
rets AS (
SELECT month_start,
formatDateTime(month_start, '%Y-%m') AS month,
round(month_close, 2) AS month_final_close,
100 * (month_close / lagInFrame(month_close) OVER (ORDER BY month_start) - 1) AS r
FROM monthly
)
SELECT month,
month_final_close,
round(r, 2) AS simple_return_pct,
round(100 * log(1 + r / 100), 2) AS log_return_pct
FROM rets
WHERE isFinite(r)
ORDER BY month_startTwelve close-to-close months, from 2.28% in 2025-07 to -1.33% in 2026-06, the last row is the same SPY June 2026 figure the first panel measured. Link the twelve properly and compare against the shortcut of just adding the percentages:
| months_linked | simple_sum_pct | geometric_compound_pct | direct_twelve_month_pct | compounding_gap_points | sum_of_monthly_log_returns_pct | twelve_month_log_return_pct |
|---|---|---|---|---|---|---|
| 12 | 19.83 | 20.79 | 20.79 | 0.96 | 18.89 | 18.89 |
The exact SQL behind every number
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS day,
argMaxIf(toFloat64(close), window_start,
toTimeZone(window_start, 'America/New_York') >= toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 09:30:00'), 'America/New_York')
AND toTimeZone(window_start, 'America/New_York') < toDateTime(concat(toString(toDate(toTimeZone(window_start, 'America/New_York'))), ' 16:00:00'), 'America/New_York')) AS cl
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2025-06-24 04:00:00'
AND window_start < '2026-07-01 08:00:00'
GROUP BY day
),
monthly AS (
SELECT toStartOfMonth(day) AS month_start,
argMax(cl, day) AS month_close
FROM daily
GROUP BY month_start
),
rets AS (
SELECT month_start,
100 * (month_close / lagInFrame(month_close) OVER (ORDER BY month_start) - 1) AS r
FROM monthly
),
ends AS (
SELECT argMin(month_close, month_start) AS base_close,
argMax(month_close, month_start) AS final_close
FROM monthly
)
SELECT count() AS months_linked,
round(sum(r), 2) AS simple_sum_pct,
round(100 * (exp(sum(log(1 + r / 100))) - 1), 2) AS geometric_compound_pct,
round(100 * (any(final_close) / any(base_close) - 1), 2) AS direct_twelve_month_pct,
round(100 * (exp(sum(log(1 + r / 100))) - 1) - sum(r), 2) AS compounding_gap_points,
round(sum(100 * log(1 + r / 100)), 2) AS sum_of_monthly_log_returns_pct,
round(100 * log(any(final_close) / any(base_close)), 2) AS twelve_month_log_return_pct
FROM rets CROSS JOIN ends
WHERE isFinite(r)The twelve months sum to 19.83% but compound to 20.79%, and the direct calculation, June 2025's close straight to June 2026's, gives 20.79%, matching the compounded figure rather than the sum. The gap here is 0.96 points on one calm-ish year; it widens with volatility and horizon. Adding monthly percentages is a common spreadsheet habit, and it is quietly wrong.
The last two columns show the convention built for adding: the log return, the natural logarithm of the growth factor, ln(1 + return). Monthly log returns sum to 18.89%, identical to the twelve-month log return of 18.89%, which is exactly why analysts and risk models use them. For small months the two conventions barely differ (2025-11: 0.21% simple, 0.21% log); the bigger the month, the wider they split (2026-04: 10.49% simple, 9.97% log). Fund fact sheets quote simple returns; log returns live in research papers and volatility math.
The checklist that reconciles any two sources
When two monthly return figures disagree, they almost always differ on one of four switches:
- Endpoints, close-to-close (the standard) or open-to-close (the month's own trading). Different whenever the boundary gapped.
- Dividends, price return or total return. Different in any month containing an ex-dividend date.
- Adjustment, raw prints or the adjusted-close series. Wildly different in any month containing a split.
- The close itself, official auction print (the standard) versus a last trade that may include extended hours.
Run the same switches on both sources and the disagreement disappears, and a fund-versus-index comparison is only valid when both figures sit on the same four settings; mismatch one and the comparison quietly measures the switch instead of the performance. Our own June 2026 recap states its convention up front for exactly this reason, every monthly number there is close-to-close, official prints, price return.
Monthly returns FAQ
What is a close-to-close return?
The percentage change from the prior period's final regular-session closing price to this period's final close. It is the standard convention for daily, monthly, and yearly stock returns on fact sheets and index reports.
Why do two websites show different returns for the same stock and month?
Almost always one of four conventions: different endpoints (close-to-close vs open-to-close), dividends counted or not, raw versus split-adjusted prices, or a different closing price source. NVDA's June 2026 measures -5.39% or -7.4% on the first switch alone.
Does a stock split change a monthly return calculation?
It should not, a split changes the share count and price together, leaving position value untouched, but it wrecks any calculation done on raw prices. NVDA's June 2024 looks like -88.75% on raw closes and measures 12.45% once the 10-for-1 split is adjusted for.
Do monthly stock returns include dividends?
Only total returns do. Price returns, including the headline S&P 500 index level, exclude them. In June 2026 the dividend added 0.25% to SPY's month, the gap between its price and total return.
Is my personal portfolio's monthly return calculated the same way?
Not once cash moves in or out. Single-security returns like the ones on this page are time-weighted; an account with deposits and withdrawals needs a money-weighted (internal-rate-of-return) calculation, or the cash flows masquerade as performance. Most brokers report a time-weighted figure precisely to strip your deposits out of the number.
Every figure above is a stored, versioned query on the real tape, expand any panel's SQL, or measure any ticker's month under any convention on the Strasmore terminal.