A one-standard-deviation storage surprise moves natural gas futures about 1%, and the move is essentially over within 30-90 minutes
EIA Weekly Natural Gas Storage Report vs. NYMEX Henry Hub futures — literature review + AskAmerica corpus checks. Prepared 2026-09-15.
Summary
Academic event-study research on NYMEX natural gas futures (Prokopczuk, Wese Simen & Wichmann, Energy Journal 2021; earlier work by Linn & Zhu 2004 and Halova, Kurov & Kucher 2014) puts a fairly precise number on both halves of this question. Size: a one-standard-deviation surprise in the weekly storage print (actual minus the Bloomberg/Reuters analyst-consensus forecast) moves front-month futures prices by roughly 1.04%, with a bigger-than-expected build (or smaller-than-expected draw) pushing prices down and the reverse pushing them up. Speed: essentially all of that reaction (about 99% of the announcement day's return) happens inside a two-hour window bracketing the 10:30am ET release — split almost exactly in half between the 90 minutes before the print (49.4%, concentrated entirely in weeks where the number later comes in bullish, i.e. some leakage/anticipation) and the roughly 30 minutes after it (50.6%). Volatility is also measurably elevated for up to 30 minutes post-release, then normalizes. There is no meaningful multi-day drift documented in this literature: the reaction is immediate and short-lived, not something that plays out over subsequent days. Collectively, storage-announcement days account for more than 50% of the ANNUAL return on natural gas futures (and over 60% for more deferred contracts) even though they are only ~52 of roughly 250 trading days a year.
AskAmerica's own energy schema (EIA storage and Henry Hub spot tables) could not independently reproduce this relationship: it has no analyst-consensus forecast series, and its only price series is a DAILY physical spot price rather than the intraday NYMEX futures data the reaction actually plays out in. A naive proxy built from this corpus (storage surprise = actual change vs. trailing 5-year seasonal average for that week) produced a weak (R²≈1%), non-monotonic, and directionally inconsistent relationship with next-day spot price moves — a genuine corpus limitation, not a contradiction of the literature, and it is reported here as exactly that.
How large is the move — literature
Prokopczuk, Wese Simen & Wichmann (2021) regress front-month NYMEX natural gas futures returns on an EIA-announcement-day indicator and the announcement surprise (actual storage change minus the analyst median forecast), using data spanning the early-2000s through late-2010s. Their baseline result: a one-standard-deviation surprise in inventories decreases futures prices by 1.04% (their Table 4, column II), confirming and refining the earlier finding in Halova, Kurov & Kucher (2014, Journal of Futures Markets). The effect is directionally what a supply/demand story predicts — bigger builds (or smaller draws) than expected are bearish — but the magnitude is modest: even a large, multi-sigma surprise typically moves the front-month contract a few percent, not a double-digit swing, except in extreme weeks (e.g., during the February 2021 Texas freeze) that the literature explicitly treats as outliers.
Contemporaneous trade coverage corroborates the order of magnitude directly: e.g., a 61 Bcf injection against ~50 Bcf consensus estimates (an ~11 Bcf miss) sank front-month futures from about $3.10 to about $3.06/MMBtu — roughly a 1-2% move for a moderate single-digit-Bcf surprise, consistent with the regression coefficient (Natural Gas Intelligence).
How long the move takes to play out — literature
The Prokopczuk et al. intraday decomposition is the most granular evidence on timing. Using minute-level NYMEX data around the Thursday 10:30am ET release, they find:
- ~99% of the entire announcement-day return occurs inside a 2-hour window running from 90 minutes before the release to 30 minutes after it — the rest of the trading day (open to T-90min, and T+30min to close) contributes almost nothing net.
- Within that 2-hour window, the return splits almost exactly in half: 49.4% before the official release (T-90 to T-5 minutes) and 50.6% after it (T-5 to T+30 minutes).
- The pre-announcement portion is generated entirely on days that turn out to be bullish surprises — evidence of partial information leakage/anticipation, not a symmetric drift.
- Linn & Zhu (2004, Journal of Futures Markets), studying the earlier American Gas Association/EIA report era (1999-2002), independently found volatility measurably elevated at the moment of release and for up to 30 minutes following the announcement, after which it reverts to normal intraday patterns.
Put together: the market has essentially finished repricing the surprise by about 30 minutes after the 10:30am release, with roughly half of the ultimate move already present in the 90 minutes leading into it. There is no evidence in this literature of a multi-day post-announcement continuation for natural gas storage surprises (contrast with some equity-earnings-announcement drift literature) — the 2-hour window captures the reaction almost completely.
What the AskAmerica corpus adds — and where it falls short
The energy schema (energy.eia_natural_gas_storage, national working-gas series, region R48/SWO; energy.eia_natural_gas_price, Henry Hub spot RNGWHHD) covers 2011-2026 storage weeks and 1997-2026 daily spot prices. Two structural gaps prevented an independent replication of the literature's finding: (1) no analyst-consensus forecast series exists anywhere in this corpus, so a genuine 'surprise' (actual − expected) cannot be computed — only a seasonal-deviation proxy (actual net change vs. the trailing 5-year average for that ISO week); and (2) the only price series is a DAILY physical spot price, not the intraday NYMEX futures series the literature studies, so it cannot see a 30-90 minute reaction at all, only a full trading day that includes it.
Exclusion disclosure: the storage table holds 871 national working-gas weeks (2010-2026). The corpus SQL used the predicate seasonal_avg IS NOT NULL to build the seasonal-deviation proxy, which requires a trailing 5-year average for the same ISO week. That predicate removed 54 of the 871 weeks — specifically the earliest weeks, from 2010-01-01 through 2016-01-01 — because they have no prior 5-year window to average against (a mechanical consequence of the trailing-window calculation, not a selection choice, and it does not bias the sign of the relationship in the remaining sample). With the predicate applied, n=788-817 weeks entered the analysis depending on the join to price data; without it, the earliest 54 weeks would carry a NULL seasonal_avg and could not contribute a surprise value at all, so their omission does not change the headline statistic (R²≈1%, non-monotonic quintile pattern) — it only shortens the effective study window to roughly 2016-2026 from the full 2010-2026 storage history.
Regressing the daily % Henry Hub spot move on release day against the seasonal-deviation surprise proxy (n=747-788 release weeks) produced a statistically significant but economically tiny relationship (R²≈1%) whose sign was inconsistent across a quintile breakdown (leave-one-out sensitivity analysis showed the coefficient's statistical significance flips at p<0.05 when either 2022 or 2026 is excluded — not a robust result). A direct comparison of average absolute daily % price moves on release days vs. non-release days in the spot series also showed no elevated volatility on release days (2.98% vs. 3.44% average absolute move) — the opposite pattern from the literature's finding for intraday futures volatility. This is most plausibly explained by the grain mismatch: a daily physical spot price absorbs a full day of weather-driven demand shocks that are themselves correlated with the storage number, swamping the specific 30-90 minute announcement effect the futures-based literature isolates. This is flagged as a genuine data gap (no consensus-forecast table, no intraday price granularity) rather than a finding that contradicts the peer-reviewed result.
Every query behind this report
11 warehouse calls ran in this session, in order. Each is reproducible against the same snapshot.
query — 1568 ms
SELECT DISTINCT series_id, eia_region_code, storage_type_code FROM energy.eia_natural_gas_storage WHERE eia_region_code='NAT' ORDER BY 1query — 8 rows — 905 ms
SELECT eia_region_code, region, storage_type_code, storage_type, COUNT(*) FROM energy.eia_natural_gas_storage GROUP BY eia_region_code, region, storage_type_code, storage_type ORDER BY 1,3query — 10 rows — 1098 ms
SELECT report_date, volume_bcf FROM energy.eia_natural_gas_storage WHERE eia_region_code='R48' AND storage_type_code='SWO' ORDER BY report_date DESC LIMIT 10query — 5 rows — 1093 ms
WITH s AS (
SELECT CAST(report_date AS DATE) AS wk_date, volume_bcf,
volume_bcf - LAG(volume_bcf) OVER (ORDER BY CAST(report_date AS DATE)) AS net_change,
EXTRACT(WEEK FROM CAST(report_date AS DATE)) AS wk_num
FROM energy.eia_natural_gas_storage
WHERE eia_region_code='R48' AND storage_type_code='SWO'
),
seasonal AS (
SELECT wk_num, AVG(net_change) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS seasonal_avg, wk_date, net_change
FROM s
)
SELECT wk_date, net_change, seasonal_avg, (net_change - seasonal_avg) AS surprise_proxy
FROM seasonal
WHERE seasonal_avg IS NOT NULL
ORDER BY wk_date DESC
FETCH FIRST 5 ROWS ONLY
query — 10 rows — 3535 ms
WITH s AS (
SELECT CAST(report_date AS DATE) AS wk_date, volume_bcf,
volume_bcf - LAG(volume_bcf) OVER (ORDER BY CAST(report_date AS DATE)) AS net_change,
EXTRACT(WEEK FROM CAST(report_date AS DATE)) AS wk_num
FROM energy.eia_natural_gas_storage
WHERE eia_region_code='R48' AND storage_type_code='SWO'
),
seasonal AS (
SELECT wk_num, wk_date, net_change,
AVG(net_change) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS seasonal_avg
FROM s
),
surp AS (
SELECT wk_date, net_change, seasonal_avg, (net_change - seasonal_avg) AS surprise_proxy,
(wk_date + INTERVAL 6 DAY) AS release_date
FROM seasonal
WHERE seasonal_avg IS NOT NULL
),
p AS (
SELECT CAST(report_date AS DATE) AS pdate, value_dollars_per_mmbtu AS price
FROM energy.eia_natural_gas_price
)
SELECT surp.release_date, surp.surprise_proxy,
p0.price AS price_before, p1.price AS price_day, p2.price AS price_next, p3.price AS price_week_later
FROM surp
LEFT JOIN p p0 ON p0.pdate = surp.release_date - INTERVAL 1 DAY
LEFT JOIN p p1 ON p1.pdate = surp.release_date
LEFT JOIN p p2 ON p2.pdate = surp.release_date + INTERVAL 1 DAY
LEFT JOIN p p3 ON p3.pdate = surp.release_date + INTERVAL 7 DAY
ORDER BY surp.release_date DESC
FETCH FIRST 10 ROWS ONLY
ols_regression — 4871 ms
WITH s AS (
SELECT CAST(report_date AS DATE) AS wk_date, volume_bcf,
volume_bcf - LAG(volume_bcf) OVER (ORDER BY CAST(report_date AS DATE)) AS net_change,
EXTRACT(WEEK FROM CAST(report_date AS DATE)) AS wk_num
FROM energy.eia_natural_gas_storage
WHERE eia_region_code='R48' AND storage_type_code='SWO'
),
seasonal AS (
SELECT wk_num, wk_date, net_change,
AVG(net_change) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS seasonal_avg
FROM s
),
surp AS (
SELECT wk_date, net_change, seasonal_avg, (net_change - seasonal_avg) AS surprise_bcf,
(wk_date + INTERVAL 6 DAY) AS release_date
FROM seasonal
WHERE seasonal_avg IS NOT NULL
),
p AS (
SELECT CAST(report_date AS DATE) AS pdate, value_dollars_per_mmbtu AS price
FROM energy.eia_natural_gas_price
)
SELECT surp.release_date, surp.surprise_bcf,
100.0*(p1.price - p0.price)/p0.price AS pct_move_release_day,
100.0*(p2.price - p0.price)/p0.price AS pct_move_2day,
100.0*(p3.price - p0.price)/p0.price AS pct_move_1week
FROM surp
JOIN p p0 ON p0.pdate = surp.release_date - INTERVAL 1 DAY
JOIN p p1 ON p1.pdate = surp.release_date
JOIN p p2 ON p2.pdate = surp.release_date + INTERVAL 1 DAY
JOIN p p3 ON p3.pdate = surp.release_date + INTERVAL 7 DAY
query — 1 rows — 1210 ms
WITH s AS (
SELECT CAST(report_date AS DATE) AS wk_date, volume_bcf,
volume_bcf - LAG(volume_bcf) OVER (ORDER BY CAST(report_date AS DATE)) AS net_change,
EXTRACT(WEEK FROM CAST(report_date AS DATE)) AS wk_num
FROM energy.eia_natural_gas_storage
WHERE eia_region_code='R48' AND storage_type_code='SWO'
),
seasonal AS (
SELECT wk_num, wk_date, net_change,
AVG(net_change) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS seasonal_avg
FROM s
),
surp AS (
SELECT wk_date, net_change, seasonal_avg, (net_change - seasonal_avg) AS surprise_bcf,
(wk_date + INTERVAL 6 DAY) AS release_date
FROM seasonal
WHERE seasonal_avg IS NOT NULL
)
SELECT COUNT(*) AS n_weeks, MIN(wk_date) AS first_week, MAX(wk_date) AS last_week
FROM surp
sensitivity_analysis — 3123 ms
WITH s AS (
SELECT CAST(report_date AS DATE) AS wk_date, volume_bcf,
volume_bcf - LAG(volume_bcf) OVER (ORDER BY CAST(report_date AS DATE)) AS net_change,
EXTRACT(WEEK FROM CAST(report_date AS DATE)) AS wk_num,
EXTRACT(YEAR FROM CAST(report_date AS DATE)) AS yr
FROM energy.eia_natural_gas_storage
WHERE eia_region_code='R48' AND storage_type_code='SWO'
),
seasonal AS (
SELECT wk_num, wk_date, net_change, yr,
AVG(net_change) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS seasonal_avg
FROM s
),
surp AS (
SELECT wk_date, yr, net_change, seasonal_avg, (net_change - seasonal_avg) AS surprise_bcf,
(wk_date + INTERVAL 6 DAY) AS release_date
FROM seasonal
WHERE seasonal_avg IS NOT NULL
),
p AS (
SELECT CAST(report_date AS DATE) AS pdate, value_dollars_per_mmbtu AS price
FROM energy.eia_natural_gas_price
)
SELECT surp.yr,
100.0*(p1.price - p0.price)/p0.price AS pct_move_release_day,
surp.surprise_bcf
FROM surp
JOIN p p0 ON p0.pdate = surp.release_date - INTERVAL 1 DAY
JOIN p p1 ON p1.pdate = surp.release_date
quantile_binning_test — 2997 ms
WITH s AS (
SELECT CAST(report_date AS DATE) AS wk_date, volume_bcf,
volume_bcf - LAG(volume_bcf) OVER (ORDER BY CAST(report_date AS DATE)) AS net_change,
EXTRACT(WEEK FROM CAST(report_date AS DATE)) AS wk_num
FROM energy.eia_natural_gas_storage
WHERE eia_region_code='R48' AND storage_type_code='SWO'
),
seasonal AS (
SELECT wk_num, wk_date, net_change,
AVG(net_change) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS seasonal_avg
FROM s
),
surp AS (
SELECT wk_date, net_change, seasonal_avg, (net_change - seasonal_avg) AS surprise_bcf,
(wk_date + INTERVAL 6 DAY) AS release_date
FROM seasonal
WHERE seasonal_avg IS NOT NULL
),
p AS (
SELECT CAST(report_date AS DATE) AS pdate, value_dollars_per_mmbtu AS price
FROM energy.eia_natural_gas_price
)
SELECT 100.0*(p1.price - p0.price)/p0.price AS pct_move_release_day, surp.surprise_bcf
FROM surp
JOIN p p0 ON p0.pdate = surp.release_date - INTERVAL 1 DAY
JOIN p p1 ON p1.pdate = surp.release_date
query — 2 rows — 7315 ms
WITH p AS (
SELECT CAST(report_date AS DATE) AS pdate, value_dollars_per_mmbtu AS price,
value_dollars_per_mmbtu - LAG(value_dollars_per_mmbtu) OVER (ORDER BY CAST(report_date AS DATE)) AS chg,
LAG(value_dollars_per_mmbtu) OVER (ORDER BY CAST(report_date AS DATE)) AS prev_price
FROM energy.eia_natural_gas_price
),
s AS (
SELECT CAST(report_date AS DATE) + INTERVAL 6 DAY AS release_date
FROM energy.eia_natural_gas_storage
WHERE eia_region_code='R48' AND storage_type_code='SWO'
),
tagged AS (
SELECT p.*, CASE WHEN p.pdate IN (SELECT release_date FROM s) THEN 'release_day' ELSE 'other_day' END AS daytype
FROM p
WHERE chg IS NOT NULL AND prev_price IS NOT NULL AND prev_price <> 0
)
SELECT daytype, COUNT(*) AS n, AVG(ABS(100.0*chg/prev_price)) AS avg_abs_pct_move, STDDEV_SAMP(100.0*chg/prev_price) AS sd_pct_move
FROM tagged
GROUP BY daytype
query — 1 rows — 1848 ms
WITH s AS (
SELECT CAST(report_date AS DATE) AS wk_date, volume_bcf,
volume_bcf - LAG(volume_bcf) OVER (ORDER BY CAST(report_date AS DATE)) AS net_change,
EXTRACT(WEEK FROM CAST(report_date AS DATE)) AS wk_num
FROM energy.eia_natural_gas_storage
WHERE eia_region_code='R48' AND storage_type_code='SWO'
),
seasonal AS (
SELECT wk_num, wk_date, net_change,
AVG(net_change) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS seasonal_avg,
COUNT(*) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS n_prior
FROM s
)
SELECT COUNT(*) AS total_weeks, SUM(CASE WHEN seasonal_avg IS NULL THEN 1 ELSE 0 END) AS excluded_weeks,
MIN(wk_date) AS earliest_week, MAX(CASE WHEN seasonal_avg IS NULL THEN wk_date END) AS last_excluded_week
FROM seasonal
Sources
- Prokopczuk, Wese Simen & Wichmann (2021), "The Natural Gas Announcement Day Puzzle", Energy Journal 42(2) — Accepted manuscript; 1.04% per 1-SD surprise, 2-hour reaction window (99% of announcement-day return), 49.4%/50.6% pre/post split
- Linn & Zhu (2004), "Natural Gas Prices and the Gas Storage Report", Journal of Futures Markets 24(3) — Intraday NYMEX volatility elevated at release and up to 30 minutes after
- Natural Gas Intelligence: bearish storage surprise sinks futures ~1-2% on an ~11 Bcf miss — Contemporaneous trade example corroborating regression magnitude
- EIA weekly natural gas storage — national working gas (R48/SWO) — AskAmerica energy.eia_natural_gas_storage table; 2011-2026 observed
Show SQL
WITH s AS (SELECT CAST(report_date AS DATE) AS wk_date, volume_bcf, volume_bcf - LAG(volume_bcf) OVER (ORDER BY CAST(report_date AS DATE)) AS net_change, EXTRACT(WEEK FROM CAST(report_date AS DATE)) AS wk_num FROM energy.eia_natural_gas_storage WHERE eia_region_code='R48' AND storage_type_code='SWO'), seasonal AS (SELECT wk_num, wk_date, net_change, AVG(net_change) OVER (PARTITION BY wk_num ORDER BY wk_date ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING) AS seasonal_avg FROM s) SELECT wk_date, net_change, seasonal_avg, (net_change - seasonal_avg) AS surprise_proxy FROM seasonal WHERE seasonal_avg IS NOT NULL ORDER BY wk_date DESC - EIA Henry Hub Natural Gas Spot Price (RNGWHHD) — AskAmerica energy.eia_natural_gas_price table; daily, 1997-2026