Skip to content

Ingest the CFTC Supplemental (Commodity Index Trader) report - #96

Merged
mspinola merged 2 commits into
mainfrom
claude/cit-supplemental-ingestion-8686f0
Aug 3, 2026
Merged

mspinola merged 2 commits into
mainfrom
claude/cit-supplemental-ingestion-8686f0

Conversation

@mspinola

@mspinola mspinola commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Description

Adds the CFTC Supplemental (Commodity Index Trader) report as a fourth report_type alongside legacy, disaggregated and tff: producer action, store domain, consumer reader, and vintage capture + canonicalisation from 2006 onward.

Executes docs/handoffs/2026-08-03-cit-supplemental.md, which is closed in this PR with every correction inline. Unblocks the companion handoff in crowdmon, which needs the index/non-index split to fix a Swap Dealer fragility weight that measurement showed doing opposite work on cocoa and gold.

Scope recorded in ADR-0002: the Supplemental is CFTC positioning, so it sits inside ADR-0007's narrowed boundary on the same argument that admitted vintage provenance. No change is proposed to ADR-0007 or ADR-0008 — this applies a rule they already state.

Purely additive. Nothing joins the natural key, VALUE_FIELDS or ALL_COLUMNS, so no stored row_sha256 moves and no existing observation is touched. A store that never runs the new action is unchanged.

Four things the data contradicted

All measured against the real 2006-2026 archives (21 files, 13,584 market-weeks). Full detail in docs/analysis/2026-08-03-cit-supplemental-measurements.md.

  • Coverage is 12 markets, then 13 from 2013, when Soybean Meal entered. Both circulating counts are right for part of the history. Six markets were renamed without changing code (four NYBOT to ICE in 2007, the two wheats in 2013), which is why coverage keys on market_code.
  • It is futures-and-options combined and the file cannot say so — no FutOnly_or_Combined column, so the existing _combined_flag would have defaulted it to False and put a guessed value in the natural key. Established by matching open interest against both Legacy series: 390/390 against combined, 0/390 against futures-only. Now asserted, with no override.
  • Index traders are carved out of three buckets, not the two CFTC's prose names. The third is non-commercial spreading: ~0% of the index book in 2006, ~9% now.
  • The fetch pattern differs from the other three. No 2006-2016 history bundle (404), and every year from 2006 returns 200 where disagg/TFF 404 before 2010, so annual_sources needed its own floor.

The identity exception rate, and why it is not alarming

Exact on ~55% of market-weeks, residual never above 2 contracts against a tolerance of 4, and flat across 21 years (per-year 51.6-59.5%, sd 2.1pp).

That is combined-report rounding, established by control rather than asserted: on the same 2026 weeks, Legacy futures-only is exact on 99.7% of rows while Legacy combined shows the identical +/-1 pattern. A combined report publishes delta-weighted option equivalents rounded to whole contracts, independently per category — the same n-addends mechanism rounding_tolerance was already derived from. validate() raises zero warnings across the full history.

Worth your eyes

The one deviation from written instructions. The brief asked for categories non_commercial / index_traders / non_reportable; this ships commercial / noncommercial / index_trader / nonreportable. A new spelling would make category == "nonreportable" silently miss every Supplemental row, while commercial — the genuinely confusable label — is identical either way, so the distinguish-by-spelling argument fails on exactly the category that needed it. report_type does the disambiguating, as the brief itself specifies. Reasoning in the closed handoff §2.

A cold-context review ran before this PR and found three real defects, all in the new coverage code (second commit): a lexicographic-max name rule that reported the stale pre-2007 NYBOT name on four live markets, a null-handling path that produced the literal "nan" on pandas 2 and a TypeError on pandas 3, and coverage_changes reporting an ingest gap as a market entry. It also found that _resolve — which sits on the permanent-hash path — had shipped untested.

Limits, stated rather than implied

  • The 2018 reclassification is not recoverable and this does not make it so. CFTC serves current state only, so the 2018 archive we hold today is already the restatement. The vintage store detects the next one. Category-migration detection stays deferred; earliest sensible date 2026-11-01.
  • Index Traders is a Legacy-taxonomy carve-out and does not nest inside Disaggregated's Swap Dealer. The two cannot be differenced to isolate levered swap flow.
  • No analysis here, deliberately. That is the companion handoff's job, against a merged and tested ingestion.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have run tests (e.g. pytest) and they pass locally
  • Any dependent changes have been merged and published in downstream modules

Verified locally: 289 tests pass on both pandas 2 and pandas 3 (Python 3.11 only — the 3.10/3.12/3.13/3.14 legs are what this PR's CI is for), ruff check src tests scripts clean, and a full green run against an empty store with no network. Separately reproduced end to end against the live CFTC archives: 21 raw snapshots captured, 54,336 observations, zero revisions, zero warnings, and a full 21-snapshot replay wrote nothing.

🤖 Generated with Claude Code

mspinola and others added 2 commits August 3, 2026 09:09
Adds `supplemental` as a fourth report_type: a new producer action
(--cot-supplemental, also in --cot-all) writing cot_supplemental/, a
cotdata.get_cot(report="supplemental") reader, and vintage capture +
canonicalisation from 2006 onward. Purely additive: nothing joins the natural
key, VALUE_FIELDS or ALL_COLUMNS, so no stored row_sha256 moves.

Four things came back different from the brief and from CFTC's own prose, all
measured against the real 2006-2026 archives:

- Coverage is 12 markets, then 13 from 2013 when Soybean Meal entered. Both
  circulating counts are right for part of the history. Three markets were
  renamed without changing code, so coverage keys on market_code.
- The report is futures-and-options combined and the file cannot say so: it
  carries no FutOnly_or_Combined column, so _combined_flag would have defaulted
  it to False and put a guessed value in the natural key. Established by
  matching open interest against both Legacy series, 390/390 against combined
  and 0/390 against futures-only, so canonicalize_supplemental asserts it.
- Index traders are carved out of three buckets, not the two CFTC describes.
  The third is non-commercial spreading, ~0% of the index book in 2006 and ~9%
  now.
- No 2006-2016 history bundle, and every year from 2006 returns 200 where
  disagg/TFF 404 before 2010, so annual_sources needed its own floor.

The open-interest identity is exact on only ~55% of market-weeks and that is
rounding: the residual never exceeds 2 contracts against a tolerance of 4, the
rate is flat across 21 years, and on the same weeks Legacy futures-only is
exact on 99.7% while Legacy combined shows the identical +/-1 pattern.
Combined reports round delta-weighted option equivalents per category, which is
the n-addends mechanism rounding_tolerance was already derived from.

Scope recorded in ADR-0002; measurements in docs/analysis/; the executed
handoff is closed in docs/handoffs/ with every correction inline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A cold-context review of the previous commit found three real defects, all in
the new coverage code, which was its least-exercised part. None touched the
store's existing output or the permanent row_sha256.

- derive_coverage/coverage() picked the market name by lexicographic max, which
  is not "current" and reported the STALE name on four real markets: Cocoa,
  Cotton, Sugar and Coffee all renamed from "... - NEW YORK BOARD OF TRADE" to
  "... - ICE FUTURES U.S." in 2007, and 'I' < 'N'. The wheat pair came out right
  under either rule, which is how it survived a first reading. Now takes the
  name at the latest report date.
- market_name is descriptive, so validate() does not require it and a canonical
  frame may carry nulls. astype(str) turned those into the literal "nan" under
  pandas 2, which is lowercase and beats every real uppercase CFTC name, and
  left a float in place under pandas 3, where the comparison raised TypeError.
  Both are now routed through clean_market_name. The suite runs green on both
  pandas majors.
- coverage_changes compared whatever years were present as though they were
  adjacent. A store holding 2006 and 2026 and nothing between reported Soybean
  Meal as entering in 2026 — an ingest artifact presented as a fact about the
  market. It now compares consecutive years only and emits a `gap` record.

Also from the review:

- The provider and the vintage layer had two coverage implementations that had
  already drifted on the name rule. Now one shared _coverage_from, with their
  agreement asserted in full rather than on year/code alone. The producer also
  derives coverage over what it WROTE rather than over everything in the files.
- _cmd_coverage wrote before checking for an empty result, so pointing
  COTDATA_STORE at the wrong root truncated a good artifact and then said
  "nothing ingested yet". write_coverage now also takes the ingest write lock,
  for the read underneath it rather than for the atomic write.
- _resolve shipped with no tests despite sitting on the row_sha256 path. Added,
  including the property that made the widening safe: no candidate spelling of
  one field is a candidate of another, and no target matches two real columns in
  the shipped 2026 headers. It also now handles Spead/Spread and composes up to
  three substitutions, so the realistic upstream cleanup of
  NComm_Postions_Spread_All_NoCIT (two defects in one name) resolves instead of
  raising. A genuine rename still raises.
- A vacuous test assertion (a prefix no CFTC column has ever had) replaced with
  one that checks what its name claims.
- Prose corrections: six renames rather than three, KCBT to CBOT rather than to
  CME, and the CHANGELOG had attached the breach-rate range (62.6-71.7%) to the
  exact-rate sentence (~55%), which is arithmetically impossible. Per-year exact
  is 51.6-59.5%. The analysis doc is corrected in place, on the same day and
  before it existed off this branch, and frozen from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mspinola
mspinola merged commit 730f3ac into main Aug 3, 2026
5 checks passed
@mspinola
mspinola deleted the claude/cit-supplemental-ingestion-8686f0 branch August 3, 2026 15:31
mspinola added a commit that referenced this pull request Aug 3, 2026
Removes docs/handoffs/2026-08-03-index-share.md, added by #97. Its own header
names crowdmon as its home and target:

    Lives at: crowdmon/docs/handoffs/2026-08-03-index-share.md
    Target:   Claude Code session, crowdmon worktree

Now committed there as crowdmon#36 (c243c125), body byte-identical, so this is
the other half of one move rather than a deletion.

The location matters beyond filing. The handoff orders analysis relating index
flow to crowdmon's Swap Dealer fragility weight, and
crowdmon/tests/test_boundaries.py refuses an import of crucible precisely so a
package cannot render a verdict on its own output. A work order for that
analysis sitting in the producer repo invites it to be executed here, against
the store rather than across the seam. The #96 brief already said so in terms:
"Do not do the analysis. That is the companion handoff in crowdmon, and it
should run against a merged, tested ingestion rather than alongside one."

What stays is 2026-08-03-cit-supplemental.md, the ingestion half, closed as
complete. That one is genuinely cotdata's: it was executed here.

Docs only. No package code, no tests, no store shape touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant