feat(Notebooks): Add risk-positioning use-case notebook - #39
Conversation
Percent-format .py notebook implementing the risk-positioning outline spec from #34. Runs end-to-end on the DEMO-KEY May 2025 preview slice; .ipynb and HTML are generated by CI. Registers the notebook in the README and preview-notebooks.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVxnJZYNRifP6pgJ5D13bz
szemyd
left a comment
There was a problem hiding this comment.
Quant review of notebooks/risk-positioning.py
Verdict: REQUEST CHANGES — posted as a comment review because GitHub blocks formal change-requests from the PR author's own identity. Treat the blocking items as merge-blockers; written so a follow-up agent can implement directly.
Reviewed the .py source against the executed .ipynb outputs. This is the most conceptually ambitious of the four use-case notebooks and much of the plumbing is right: strictly trailing ranks (the alpha-discovery transform, correctly reused), sign-preserving composite, the mark-price cross-check on the return source (corr 0.996 — good practice, keep it), overlap-of-windows disclosure, and left-joins with forward-only fill. The blockers are that the notebook's story and its own executed numbers point in opposite directions without acknowledgment, and two outcome-measurement bugs. Details inline; summary below.
Blocking
- The narrative contradicts the data and nobody mentions it (inline at the threshold table): in-window, higher crowding → higher 1-day forward returns (+0.63% at score ≥ 0.8 vs ~+0.33%/day baseline) and no worse drawdowns. The "most exposed to a sharp unwind" framing is asserted in Section 1 and re-asserted in the Takeaways as if demonstrated. Add an unconditional baseline row, state the in-window finding plainly (crowding behaved as momentum-confirmation this month), and rewrite the first Takeaway. A straight negative result reads as rigor; the current juxtaposition reads as spin.
forward_vol/forward_max_drawdownare off by one bar (inline, verified with synthetic data): both include the current bar (known at score time) and cover only h−1 future bars, inconsistent withforward_return's(t, t+h]. The vol leak is correlated with the score via vol clustering, flattering the "crowding → forward vol" panel. Two-line fix withrolling(h).…().shift(-h); re-run the event study after.
Important
- Chart 3's six "sharpest unwinds" are one 35-minute event cluster (inline), and the annotated prior scores (0.50–0.57, one 0.79) answer the chart's own question — "was the score elevated beforehand?" — with "no", which the text should say. De-cluster with a min-separation rule and state the answer.
- The OI component ranks raw per-5m OI change — effectively noise (inline): it doesn't measure the "leverage building" the text describes, and it mechanically compresses the composite's dispersion (only 2.7% of bars ≥ 0.9). Rank a 6h/1d OI change instead; also tighten
min_periodsso day-one "7-day ranks" on a handful of observations don't seed the deciles.
Minor
- Funding and basis both price the same perp-vs-index premium, so the composite is effectively ⅔ premium + ⅓ OI; one sentence on component correlation (or a printed 3×3 rank-correlation) would preempt the "aren't two of these the same thing?" question.
- Decile counts (892/decile) are computed on the score; tail rows with NaN 1-day outcomes still sit in those counts while dropping out of the means — trivial, but a
dropnaper horizon (or a footnote) would make the printed counts exact. pd.qcut(..., labels=range(1,11), duplicates="drop")raises if bins are ever actually dropped — latent breakage on lower-variance symbols/windows.- Section numbering jumps
## 3 → ## 5 → ## 8(template remnant).
Suggested acceptance criteria
- The threshold table carries a baseline row and a plain-language statement of the in-window result; Takeaways no longer claim unwind-risk evidence the month didn't provide.
- Forward-outcome helpers cover exactly
t+1 .. t+h; event study re-run. - Chart 3 shows de-clustered events and states what the prior scores were.
- OI component measures a multi-hour build; composite sections re-run and re-interpreted.
- Re-executed
.ipynb/HTML committed by CI so outputs match the revised source.
Generated by Claude Code
- Confront the data honestly: add an unconditional baseline row to the threshold table and a plain-language statement that in May 2025 elevated crowding preceded above-baseline returns and unexceptional drawdowns (momentum/carry-confirmation, not fragility). Reframe Section 1 as a hypothesis about conditional tails one month cannot confirm, and rewrite the first Takeaway to separate what was demonstrated (a clean, look-ahead-free construction) from what was not (unwind evidence). - Fix forward_vol / forward_max_drawdown off-by-one: both now cover exactly t+1..t+h via rolling(h).shift(-h), excluding the contemporaneous bar (which was leaking into the "crowding -> forward vol" panel); clip drawdown at 0. The event study re-runs on the corrected windows. - De-cluster Chart 3's "sharpest moves" (they were one 35-minute event as six overlapping windows) with a min-separation rule, and state that the prior scores were near-neutral — i.e. the score did not flag the drop in advance. - Rank a 1-day OI change (leverage build) instead of the ~white-noise 5-minute change, and require a quarter-window of history for the trailing ranks. - Print the 3x3 component rank correlation (funding and basis both price the premium); count valid observations per decile at the long horizon; guard the decile qcut; renumber sections. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVxnJZYNRifP6pgJ5D13bz
|
Thanks — all addressed in the latest commit (CI green on real data). Point by point: Blocking
Important
Minor: printed the 3×3 component rank correlation (funding–basis ≈ 0.42 on real data — they price the same premium); per-horizon valid counts; guarded the decile qcut; sections renumbered. Generated by Claude Code |
szemyd
left a comment
There was a problem hiding this comment.
Quant re-review (round 2) of notebooks/risk-positioning.py @ 4791054
Verdict: clean — no remaining blockers from the quant-review standpoint. Posted as a comment review (GitHub blocks formal states from the PR author's identity). All round-1 items are fixed; each verified against the re-executed .ipynb:
- Narrative vs data — resolved properly, not papered over. Section 1 is reframed as an explicit hypothesis about conditional tails that one month cannot confirm or refute; the threshold table now carries an unconditional baseline row (all bars +0.283%/1d vs +0.559% at ≥0.6 and +0.648% at ≥0.8, drawdowns unexceptional, vol up modestly); a "What the table says — read straight" block states that the score behaved as momentum/carry-confirmation this month; and the Takeaways are split into "what was demonstrated" (the construction) vs "what was not demonstrated" (unwind risk). This is the strongest version of this notebook: the negative result is the content, not a footnote.
- Forward-outcome off-by-one — fixed exactly as recommended:
ret.rolling(h).std().shift(-h)andclose.rolling(h).min().shift(-h) / close − 1clipped at 0, with docstrings explaining the[t+1, t+h]window and why the clip now carries the ≤ 0 semantics. All three forward outcomes are consistent, and the event study was re-run on the corrected definitions. - Event de-clustering — fixed with the greedy min-separation rule; the six sharpest 1-day drops now fall on six distinct dates (May 4, 11, 23, 27, 29, 30) instead of one 35-minute cluster, and the printed summary states the honest answer to the chart's question: prior scores ranged 0.22–0.77, mean 0.45 — near-neutral, the score did not flag the unwinds in advance, consistent with the threshold table.
- OI component — now ranks the trailing 1-day OI change with the rationale documented;
min_periods = max(10, window//4)keeps day-one ranks off tiny samples; and the printed component rank-correlation matrix (funding–basis 0.42) preempts the "two of these are the same premium" objection.
Minors also done: decile counts now count valid observations at the 1-day horizon (~800/decile instead of the nominal 892), the qcut decile binning is robust to dropped bins (labels=False + 1), and section numbering is sequential.
Optional polish (non-blocking)
- The PR description could add one line noting the notebook now explicitly reports the negative in-window result (high crowding preceded above-baseline returns in May 2025) — it's the most interesting thing in the notebook and makes the "risk-state diagnostic, not a crash forecast" framing concrete.
- As with the sibling PRs: the README/
preview-notebooks.jsonrows collide across #36–#39, so whichever merges last needs the trivial rebase.
Generated by Claude Code
|
Thanks. The PR description now notes the negative in-window result (elevated crowding preceded above-baseline returns and unexceptional drawdowns — momentum/carry-confirmation, not fragility), which makes the "risk-state diagnostic, not a crash forecast" framing concrete. On the README/ Generated by Claude Code |
Summary
Adds
notebooks/risk-positioning.py, implementing the risk-positioning outline from #34 (use case: Read positioning & risk).Combines funding, basis and open-interest change into a transparent crowding score (trailing rolling percentile ranks — the alpha-discovery transform), then runs a decile event study and an in-window threshold table.
DEMO-KEY.t+1 .. t+h; the composite keeps sign (>0.5 long-crowded, <0.5 short-crowded); derivative price is a cross-check on the OHLCV return source (corr 0.996), not a replacement..ipynband HTML are generated by CI. Adds one README row and onepreview-notebooks.jsonslug (sibling PRs touch these too — whichever merges last needs a trivial rebase).Part of splitting #35 into one PR per notebook. Implements #34.
🤖 Generated with Claude Code