Skip to content

Value each week at the multiplier in force that week - #24

Merged
mspinola merged 1 commit into
mainfrom
claude/effective-dated-exposure
Aug 23, 2026
Merged

Value each week at the multiplier in force that week#24
mspinola merged 1 commit into
mainfrom
claude/effective-dated-exposure

Conversation

@mspinola

Copy link
Copy Markdown
Owner

The consumer half of #22. This is the change that actually fixes the 740 understated Russell weeks.

Important

Merge marketdata#23 first. CI checks out marketdata's default branch, and this declares crucible-marketdata>=0.2.0. Until that merges, cotmetrics CI fails at the dependency-floor step with crucible-marketdata: installed 0.1.0 does NOT satisfy declared '>=0.2.0'. That is the intended behaviour, not a flake.

The problem

exposure applied one point value, today's, to a market's entire COT history. ICE halved the Russell multiplier from $100 to $50 effective 2016-12-05 and converted each open lot into two, with no CFTC rename to mark it, so 740 of RTY's 1,247 priced weeks were valued at half their true notional and dollar risk.

The shape of the fix

The question is split in two rather than changing point_values in place:

answers source
point_values() can this market be priced at all? current contract_specs, unchanged
point_value_series() what was a given week worth? marketdata's regime table, else the flat current value

Keeping the membership gate on the current table matters: a regime table cannot say a market is unpriceable, and MFS/MME must keep dropping out and naming themselves. market_exposure now multiplies elementwise and reports the week's multiplier in the point_value column, so a reader checking why an old week is large can see what produced it.

Measured against the real store

before after
RTY weeks at 100.0 / 50.0 0 / 1,247 740 / 507
median |notional|, pre-2016-12-06 $1.33bn $2.66bn
median |notional|, post-cut $2.99bn $2.99bn (unchanged)
Russell share of Equities |notional|, pre-cut 11.9% 21.2%
markets whose numbers changed 1 of 47 — all others bit-identical

Every figure matches what #22 predicted, including the 740 count and the 21.2% share.

The percentile is why this mattered more than the level. expanding_pct_rank ranks each week against its own past, so an under-scaled first half made every post-2016 reading come out more extreme rather than merely smaller.

Two deliberate refusals

Against a marketdata without the regime API this raises and names the version. It does not fall back to the flat series. That fallback would return a plausible number that is wrong by 2x on RTY, and nothing downstream could tell — which is the exact failure class this whole thread of work exists to remove.

Where a multiplier was never established, the dollars are NaN, not a guess (LBR before 1995). A gap is visible on a chart; a guess is not. Tested that such weeks drop out of aggregate_exposure rather than silently entering a sum as a zero.

A gap found on the way

crucible-marketdata was missing from scripts/check_dep_floors.py's INTERNAL set, so the floor declared in pyproject.toml was never actually checked. It did not matter while the floor was a nominal >=0.1.0; it matters now that it is a correctness floor. npf's and cot-analyzer's copies of the same script already listed it. Verified the check now fails against 0.1.0 and passes against 0.2.0.

Checks

  • pytest tests/ -q: 352 passed (8 new), up from 344
  • ruff check src tests: clean
  • check_dep_floors.py: passes on 0.2.0, proven to fail on 0.1.0
  • end-to-end aggregate_exposure: Equities 1,109 weeks / 6 markets, whole book 45 markets, MFS and MME still dropped with the same message
  • no em dashes

The audit doc in #22 is point-in-time and deliberately not amended, per the analysis/ lifecycle rule. This PR is the record of what was done about it.

Still open

npf validation/costs.py carries the same defect at roughly a tenth the severity: it applies today's Point Value and Tick Value to historical trades, and the slippage term cancels because both halve, leaving only the fixed commission term wrong. Not in this PR.

Recap in plain language

cotmetrics now values each week using the contract multiplier that was actually in force that week, instead of today's. Only one market in 47 is affected, but it is affected badly: the Russell's dollar figures before December 2016 were exactly half what they should have been, which also made every reading since then look more extreme than it was. Everything else in the book is unchanged to the last digit.

This is a confirmed correction, not a judgment call: the exchange notice gives the date and the ratio, and every number here matches what the audit predicted before the code was written. The dollar-risk percentile's prerequisite is now met.

🤖 Generated with Claude Code

exposure applied ONE point value, today's, to a market's whole COT history.
That is wrong wherever an exchange re-denominated a contract. ICE halved the
Russell multiplier from $100 to $50 on 2016-12-05 and converted each open lot
into two, with no CFTC rename to mark it, so 740 of RTY's 1,247 priced weeks
were valued at half their true notional and dollar risk. Audited in #22, data
added in marketdata#23.

Splits the question in two rather than changing point_values in place:

  point_values()          can this market be priced at all? Current spec,
                          unchanged, still the membership gate, so a market with
                          no multiplier still drops out naming itself.
  point_value_series()    what was a given WEEK worth? Defers to marketdata's
                          regime table for declared symbols and returns the flat
                          current value otherwise.

market_exposure multiplies elementwise and reports the week's multiplier in the
point_value column, so a reader checking why an old week is large can see what
produced it. Measured against the real store: RTY 740 weeks at 100.0 and 507 at
50.0, median pre-cut |notional| $1.33bn -> $2.66bn, post-cut unchanged, Russell
share of Equities 11.9% -> 21.2%. All 46 other markets bit-identical.

The percentile is the reason this mattered more than the level. expanding_pct_rank
ranks each week against its own past, so an under-scaled first half made every
post-2016 reading come out more extreme rather than merely smaller.

Against a marketdata without the regime API this RAISES and names the version.
Falling back to the flat series would return a plausible number that is wrong by
2x on RTY, and nothing downstream could tell. Same reasoning as returning NaN
where a multiplier was never established (LBR before 1995) rather than a guess:
a gap is visible on a chart and a guess is not.

Floor: crucible-marketdata>=0.2.0, a correctness floor rather than a convenience
one. crucible-marketdata was also missing from check_dep_floors.py's INTERNAL
set, so that floor would not have been checked at all; npf's and cot-analyzer's
copies already listed it. Verified the check now fails against 0.1.0.

352 passed (8 new), ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mspinola
mspinola merged commit 0c04e8c into main Aug 23, 2026
4 of 8 checks passed
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