Offside: a cohort's cost basis marked to market, per contract - #28
Merged
Conversation
Every other level metric here is a statement about position SIZE. The range index
and z-score say how unusual the size is; exposure says how large it is in dollars
or dollar risk. None says whether the holders are winning or losing, which is a
first-moment claim and needs a price to compare against. This adds the comparison:
average-cost accounting on the weekly net position, marked at the report Tuesday,
in units of the market's own trailing weekly volatility.
sign(N), not N. The reading is per CONTRACT, so a 400-lot cohort and a 400,000-lot
cohort read alike when both sit the same distance under. That is the opposite
choice from exposure, which multiplies by size and by sigma; these are complements
and the product would be a third quantity (a dollar loss) that neither returns.
Lives beside exposure rather than in CotIndexer for three reasons: it needs an
explicit propadj tier, which tests/test_price_tier.py forbids in that module by
design; it is lookback-invariant; and per-cohort columns in a flat {symbol}.parquet
would be three more that nothing else reads. No new parquet column, so
METRICS_CACHE_VERSION is untouched.
propadj is a guard, not a preference: additive back-adjustment drives 14 of 45
markets through zero (HO 90.6% of bars, RB 77.1%), and log(P/B) is undefined either
side of that while still returning a number. Reproduces the published CL January
2015 figures exactly (basis 99.47, price 52.55, offside -15.2 sigma on 274,271 long
contracts, while the range index read 46.6 and saw nothing).
Carries the measured limits in the docstring rather than in a doc that can drift:
extremity is built by winners and is close to the opposite of offside (14.2%
underwater at an index extreme against 42.7% when neutral); Commercials sit
underwater 65.6% of the time by design, so is_hedge_leg exists to label rather than
rank them; and the pre-registered capitulation test returned adverse-move proxy, so
this is a display and not a timing signal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
cotmetrics.offside: a cohort's average cost basis, marked to market, in units of the market's own trailing weekly volatility.Every other level metric in this package is a statement about position SIZE. The range index and z-score say how unusual the size is against its own history;
exposuresays how large it is in dollars or dollar risk. None of them says whether the people holding it are winning or losing, which is a first-moment claim and needs a price to compare against.Two design points worth reviewing
sign(N), notN. The reading is per CONTRACT, so a 400-lot cohort and a 400,000-lot cohort read alike when both sit the same distance under. That is the opposite choice fromexposure, which multiplies by size and by sigma to answer how much selling a vol move must force. The two are complements; their product would be a dollar loss, a third quantity neither module returns.It lives beside
exposure, not inCotIndexer. Three reasons converge: it needs an explicitpropadjtier, whichtests/test_price_tier.pyforbids inside that module by design; it is lookback-invariant, where everything inprocess_lookbackis window-scoped; and per-cohort columns in a flat{symbol}.parquetwould be three more that nothing else reads. Consequence: no new parquet column, soMETRICS_CACHE_VERSIONis untouched and existing caches stay valid.The price tier is a guard, not a preference
backadjlooks right (a cost basis is a difference in points) and is unusable: additive back-adjustment drives 14 of 45 markets through zero (HO 90.6% of bars, RB 77.1%, OJ 73.6%), andlog(P/B)is undefined either side of that while still returning a plausible number.propadjis non-positive on exactly one print across the universe (CL 2020-04-20), which is masked. Same reasoningexposure.sigma_seriesuses, and_basis_closerefuses any other tier by name.Verification
Reproduces the published figures from
npf/docs/analysis/2026-08-23-offside-positioning-measure.mdexactly, through an independent implementation:18 new store-free tests (the recurrence branch by branch, sign symmetry, size-invariance, vol-scaling, and every guard). Full suite 379 passed, ruff clean.
What the docstring carries, and why it is there rather than in a doc
Three measured results that a reader of this module would otherwise get wrong:
is_hedge_legexists to label rather than rank them.Version 0.6.0 to 0.7.0. Consumed by cot-analyzer's Offside column (mspinola/cot-analyzer#59), which pins
>=0.7.0.🤖 Generated with Claude Code