← All studies · askamerica.ai

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

Speculative positioning vs. physical supply: cross-correlation by lag CFTC Disaggregated COT (managed-money net longs) vs. EIA weekly stocks/storage, 4-week changes, 2009-2026 WTI crude: positioning vs. commercial stocks -0.15 -0.10 -0.05 0.00 0.05 0.10 lag (weeks) correlation (r) positioning leads 8wk positioning leads 4wk positioning leads 2wk contemporaneous positioning lags 2wk positioning lags 4wk positioning lags 8wk n=873 weeks. Peak r=0.09 at positioning leading by 4 weeks - weak. Henry Hub gas: positioning vs. Lower-48 storage -0.15 -0.10 -0.05 0.00 0.05 0.10 lag (weeks) correlation (r) positioning leads 8wk positioning leads 4wk positioning leads 2wk contemporaneous positioning lags 2wk positioning lags 4wk positioning lags 8wk n=873 weeks. Peak |r|=0.14 at contemporaneous - positioning tracks storage, doesn't anticipate it. Crude: strongest lead-lag r r = 0.09 positioning leads by ~4wk Gas: strongest lead-lag r r = -0.14 contemporaneous, not leading Weekly changes joined on ISO week (COT Tuesday report vs EIA weekly stocks/storage report). All correlations are weak (|r| < 0.15); none support a strong lead-lag relationship in either direction. AskAmerica · askamerica.ai
SVG

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

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

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 50
query — 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 ONLY
query — 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 ONLY
query — 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 ONLY
query — 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 ONLY
query — 7 rows — 4357 ms
SELECT DISTINCT padd, eia_area_code, product FROM energy.eia_petroleum_stocks WHERE product_code = 'EPC0' FETCH FIRST 20 ROWS ONLY
query — 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 ONLY
query — 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 ONLY
query — 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

  1. 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.
  2. EIA weekly petroleum stocks (crude, ex-SPR) and weekly natural gas storage (Lower 48 working gas) — EIA API v2, weekly series
  3. CFTC, Interim Report on Crude Oil (2008) — daily Granger causality tests on managed-money positions vs. WTI prices, 2000-2008
  4. CFTC, The Role of Speculators in the Crude Oil Futures Market
  5. IMF WP/14/218, Oil Price Volatility and the Role of Speculation