diff --git a/CHANGELOG.md b/CHANGELOG.md index e011a0a..c4b80a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.** diff --git a/src/cotdata/cot.py b/src/cotdata/cot.py index 5cde2a2..a794539 100644 --- a/src/cotdata/cot.py +++ b/src/cotdata/cot.py @@ -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 diff --git a/src/cotdata/registry.yaml b/src/cotdata/registry.yaml index 92a198a..028bfd0 100644 --- a/src/cotdata/registry.yaml +++ b/src/cotdata/registry.yaml @@ -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, @@ -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 diff --git a/tests/test_registry.py b/tests/test_registry.py index c402c9d..1d5e327 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -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