Skip to content

cotmetrics 0.15.0: FlowRoles, speculator and retail series, weekly flow z - #52

Merged
mspinola merged 9 commits into
mainfrom
claude/cot-flows-primitive
Sep 27, 2026
Merged

mspinola merged 9 commits into
mainfrom
claude/cot-flows-primitive

Conversation

@mspinola

@mspinola mspinola commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

The cotmetrics half of "Scope, restated v2" (top of docs/handoffs/2026-09-26-cot-flow-states-cross-universe.md, and the superseding note at the top of docs/design/cot-flows.md): one role series per market. The branch's history carries the withdrawn designs (per-cohort flow columns, state vocabulary, markers); the last commit takes it to v2, and the net diff against main is only this:

  • cotmetrics.flow_roles, generated by scripts/analysis/gen_flow_roles.py from the propadj cohort-collapse table (same-week price correlation, threshold 0.15, gross-share floor 0.05, measured 2026-09-26): a FlowRoles entry per (report, symbol) naming the speculator, counterparty, neutral and inert cohorts and how retail behaves, with a per-report default (Managed Money / Leveraged Funds) for markets the table does not cover. ZT is the one market with no speculator. The table is the propadj one rather than the cache-close one v2 names; that was the user's call, because the cache close is additively back-adjusted and truncated HO, RB and OJ.
  • cotmetrics.flows: weekly_change (NaN across a missed report week and a contract-code switch, via Source Code from build_category_frame), flow_z (fixed 52-week window, min 26, no mean subtracted, NaN never 0), speculator_net, retail_net, and speculator_frame.
  • CotIndexer.get_speculator_data(name, lookback): the speculator net, its range index at the page lookback, its flow z and the retail net, for the Positioning Index panel on /analysis (mspinola/cot-analyzer PR to follow). get_category_data is unchanged from main.
  • Version 0.14.4 to 0.15.0. Nothing in signals, conditions, models or synthesis reads these columns; no forward return anywhere.

Not built: the Legacy back-fill of the speculator series before 2006 (not in the requested scope).

Verification: store-free suite 517 passed, 3 skipped (main 498); ruff clean; gen_flow_roles.py --check confirms the committed table is current. On the live store: Gold's speculator is Managed Money (retail spec-like), Corn's Managed Money (retail neutral), Euro's Asset Manager + Leveraged Funds (retail spec-like); Gold's speculator net equals its Managed Money net exactly.

Merge after #51 so the cited docs resolve.

🤖 Generated with Claude Code

mspinola and others added 2 commits September 26, 2026 14:12
… state vocabulary (0.15.0)

The Disaggregated and TFF category frames carried levels only: net,
percent of OI, the range index, a level z-score and momentum. The gold
flow-state study (docs/analysis/2026-09-26-cot-flow-states-gold.md) and
its 42-market replication showed that the week-over-week change per
cohort, scaled by that cohort's own trailing 52-week standard deviation
of weekly changes, is the picture a single week's bar chart cannot give,
and that nothing in the package computed it. This adds it as a column
family beside the existing ones, computed inside get_category_data, so
cot-analyzer can draw it without computing anything.

cotmetrics.flows is pure and store-free and takes the OUTPUT of
build_category_frame, inheriting the Swap_/Swap__ resolution, the dtype
coercion and present_categories. Per present category it emits dNet,
dLong, dShort, "Flow Z 52w" and "Flow Thin"; the window rides in the
column name because it is fixed and is NOT the page lookback (params.yaml
sets Custom to 8 weeks on OJ and 216 on PA, and a flow scaled by those
would be a different statistic per market). The z subtracts no mean and
uses no epsilon: the rolling mean of a first difference is ~0 by
construction, and indicators.calculate_z_score, whose own test pins that
a constant series yields 0, would label an empty window QUIET where the
honest answer is no reading. NaN is kept under min_periods and on a
zero-sd window. weekly_change masks a diff across an index hole longer
than 8 days and across a change of contract population, read from the
CFTC_Contract_Market_Code_Quotes column that build_category_frame now
carries as "Source Code": cotdata rewrites the plain code column to the
primary code on predecessor rows, so the Quotes twin is the only per-row
record of which contract a row came from. Measured on the live store the
seam mask fires on exactly LBR 2023-02-21, 02-28, 03-14 and RTY
2008-09-23, 2017-08-15, the gap mask on 6N 2006-07-11, and nowhere else;
those rows printed 2 to 4 sigma "flows" that were population switches.

The counterparty composite row (the sum of the cohorts on the other side
of the speculators' flow) is keyed per (report, symbol) from
cotmetrics.flow_roles, a generated data module, because the grouping the
gold study used, Producer/Merchant plus Swap Dealers, is a precious-metals
fact: elsewhere the swap dealers carry the index book and sit on neither
side. The roles come from the same-week price correlation of each
cohort's flow, measured on the ratio-adjusted marketdata series
(docs/analysis/2026-09-26-cot-cohort-roles-propadj.md, in the companion
docs PR), and only the 21 markets whose counterparty set is stable on
rolling 156-week windows under a criterion fixed before the run carry a
measured entry; the other 21 fall back to the per-report default with
the source recorded as such. The opinion triple that feeds the state is
the per-report default everywhere so the eight state names keep one
meaning. state_eligible is False on TFF equities, rates and crypto
(ADR-0005, still Proposed), so those markets get z cells and no state.
scripts/analysis/gen_flow_roles.py regenerates the module from the two
CSVs and refuses when the CSV's stability flag disagrees with the
recorded criterion.

flow_signs and flow_state are the gold script's classifier restated in
the fomo_zones style: object labels, None wherever an input is NA (the
first 26 weeks of every market are None, never QUIET, which both
reproducers got wrong), the eight Disaggregated names verbatim, a sign
string on TFF. It is a caption vocabulary and not a signal: the
pre-registered crucible test of VALUE_ACCUM failed the gauntlet on all
three pillars on 2026-09-26 (#50). Nothing here attaches a forward
return, nothing in signals, conditions, models, movers or synthesis
reads these columns, and no SETUP_* constant is minted. Held out on
purpose: dGross, straddle and derisk (fixed multipliers designed on gold
and untested anywhere), percent-of-OI flows, TFF state names.

get_category_data concatenates the flow frame after build_category_frame
and before the price merge, re-attaching attrs (pandas drops them on
concat, the same trap the merge already handles). No new lru_cache key,
no signature change; the frame gains 33 columns per market and every
pre-existing column is unchanged (verified on Gold against main). The
store-free suite gains 28 tests: the exact zero-sum identity on both
reports, the 26-row warm-up, NaN not 0 on a constant category, both
masks, the Swap_/Swap__ parity, the window fixed against lookbacks of 8
and 216, the roles coverage invariant, ADR-0005 eligibility, the
composite equal to the sum of its members, and the concat path through
get_category_data with a monkeypatched get_cot. Parity against the
42-market universe parquet: max difference exactly 0.0 on 40 markets;
LBR, RTY and 6N differ only on their masked rows and the 52 rows after.
cot-analyzer's own suite (897 tests) passes against this tree unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Decision 6 of docs/design/cot-flows.md, taken on the day's evidence. On
every Disaggregated market Producer/Merchant is the counterparty in at
least two thirds of the rolling 156-week windows, while on the nine
physicals that fail the stability criterion (CC, CL, GF, HE, HO, NG, RB,
SB, ZW) the swap dealers swing between neutral, speculator and
counterparty because they carry the index book. A fallback should be the
conservative core, and every cohort is still drawn as its own row, so
the default composite is Producer/Merchant with swap in the neutral set.
The precious-metals grouping (Producer/Merchant plus Swap Dealers)
survives only as a measured entry where the swap dealers do absorb flow:
GC, PA, PL, SI and, with Other Reportable, HG, SB, SI and HO. Regenerated
from the same CSVs; the 21 measured entries are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mspinola added a commit that referenced this pull request Sep 26, 2026
…fallback

The nine physicals that fail the stability criterion fall back to
Producer/Merchant with the swap dealers neutral, since Producer/Merchant is
the counterparty in at least two thirds of the rolling windows on every
physical and the swap dealers there carry the index book. GC keeps its
measured entry; LBR's measured set equals the new default. Shipped in #52.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mspinola and others added 2 commits September 26, 2026 18:08
… set

For cot-analyzer PR 3 (docs/design/cot-flows.md), so the view draws the level
markers and boxes the divergent weeks without computing either itself.

- build_flow_frame emits, per category whose range index the frame carries,
  "<prefix><header> Flow From Level" (the prior row's range index at the page
  lookback) and "<prefix><header> Flow Level Mark" (Int64: 1 for an active flow
  leaving the top of the range, -1 the bottom, 0 otherwise, NA where either input
  is missing). The one part of the flow family that follows the lookback, on
  purpose, and its names carry the header the way index_col does.
- level_marks(z, from_level): strict |z| > FLOW_ACTIVE_Z and strict 20 / 80
  (FLOW_LEVEL_LOW / HIGH, the Cowork mockup's cutoffs, unsearched).
- DIVERGENT_FLOW_STATES: the twelve all-active mixed-sign labels, six
  Disaggregated names and six TFF sign strings.
- The fixed-window test now compares every column except the level pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Series level with a different index than z was reindexed onto z's index and
came back all NA. Two Series must now share an index, and anything paired with
an array must match its length. The docstring records that the level inherits
the range index's straddle of a contract-code seam. Tests now assert marks of
both directions exist and that a seam week writes its level with no mark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mspinola and others added 5 commits September 26, 2026 18:56
Scoped by the "Scope, restated" block of the cross-universe handoff: PR 1 and
PR 2 only. The two reverts above take out the PR 3 material (prior-week level
columns at 20/80, the divergent-state set).

- flow_rows / MERGED_ROWS: on GC, SI, PL and PA, Producer/Merchant and Swap
  Dealers draw as one "Commercials" row (from the cohort-collapse table);
  everywhere else each category is its own row. build_flow_frame emits the
  merged row's dNet, legs, z and thin flag, its range index at the page
  lookback, and attrs["flow_rows"]. On the real store the Commercials dNet
  equals Legacy Commercial's net change exactly on Gold and Silver (1,058 weeks).
- level_marks(index): 1 at or above 90, -1 at or below 10, the same week's
  index, as the mockup script draws them (FLOW_LEVEL_LOW / HIGH, unsearched).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of the running panel: at the page's tuned lookback (26 weeks on
gold) a cohort sits in its top or bottom decile for months, and the markers
came in runs of fifteen that hid the cells.

- Every row carries "Level 156w", its net's range index over a fixed three
  years (min 52 weeks), the prototype's window; like the z window it is fixed
  and named with it, and the page lookback never reaches it.
- level_entries replaces level_marks: 1 on the week the level enters its top
  decile, -1 its bottom, inclusive at 90 / 10. On the live store that is 0 to
  10 marks per row over 104 weeks on Gold, Corn, S&P 500 and Euro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…low_z

"Scope, restated v2" (cross-universe handoff) withdraws the multi-row heatmap,
the state vocabulary and the markers, and keeps one role series per market.
This commit takes the branch to that surface; the net diff against main is now
only what v2 asks of cotmetrics.

- flow_roles.py, generated by gen_flow_roles.py from the propadj
  cohort-collapse table (threshold 0.15, gross-share floor 0.05, measured
  2026-09-26): FlowRoles per (report, symbol) with speculator, counterparty,
  neutral, inert and retail behaviour; per-report default Managed Money /
  Leveraged Funds for markets the table does not cover; ZT has no speculator.
  The propadj table, not the cache-close one v2 names, on the user's decision.
- flows.py: weekly_change (gap and contract-seam masks), flow_z (fixed 52w,
  min 26, no mean, NaN never 0), speculator_net, retail_net, and
  speculator_frame (net, range index at the page lookback, dNet, flow z,
  retail net, roles in attrs).
- CotIndexer.get_speculator_data(name, lookback): that frame for /analysis.
  get_category_data is main's again.
- Removed: per-category flow columns, the counterparty composite, flow signs
  and state names, merged rows, level marks, the 3-year level.

Legacy back-fill before 2006 is not built (not in the requested scope).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mspinola mspinola changed the title Add the weekly cohort flow primitive: dNet, flow z, per-market roles, state vocabulary (0.15.0) cotmetrics 0.15.0: FlowRoles, speculator and retail series, weekly flow z Sep 26, 2026
mspinola added a commit that referenced this pull request Sep 26, 2026
The display is one role series per market: the speculator's index and one
flow-z strip on the existing /analysis Positioning Index panel, built as
cotmetrics #52 and cot-analyzer #142. Section 3 now describes what is built
and where it departs from the v2 block (propadj roles table, page lookback,
retail as a label, no 20/80 bands, no Legacy back-fill yet); the withdrawn
heatmap, state strip, Flow view and board are a short record, full specs in
this file's history. Decisions, hazards, render checks and the don'ts follow.
Section 2 measurements unchanged. Carries Cowork's superseding note in
substance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mspinola added a commit that referenced this pull request Sep 27, 2026
"Scope, restated v2" (Cowork, 2026-09-26 evening) goes to the top verbatim,
with a short status note under it: built as cotmetrics #52 and cot-analyzer
#142, #140 and #141 closed, departures recorded in docs/design/cot-flows.md
section 3. The v1 block, which v2 supersedes and which had been written over
in the main checkout's copy, follows it verbatim as the record (recovered from
the session that read it), per the append-only rule for handoffs. The rest of
the file is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mspinola
mspinola merged commit 7a15ac2 into main Sep 27, 2026
4 checks passed
@mspinola
mspinola deleted the claude/cot-flows-primitive branch September 27, 2026 00:26
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