Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Twelve more years of grain positioning: ZC, ZS, ZW, ZO and KE now stitch their
pre-1998 codes.** The five CBOT/KCBT grains re-coded at the 1997/1998 year boundary
(`002601` to `002602`, `005601` to `005602`, `001601` to `001602`, `004601` to `004603`,
`001611` to `001612`), and the registry only carried the new codes, so the store's grain
history began 1998-01-06 although the CFTC publishes it from 1986-01-15. The old codes are
the same contracts reported in **thousands of bushels** rather than contracts: every
position column is divisible by 5 on 99.3 to 100% of pre-1998 rows and on none after,
open interest drops 5.0 to 5.7x at the seam (oats 3.8x, a real move), and the trader
counts do not move (corn 366 to 345, wheat 164 to 169). Each predecessor carries scale
`0.2`, the first use of a scale below 1 (lumber's 4.0 is the other). No overlap week.
The producer writes the five new per-code files on its next `--cot` run; until then
`get_cot` returns the post-1998 series exactly as before.

### Changed

- **`get_cot` overlap weeks go to the larger scaled open interest, not the primary code.**
Expand Down
4 changes: 2 additions & 2 deletions src/cotdata/cot.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ def get_cot(name: str, report: str = "legacy") -> pd.DataFrame:
h[_CODE_COL] = sym.cftc_code
frames.append(h)
frames = [f for f in frames if not f.empty]
if not frames:
return primary
if len(frames) < 2: # nothing to stitch (predecessor files absent,
return primary # e.g. before the producer's next run): primary as-is
combined = pd.concat(frames)
# Overlap weeks (a hand-off between codes) keep the row with the larger SCALED
# open interest, not the primary code. At the 2017 Russell hand-off the primary
Expand Down
17 changes: 17 additions & 0 deletions src/cotdata/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,29 @@ Energies:
cftc_code: "067411"

Grains:
# The five CBOT/KCBT grains below re-coded at the 1997/1998 year boundary. The old
# codes are the same contracts reported in THOUSANDS OF BUSHELS rather than contracts
# (5,000 bu each): every position column in the old files is divisible by 5 on 99.3 to
# 100% of rows, none are after the switch, open interest drops 5x at the seam and the
# trader counts do not move. Scale 0.2 converts to contracts. No overlap week: the old
# code ends 1997-12-30, the new one starts 1998-01-06. Measured 2026-10-10 against the
# CFTC deacot1986_2016 bundle; see CHANGELOG.
ZC:
cftc_code: "002602"
hist_codes:
- ["002601", 0.2]
ZS:
cftc_code: "005602"
hist_codes:
- ["005601", 0.2]
ZM:
cftc_code: "026603"
ZL:
cftc_code: "007601"
ZW:
cftc_code: "001602"
hist_codes:
- ["001601", 0.2]
# Added 2026-08-04 for crowdmon's contract-spec backlog: the smallest market of the ten
# candidates and the strongest on the criterion that decides, Managed Money share 0.433
# against a covered median of 0.137 (crowdmon docs/design/amendments-2026-08-03.md §C17,
Expand All @@ -71,8 +84,12 @@ Grains:
# Held-out generalization grains (new symbols, never in the deploy selection).
ZO: # Oats (CBOT)
cftc_code: "004603"
hist_codes:
- ["004601", 0.2]
KE: # KC HRW Wheat (Kansas City, CME)
cftc_code: "001612"
hist_codes:
- ["001611", 0.2]

# Dairy — a genuinely new asset class for the held-out generalization test: real
# commercial hedgers, never touched CMR selection. Collected here; cot-analyzer
Expand Down
9 changes: 9 additions & 0 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,12 @@ def test_golden_identity_checksum():
# symbols.
assert digest == "3dd3df8eb26d850654107e16568cddb6", (
"registry identity facts changed; update the expected checksum if intended")


# ── the 1998 grain re-code: five predecessors at scale 0.2 (thousand bushels → contracts) ─
def test_grain_predecessors_scaled_to_contracts():
expected = {"ZC": "002601", "ZS": "005601", "ZW": "001601", "ZO": "004601", "KE": "001611"}
for sym, old in expected.items():
assert hist_code_scales(symbol(sym).hist_codes) == [(old, 0.2)], sym
for sym in ("ZM", "ZL", "ZR"): # single-code since 1986, nothing to stitch
assert symbol(sym).hist_codes == (), sym
Loading