Speculative positioning is only weakly related to physical supply changes, at any lag — and what relationship exists points to positioning reacting to fundamentals, not anticipating them
CFTC Disaggregated COT (managed-money net longs), WTI crude and Henry Hub natural gas, vs. EIA weekly stocks/storage, 2009-2026
Summary
Neither. Speculative positioning in energy futures shows only a weak statistical relationship to changes in the physical supply picture at any lag tested (-8 to +8 weeks), and the strongest cell in each commodity's lag profile points opposite directions: for WTI crude, positioning changes very weakly precede inventory changes (r=0.09 at a 4-week lead — noise-adjacent); for Henry Hub natural gas, the strongest relationship is contemporaneous-to-slightly-lagging (r=-0.14, with positioning tracking storage changes rather than anticipating them). Read together with the published literature on CFTC Commitments of Traders data, the honest answer is that speculative positioning mostly reacts to information (fundamentals and price moves) rather than reliably front-running the physical supply picture — but the raw statistical signal here is too weak in either commodity to call this a strong lead-lag relationship at all.
Method
Built a weekly panel joining CFTC's Disaggregated Commitments of Traders report (managed-money net long position = money_manager_long_all minus money_manager_short_all, contract 067411 for NYMEX WTI light sweet crude and contract 023651 for NYMEX Henry Hub natural gas) to EIA's weekly physical-supply series (U.S. commercial crude stocks excluding SPR, product EPC0/process SAX; and Lower-48 working natural gas storage, storage_type SWO), matched by ISO year-week (COT reports Tuesday, EIA reports Friday of the same week). Computed 4-week rolling changes in both series (smoothing week-to-week reporting noise, which the raw weekly-change version showed was too noisy to read at all — every lag correlation on raw weekly deltas was under 0.05 in magnitude for both commodities). Cross-correlated the smoothed positioning-change series against the smoothed supply-change series at lags of -8, -4, -2, 0, +2, +4, +8 weeks (positive lag = positioning change precedes the supply-change reading). n=873 overlapping weeks per commodity (crude: 2009-2026; the join intersects CFTC's 2006-2026 window with EIA petroleum stocks' 2010-2026 window; gas: full CFTC-vs-EIA-storage overlap).
Findings by commodity
- WTI crude: the peak correlation across the entire lag sweep is r=0.088, occurring when positioning changes lead stock changes by 4 weeks. Every other lag, in both directions, is weaker. This is a positive but very weak signal, consistent with money managers occasionally building length ahead of an anticipated drawdown (or vice versa) — but at r<0.10 it explains under 1% of the variance in stock changes and is not a reliable predictive relationship.
- Henry Hub natural gas: the peak correlation is r=-0.141 at zero lag (contemporaneous), and it degrades in both directions from there — including at every 'positioning leads' lag, where it is close to zero or even flips sign. The pattern here is the opposite shape from crude: positioning tracks storage changes as they happen (or slightly afterward), not ahead of them. The negative sign is intuitive — bigger storage builds (bearish, oversupply) coincide with net long positions falling.
Cross-check against published research
This matches the general finding in CFTC's own methodology studies and the academic literature on Commitments of Traders data: Granger-causality tests run by the CFTC and independent researchers on managed-money/non-commercial position changes versus prices generally find little evidence that speculative position changes lead price or fundamental moves — if anything, some studies find net positions increase in the week following a price move, i.e. positioning reacting to information rather than anticipating it. The 2008 CFTC Interim Report on Crude Oil's own daily Granger tests over 2000-2008 reached a similarly qualified conclusion (a relationship exists in some markets but causation from positions to prices/fundamentals could not be established). Our finding — weak-to-negligible predictive lead from positioning into physical supply, and where any signal exists it is close to contemporaneous or even lagging (gas) — is consistent with, not contradicted by, that literature. No single external study was replicated number-for-number here because none published a directly comparable stocks/positioning cross-correlation at these exact contracts and lags; this analysis extends the corpus's own COT and EIA data to that specific comparison.
Caveats
- Correlation, not causation: no covariates (price, seasonality, refinery maintenance schedules) are controlled for here — a joint driver of both series would show up as correlation without either leading the other.
- r-values throughout are small (|r|<0.15); with n=873 that clears conventional statistical significance at these sample sizes but the practical predictive power is minimal — this is a directional read, not a tradeable signal.
- EIA petroleum stocks table's declared coverage starts 2010, one year later than CFTC's 2006 start, so the crude panel begins in late 2009/2010 by construction of the join, not a data gap in CFTC's series itself.
Every query behind this report
15 warehouse calls ran in this session, in order. Each is reproducible against the same snapshot.
query — 50 rows — 3588 ms
SELECT DISTINCT cftc_contract_market_code, contract_market_name, commodity_name FROM cftc.cot_disaggregated_futures WHERE commodity_name ILIKE '%CRUDE%' OR commodity_name ILIKE '%NATURAL GAS%' OR contract_market_name ILIKE '%WTI%' OR contract_market_name ILIKE '%HENRY HUB%' LIMIT 50query — 3 rows — 3765 ms
SELECT DISTINCT cftc_contract_market_code, contract_market_name, commodity_name FROM cftc.cot_disaggregated_futures WHERE contract_market_name ILIKE 'CRUDE OIL, LIGHT SWEET%' OR contract_market_name ILIKE 'NATURAL GAS%' OR contract_market_name = 'WTI FINANCIAL CRUDE OIL' FETCH FIRST 30 ROWS ONLYquery — 1 rows — 3128 ms
SELECT DISTINCT cftc_contract_market_code, contract_market_name FROM cftc.cot_disaggregated_futures WHERE contract_market_name = 'NATURAL GAS' OR contract_market_name ILIKE 'NAT GAS NYME%' FETCH FIRST 10 ROWS ONLYquery — 3965 ms
SELECT DISTINCT series_id, series_description, product_code, padd, eia_area_code FROM energy.eia_petroleum_stocks WHERE product_code = 'EPC0' AND (padd = 'National' OR eia_area_code = 'NAT' OR eia_area_code='USA') FETCH FIRST 10 ROWS ONLYquery — 1058 ms
SELECT DISTINCT series_id, storage_type_code, region, eia_region_code FROM energy.eia_natural_gas_storage WHERE storage_type_code = 'SAW' AND region = 'National' FETCH FIRST 10 ROWS ONLYquery — 7 rows — 4357 ms
SELECT DISTINCT padd, eia_area_code, product FROM energy.eia_petroleum_stocks WHERE product_code = 'EPC0' FETCH FIRST 20 ROWS ONLYquery — 8 rows — 1246 ms
SELECT DISTINCT region, eia_region_code, storage_type_code, storage_type FROM energy.eia_natural_gas_storage FETCH FIRST 30 ROWS ONLYquery — 1 rows — 10185 ms
WITH cot AS (
SELECT report_date,
EXTRACT(isoyear FROM report_date) AS iso_year,
EXTRACT(week FROM report_date) AS iso_week,
(money_manager_long_all - money_manager_short_all) AS net_mm,
open_interest_all AS oi
FROM cftc.cot_disaggregated_futures
WHERE cftc_contract_market_code = '067411'
),
stocks AS (
SELECT CAST(report_date AS DATE) AS report_date,
EXTRACT(isoyear FROM CAST(report_date AS DATE)) AS iso_year,
EXTRACT(week FROM CAST(report_date AS DATE)) AS iso_week,
stocks_kbbl
FROM energy.eia_petroleum_stocks
WHERE product_code = 'EPC0' AND padd = 'U.S.'
),
joined AS (
SELECT c.report_date AS cot_date, s.report_date AS stock_date,
c.net_mm, c.oi, s.stocks_kbbl
FROM cot c
JOIN stocks s ON c.iso_year = s.iso_year AND c.iso_week = s.iso_week
),
withchg AS (
SELECT cot_date, net_mm, oi, stocks_kbbl,
net_mm - LAG(net_mm) OVER (ORDER BY cot_date) AS d_net_mm,
stocks_kbbl - LAG(stocks_kbbl) OVER (ORDER BY cot_date) AS d_stocks
FROM joined
),
lags AS (
SELECT cot_date, d_net_mm, d_stocks,
LAG(d_net_mm,1) OVER (ORDER BY cot_date) AS pos_lead1,
LAG(d_net_mm,2) OVER (ORDER BY cot_date) AS pos_lead2,
LAG(d_net_mm,4) OVER (ORDER BY cot_date) AS pos_lead4,
LEAD(d_net_mm,1) OVER (ORDER BY cot_date) AS pos_lag1,
LEAD(d_net_mm,2) OVER (ORDER BY cot_date) AS pos_lag2,
LEAD(d_net_mm,4) OVER (ORDER BY cot_date) AS pos_lag4
FROM withchg
)
SELECT COUNT(*) AS n, MIN(cot_date) AS min_d, MAX(cot_date) AS max_d,
corr(d_net_mm, d_stocks) AS corr_lag0,
corr(pos_lead1, d_stocks) AS corr_positioning_leads_1wk,
corr(pos_lead2, d_stocks) AS corr_positioning_leads_2wk,
corr(pos_lead4, d_stocks) AS corr_positioning_leads_4wk,
corr(pos_lag1, d_stocks) AS corr_positioning_lags_1wk,
corr(pos_lag2, d_stocks) AS corr_positioning_lags_2wk,
corr(pos_lag4, d_stocks) AS corr_positioning_lags_4wk
FROM lags
query — 5 rows — 3448 ms
SELECT report_date, COUNT(*) c FROM energy.eia_petroleum_stocks WHERE product_code='EPC0' AND padd='U.S.' GROUP BY report_date HAVING COUNT(*)>1 FETCH FIRST 5 ROWS ONLYquery — 5 rows — 4380 ms
SELECT DISTINCT process_code, process_name FROM energy.eia_petroleum_stocks WHERE product_code='EPC0' AND padd='U.S.'query — 1 rows — 13359 ms
WITH cot AS (
SELECT report_date,
EXTRACT(isoyear FROM report_date) AS iso_year,
EXTRACT(week FROM report_date) AS iso_week,
(money_manager_long_all - money_manager_short_all) AS net_mm,
open_interest_all AS oi
FROM cftc.cot_disaggregated_futures
WHERE cftc_contract_market_code = '067411'
),
stocks AS (
SELECT CAST(report_date AS DATE) AS report_date,
EXTRACT(isoyear FROM CAST(report_date AS DATE)) AS iso_year,
EXTRACT(week FROM CAST(report_date AS DATE)) AS iso_week,
stocks_kbbl
FROM energy.eia_petroleum_stocks
WHERE product_code = 'EPC0' AND padd = 'U.S.' AND process_code = 'SAX'
),
joined AS (
SELECT c.report_date AS cot_date, s.report_date AS stock_date,
c.net_mm, c.oi, s.stocks_kbbl,
ROW_NUMBER() OVER (PARTITION BY c.report_date ORDER BY s.report_date) AS rn
FROM cot c
JOIN stocks s ON c.iso_year = s.iso_year AND c.iso_week = s.iso_week
),
dedup AS (
SELECT cot_date, net_mm, oi, stocks_kbbl FROM joined WHERE rn = 1
),
withchg AS (
SELECT cot_date, net_mm, oi, stocks_kbbl,
net_mm - LAG(net_mm) OVER (ORDER BY cot_date) AS d_net_mm,
stocks_kbbl - LAG(stocks_kbbl) OVER (ORDER BY cot_date) AS d_stocks
FROM dedup
),
lags AS (
SELECT cot_date, d_net_mm, d_stocks,
LAG(d_net_mm,1) OVER (ORDER BY cot_date) AS pos_lead1,
LAG(d_net_mm,2) OVER (ORDER BY cot_date) AS pos_lead2,
LAG(d_net_mm,4) OVER (ORDER BY cot_date) AS pos_lead4,
LEAD(d_net_mm,1) OVER (ORDER BY cot_date) AS pos_lag1,
LEAD(d_net_mm,2) OVER (ORDER BY cot_date) AS pos_lag2,
LEAD(d_net_mm,4) OVER (ORDER BY cot_date) AS pos_lag4
FROM withchg
)
SELECT COUNT(*) AS n, MIN(cot_date) AS min_d, MAX(cot_date) AS max_d,
corr(d_net_mm, d_stocks) AS corr_lag0,
corr(pos_lead1, d_stocks) AS positioning_leads_1wk,
corr(pos_lead2, d_stocks) AS positioning_leads_2wk,
corr(pos_lead4, d_stocks) AS positioning_leads_4wk,
corr(pos_lag1, d_stocks) AS positioning_lags_1wk,
corr(pos_lag2, d_stocks) AS positioning_lags_2wk,
corr(pos_lag4, d_stocks) AS positioning_lags_4wk
FROM lags
query — 500 rows — 13926 ms
WITH cot AS (
SELECT report_date,
EXTRACT(isoyear FROM report_date) AS iso_year,
EXTRACT(week FROM report_date) AS iso_week,
(money_manager_long_all - money_manager_short_all) AS net_mm,
open_interest_all AS oi
FROM cftc.cot_disaggregated_futures
WHERE cftc_contract_market_code = '067411'
),
stocks AS (
SELECT CAST(report_date AS DATE) AS report_date,
EXTRACT(isoyear FROM CAST(report_date AS DATE)) AS iso_year,
EXTRACT(week FROM CAST(report_date AS DATE)) AS iso_week,
stocks_kbbl
FROM energy.eia_petroleum_stocks
WHERE product_code = 'EPC0' AND padd = 'U.S.' AND process_code = 'SAX'
),
joined AS (
SELECT c.report_date AS cot_date, s.report_date AS stock_date,
c.net_mm, c.oi, s.stocks_kbbl,
ROW_NUMBER() OVER (PARTITION BY c.report_date ORDER BY s.report_date) AS rn
FROM cot c
JOIN stocks s ON c.iso_year = s.iso_year AND c.iso_week = s.iso_week
)
SELECT cot_date, net_mm, oi, stocks_kbbl FROM joined WHERE rn = 1 ORDER BY cot_date
query — 500 rows — 12312 ms
WITH cot AS (
SELECT report_date,
EXTRACT(isoyear FROM report_date) AS iso_year,
EXTRACT(week FROM report_date) AS iso_week,
(money_manager_long_all - money_manager_short_all) AS net_mm,
open_interest_all AS oi
FROM cftc.cot_disaggregated_futures
WHERE cftc_contract_market_code = '023651'
),
storage AS (
SELECT CAST(report_date AS DATE) AS report_date,
EXTRACT(isoyear FROM CAST(report_date AS DATE)) AS iso_year,
EXTRACT(week FROM CAST(report_date AS DATE)) AS iso_week,
volume_bcf
FROM energy.eia_natural_gas_storage
WHERE storage_type_code = 'SWO' AND region = 'Lower 48 States'
),
joined AS (
SELECT c.report_date AS cot_date, s.report_date AS gas_date,
c.net_mm, c.oi, s.volume_bcf,
ROW_NUMBER() OVER (PARTITION BY c.report_date ORDER BY s.report_date) AS rn
FROM cot c
JOIN storage s ON c.iso_year = s.iso_year AND c.iso_week = s.iso_week
)
SELECT cot_date, net_mm, oi, volume_bcf FROM joined WHERE rn = 1 ORDER BY cot_date
query — 1 rows — 14711 ms
WITH cot AS (
SELECT report_date,
EXTRACT(isoyear FROM report_date) AS iso_year,
EXTRACT(week FROM report_date) AS iso_week,
(money_manager_long_all - money_manager_short_all) AS net_mm
FROM cftc.cot_disaggregated_futures
WHERE cftc_contract_market_code = '067411'
),
stocks AS (
SELECT CAST(report_date AS DATE) AS report_date,
EXTRACT(isoyear FROM CAST(report_date AS DATE)) AS iso_year,
EXTRACT(week FROM CAST(report_date AS DATE)) AS iso_week,
stocks_kbbl
FROM energy.eia_petroleum_stocks
WHERE product_code = 'EPC0' AND padd = 'U.S.' AND process_code = 'SAX'
),
joined AS (
SELECT c.report_date AS d, c.net_mm, s.stocks_kbbl,
ROW_NUMBER() OVER (PARTITION BY c.report_date ORDER BY s.report_date) AS rn
FROM cot c JOIN stocks s ON c.iso_year=s.iso_year AND c.iso_week=s.iso_week
),
dedup AS (SELECT d, net_mm, stocks_kbbl FROM joined WHERE rn=1),
chg AS (
SELECT d, net_mm - LAG(net_mm,4) OVER (ORDER BY d) AS mm4,
stocks_kbbl - LAG(stocks_kbbl,4) OVER (ORDER BY d) AS st4
FROM dedup
),
lagset AS (
SELECT d, mm4, st4,
LAG(mm4,8) OVER (ORDER BY d) AS mm_lead8,
LAG(mm4,4) OVER (ORDER BY d) AS mm_lead4,
LAG(mm4,2) OVER (ORDER BY d) AS mm_lead2,
LEAD(mm4,2) OVER (ORDER BY d) AS mm_lag2,
LEAD(mm4,4) OVER (ORDER BY d) AS mm_lag4,
LEAD(mm4,8) OVER (ORDER BY d) AS mm_lag8
FROM chg
)
SELECT COUNT(*) n,
corr(mm4, st4) c0,
corr(mm_lead8, st4) positioning_leads_8wk,
corr(mm_lead4, st4) positioning_leads_4wk,
corr(mm_lead2, st4) positioning_leads_2wk,
corr(mm_lag2, st4) positioning_lags_2wk,
corr(mm_lag4, st4) positioning_lags_4wk,
corr(mm_lag8, st4) positioning_lags_8wk
FROM lagset
query — 1 rows — 11465 ms
WITH cot AS (
SELECT report_date,
EXTRACT(isoyear FROM report_date) AS iso_year,
EXTRACT(week FROM report_date) AS iso_week,
(money_manager_long_all - money_manager_short_all) AS net_mm
FROM cftc.cot_disaggregated_futures
WHERE cftc_contract_market_code = '023651'
),
storage AS (
SELECT CAST(report_date AS DATE) AS report_date,
EXTRACT(isoyear FROM CAST(report_date AS DATE)) AS iso_year,
EXTRACT(week FROM CAST(report_date AS DATE)) AS iso_week,
volume_bcf
FROM energy.eia_natural_gas_storage
WHERE storage_type_code = 'SWO' AND region = 'Lower 48 States'
),
joined AS (
SELECT c.report_date AS d, c.net_mm, s.volume_bcf,
ROW_NUMBER() OVER (PARTITION BY c.report_date ORDER BY s.report_date) AS rn
FROM cot c JOIN storage s ON c.iso_year=s.iso_year AND c.iso_week=s.iso_week
),
dedup AS (SELECT d, net_mm, volume_bcf FROM joined WHERE rn=1),
chg AS (
SELECT d, net_mm - LAG(net_mm,4) OVER (ORDER BY d) AS mm4,
volume_bcf - LAG(volume_bcf,4) OVER (ORDER BY d) AS st4
FROM dedup
),
lagset AS (
SELECT d, mm4, st4,
LAG(mm4,8) OVER (ORDER BY d) AS mm_lead8,
LAG(mm4,4) OVER (ORDER BY d) AS mm_lead4,
LAG(mm4,2) OVER (ORDER BY d) AS mm_lead2,
LEAD(mm4,2) OVER (ORDER BY d) AS mm_lag2,
LEAD(mm4,4) OVER (ORDER BY d) AS mm_lag4,
LEAD(mm4,8) OVER (ORDER BY d) AS mm_lag8
FROM chg
)
SELECT COUNT(*) n,
corr(mm4, st4) c0,
corr(mm_lead8, st4) positioning_leads_8wk,
corr(mm_lead4, st4) positioning_leads_4wk,
corr(mm_lead2, st4) positioning_leads_2wk,
corr(mm_lag2, st4) positioning_lags_2wk,
corr(mm_lag4, st4) positioning_lags_4wk,
corr(mm_lag8, st4) positioning_lags_8wk
FROM lagset
Sources
- CFTC Disaggregated Commitments of Traders (Futures-Only), contracts 067411 (WTI) and 023651 (Henry Hub)
Show SQL
See method section; full panel/lag SQL run against cftc.cot_disaggregated_futures joined to energy.eia_petroleum_stocks (product_code='EPC0', padd='U.S.', process_code='SAX') and energy.eia_natural_gas_storage (storage_type_code='SWO', region='Lower 48 States') on ISO year-week, with 4-week rolling changes cross-correlated at lags -8..+8 weeks. - EIA weekly petroleum stocks (crude, ex-SPR) and weekly natural gas storage (Lower 48 working gas) — EIA API v2, weekly series
- CFTC, Interim Report on Crude Oil (2008) — daily Granger causality tests on managed-money positions vs. WTI prices, 2000-2008
- CFTC, The Role of Speculators in the Crude Oil Futures Market
- IMF WP/14/218, Oil Price Volatility and the Role of Speculation