DEV Community

Devil Scrapes
Devil Scrapes

Posted on

Filter the CFTC COT report for WHEAT and you get three different markets

Quick answer

Ask the CFTC's Commitments of Traders data for commodity_name = 'WHEAT' on the 2026-09-15 report and you don't get one market back. You get three: soft red winter wheat on the Chicago Board of Trade, hard red winter wheat on the same exchange, and hard red spring wheat on the MIAX Futures Exchange. They are different contracts with different open interest and different traders, and speculators can be positioned in opposite directions in them. Add them up and you get a number that describes no real market.

The CFTC Commitments of Traders Scraper pulls the COT reports straight from the CFTC's own public data portal. Most of the engineering is in the dull parts. Most of what you need to know to use the data well is in that one gotcha.

Why does one commodity filter return several markets?

Because commodity_name is a family, not a contract. In the CFTC's legacy dataset, the wheat contracts all carry commodity_name: "WHEAT". The field that tells them apart is market_and_exchange_names (WHEAT-SRW - CHICAGO BOARD OF TRADE), and the one that identifies a contract for good is cftc_contract_market_code (001602 for SRW, 001612 for HRW).

Our own cloud QA run shows why this matters. It used the Actor's default input (wheat, Chicago Board of Trade, report dates 2026-06-30 to 2026-09-15) and returned 24 rows: 12 weekly reports Ɨ 2 contracts. Speculator net positioning (non-commercial long minus short), straight from those rows:

report date   contract          open interest   speculator net
2026-06-30    WHEAT-SRW (CBOT)        406,723          -55,004
2026-06-30    WHEAT-HRW (CBOT)        260,913          -10,339
2026-09-15    WHEAT-SRW (CBOT)        485,138           +1,228
2026-09-15    WHEAT-HRW (CBOT)        310,836          +30,335
Enter fullscreen mode Exit fullscreen mode

Speculators ended the window net long in both contracts, but by very different amounts. HRW moved to +30,335 while SRW barely crossed zero. A chart summed on commodity_name blurs those two into one line. Group on cftc_contract_market_code and you keep them apart.

So the Actor's commodity input is an exact match on the CFTC's family name, and market is a substring match on the market-and-exchange name. You can use market to narrow a family down to the contract you actually trade.

What does the raw CFTC API actually hand you?

Strings. This is a real row from the CFTC's Socrata endpoint:

{
  "id": "260630001612F",
  "commodity_name": "WHEAT",
  "open_interest_all": "260913",
  "noncomm_positions_long_all": "66398",
  "report_date_as_yyyy_mm_dd": "2026-06-30T00:00:00.000"
}
Enter fullscreen mode Exit fullscreen mode

Every position count is a JSON string, and the field named ..._yyyy_mm_dd holds a full timestamp. Both are small problems, and both break a spreadsheet or a pandas pipeline quietly: string columns sort lexically, and "66398" > "100000". The Actor coerces every position field to a number and trims the date to real YYYY-MM-DD. If a value won't parse, it's set to null and a warning goes in the log. We don't guess a number for you.

The row id isn't random, either. 260630001612F is the report date (260630), the contract code (001612), and F for futures-only. It's stable, so you can dedupe on it when you re-pull overlapping windows.

How much history is there?

A lot. When we queried the legacy futures-only dataset it held 289,274 rows, running from 1986-01-15 to the 2026-09-15 report. The Actor covers all six COT variants: Legacy, Disaggregated, and Traders in Financial Futures, each as futures-only or futures-and-options-combined. You pick one with the reportType field. A second cloud run backfilled wheat from 2024 onward, returning 100 rows with 100 distinct IDs: 50 consecutive weekly reports for each of the two CBOT contracts.

What the Actor gives you

One row per market per report date, with the Socrata id, report_type, market-and-exchange name, report date, commodity name, contract market code, total open interest, non-commercial and commercial long and short positions, week-over-week change in open interest, and total trader count. You can filter by report type, commodity, market, and date range. There's no API key. We handle the pagination, retries with backoff on 408 / 429 / 503, and the type coercion, so the dataset exports to JSON, CSV, or Excel ready to chart.

Honest limitations 🚧

  • A capped run returns the oldest weeks, not the newest. The wheat backfill capped at 100 rows over 2024-01-01 to 2026-09-15 returned January through December 2024. To get this week's report, set dateFrom close to today rather than relying on the cap.
  • pct_of_open_interest_all is always 100 in the legacy dataset. We checked all 289,274 rows and it held one value. It's passed through for completeness, but it tells you nothing.
  • The CFTC's schedule is the Actor's schedule. Reports come out weekly and describe the prior Tuesday's positions. The Actor can't return a report before the CFTC publishes it.
  • Raw positions only. The Actor doesn't compute net positioning, COT index, or z-scores. Those take one subtraction or a rolling window downstream.
  • Use the Legacy report for position fields. The v1 row shape follows the Legacy report. The Disaggregated and TFF datasets split traders into different categories (managed money, swap dealers, leveraged funds and so on) and don't publish the Legacy commercial/non-commercial columns. On those report types, you get open interest, the week-over-week change, and trader count, but the long/short position fields come back null.

FAQ

Do I need a CFTC or Socrata API key?
No. The CFTC's public reporting portal is keyless open data.

What does it cost?
$3.20 per 1,000 rows under Pay-Per-Event: a $0.20 start fee plus $0.003 per row written to your dataset. A year of weekly reports for one contract is 52 rows.

Why did my WHEAT query return more markets than I expected?
Because WHEAT is a family of contracts. Add a market filter, or group on cftc_contract_market_code.

What if my filters match nothing?
The run succeeds with zero rows and a status message listing the filters it used, and you're charged only the start fee. If the CFTC portal can't be reached at all, the run fails loudly instead of returning a quiet, empty success.

→ CFTC Commitments of Traders Scraper on Apify


Built by Devil Scrapes. We handle the paging, retries and type coercion, so the numbers in your dataset are numbers.

Top comments (1)

Collapse
 
makeev profile image
Mikhail Makeev •

The family-versus-contract split is the one that bit me with tickers. Berkshire files its classes as BRK.A and BRK.B, the symbol table spells them with a dash, and a tag that didn't survive the dot-to-dash step left one earnings filing with no ticker at all, so it never showed up in the feed. Going the other way was worse: a filing tagged GOOG was invisible to anyone asking for GOOGL, so a query for one class now also returns its same-CIK siblings. Your id being date plus contract code plus F is a nice property. Did you ever see the CFTC reassign a contract code to a different market?