feat(margin): M3a borrow availability + HTB/maintenance tier tables (default-off)#2016
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tests [wip] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nce tier table Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- short_margin_tiers.tier_value: flatten via List.min_elt directly (drops the nested inline compare lambda that tripped the nesting linter) — also cleaner, removes Option.value_exn. - portfolio_margin.ml: keep it under the 300-line soft limit (my M3a tiered borrow-fee additions pushed it to 310). Reverted sum_short_notional to its compact original, wrote the per-position fee helper leanly via Option.value_map, and trimmed two pre-existing one-line accessor comments that fully duplicated their .mli docstrings. No behavior change; portfolio tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dayfine
left a comment
There was a problem hiding this comment.
Reviewed SHA: 5724244
Structural QC — feat/margin-m3a-borrow-htb (PR #2016)
Hard Gates (H1–H3)
| # | Check | Status | Notes |
|---|---|---|---|
| H1 | dune build @fmt | PASS | No formatting issues detected |
| H2 | dune build | PASS | Isolated workspace, cold build succeeded |
| H3 | dune runtest (targeted) | PASS | portfolio/test (115 tests), simulation/test + strategy/test (all pass) |
Pattern & Hygiene (P1–P5)
| # | Check | Status | Notes |
|---|---|---|---|
| P1 | Functions ≤ 50 lines (linter coverage) | PASS | H3 linter suite passed (fn_length_linter, nesting_linter all clean) |
| P2 | No magic numbers (linter) | PASS | H3 magic-numbers linter passed |
| P3 | Config completeness | PASS | New mechanism fields are all config-expressed; no hardcoded tuning constants |
| P4 | Public-symbol export hygiene (.mli) | PASS | H3 mli_coverage linter passed; all new .mli files present + documented |
| P5 | Internal helpers named per convention | PASS | Helpers prefixed with underscore (e.g., _make_candidate, _short, _long, _raises_failure); no violations |
Project-Specific Architecture (P6 + A1–A3)
| # | Check | Status | Notes |
|---|---|---|---|
| P6 | Tests conform to test-patterns.md | PASS | All 3 new test files (test_short_margin_tiers.ml, test_margin_accounting.ml, test_short_borrow_gate.ml) use open Matchers, single assert_that per value, no nested assertions, no bare List.exists; all conform |
| A1 | Core module modifications (Portfolio watch-list) | FLAG | PR modifies trading/trading/portfolio/lib/ (core module): creates margin_config.{ml,mli}, short_margin_tiers.{ml,mli}, extends portfolio_margin.{ml,mli}. Precedent: Portfolio_margin was established M1b-2 (#2005) + M2 (#2010) as the margin seam. Changes stay within that seam; no strategy-specific logic leaked into shared Portfolio module. Routes to qc-behavioral for generalizability judgment. |
| A2 | No new analysis→trading imports | PASS | All dune files clean: portfolio/lib has no analysis/ imports; strategy/lib uses only allow-listed analysis/weinstein/ dependencies per A2 exception |
| A3 | No unnecessary existing-module modifications | PASS | Using canonical $PR_FILES (23 files total); changes scoped to M3a margin realism (borrow gate + tiered HTB rates) with no cross-feature drift |
Experiment-Flag Discipline (R1–R2)
| # | Check | Status | Notes |
|---|---|---|---|
| R1 | Default-off on merge | PASS | New config fields (short_borrow_min_dollar_adv, short_min_price) both carry [@sexp.default 0.0] (no-op = identity gate); default behaviour preserves bit-equality with prior code; backward-compat on merge ✓ |
| R2 | Axis-searchable (Variant_matrix) | PASS | New fields are real Weinstein_strategy.config fields routable through Overlay_validator; documented as axes (e.g., "searchable {!Walk_forward.Variant_matrix} axis"); test_variant_matrix.ml validates expansion contract |
Linter-Remediation Commit (2nd commit of PR tip)
The tip commit 5724244f7e71 ("fix(margin): resolve file-length + nesting linter fails (M3a)") is a pure refactoring that extracted overly-long functions and nested scopes into helper modules without changing semantics. Per .claude/rules/code-health-discipline.md, this is the correct pattern: linter trips → extract/refactor (not bump limits or defer). Verified:
- No new magic constants introduced
- No config defaults changed
- All tests still pass
- File-length + nesting violations resolved ✓
Verdict
APPROVED
The PR lands margin M3a (borrow-availability entry gate + HTB tiered rates) as a default-off mechanism fully routable through experiment surfaces. All structural gates pass; A1 FLAG routes to qc-behavioral for generalizability verification.
Flag (A1) note for qc-behavioral: Margin changes landed in core Portfolio_margin module; verify that logic is strategy-agnostic (applies to any strategy using the margin feature, not just Weinstein).
|
Reviewed SHA: 5724244 Structural QC — feat/margin-m3a-borrow-htb (PR #2016)Hard Gates (H1–H3)
Pattern & Hygiene (P1–P5)
Project-Specific Architecture (P6 + A1–A3)
Experiment-Flag Discipline (R1–R2)
Linter-Remediation Commit (2nd commit of PR tip)The tip commit
VerdictAPPROVED The PR lands margin M3a (borrow-availability entry gate + HTB tiered rates) as a default-off mechanism fully routable through experiment surfaces. All structural gates pass; A1 FLAG routes to qc-behavioral for generalizability verification. Flag (A1) note for qc-behavioral: Margin changes landed in core |
|
Reviewed SHA: 5724244 Behavioral QC — feat/margin-m3a-borrow-htb (PR #2016)Contract-pinning review by code reading (no local build; CI green at tip, qc-structural ran the targeted suites — same precedent as the #2015 review). qc-structural's A1 FLAG on Contract Pinning Checklist
Behavioral Checklist (margin/infra — domain rows NA per authority file "pure infra" note; borrow-gate faithfulness judged under A1/W)
Observations (non-blocking, no rework)
Quality Score5 — Exemplary contract pinning: every VerdictAPPROVED qc-structural APPROVED + qc-behavioral APPROVED at 5724244; CI green (build-and-test + perf-tier1-smoke). All three merge gates satisfied. |
) ## margin M3b — buy-in stress mode (deterministic stress-path, default-off) Closes the remaining half of M3 (short-side squeeze robustness) from `dev/plans/levered-longshort-margin-realism-2026-07-14.md`. Builds on M3a (#2016). ### What it does — the mechanism Models the worst-case squeeze cost for a levered long-short config: when a short is hard-to-borrow (HTB), the lender can recall the borrowed shares, forcing a buy-in. When armed, **every** held short that is HTB at its current mark is force-covered at the next weekly (Friday) close — a deterministic **upper bound** on buy-in cost for the M4 promotion-grid stress cells. New pure module `Short_buyin` (`trading/trading/simulation/lib/short_buyin.{ml,mli}`), the short-side mirror of the M2 `Long_maintenance` runner: - `select_buyins` — pure HTB filter over marked short holdings. - `buyin_stress_transitions` — Friday-gated `TriggerExit` builder; each cover is tagged `StrategySignal { label = "buyin_stress" }` at the mark, so forensics separate buy-ins from strategy exits, short maintenance covers (`margin_call`) and long reduces (`maintenance_reduce`). ### Deterministic by design Chose the deterministic **stress-path** branch over the probabilistic forced-cover: the M4 grid wants the single worst-case path (every HTB borrow recalled), not sampled paths, and every analysis function must stay pure/reproducible (CLAUDE.md). No RNG anywhere — same portfolio + config always yield the same covers. ### HTB definition — dedicated threshold A short marked strictly below a positive `short_buyin_htb_price_below` is HTB (`Margin_config.is_buyin_htb ~price`). Chose a dedicated threshold over reusing an M3a tier band because buy-in (share recall) is a distinct broker event from a maintenance breach — a lender can recall a name comfortably above its maintenance requirement — so the stress cell's "which shorts get bought in" is decoupled from the leverage/maintenance dials and the M4 grid can vary them orthogonally. That low-priced names are the HTB ones mirrors the M3a price-tier philosophy. ### R1 (default-off) `Margin_config` gains `short_buyin_stress_mode : bool [@sexp.default false]` + `short_buyin_htb_price_below : float [@sexp.default 0.0]`. Placing them on `Margin_config` (already threaded to `Margin_runner.tick`) needs **zero new simulator/panel_runner threading**. At the defaults nothing is ever HTB (mode off, and no positive mark is below 0.0), so `tick` stays bit-equal — goldens unchanged, no re-pin. ### R2 (searchable) Both fields resolve through `Overlay_validator` via the nested `margin_config.*` overlay keys; axis-expansion tests `test_short_buyin_stress_mode_axis_expands` and `test_short_buyin_htb_price_below_axis_expands` in `test_variant_matrix.ml`. ### Wiring + collision handling `Margin_runner.tick` appends buy-in covers alongside the short force-cover and M2 long-reduce (same Friday cadence + `dedup_strategy_exits_for_margin` seam). A short flagged by both the maintenance check and the buy-in mode is covered once — `_drop_buyins_colliding_with_covers` keeps the `margin_call` (richer detail), drops the duplicate `buyin_stress`. ### Bar-cadence caveat Documented in `short_buyin.mli` + the `Margin_config` field docs: daily-close marks cannot see an intraweek gap-through-recall; a Monday-to-Thursday squeeze is only covered at Friday's close — M4 stress-path gap scenarios extend this. ### M3a QC drive-bys closed (both non-blocking observations from #2016) 1. Added the symmetric `test_short_borrow_rate_tiers_axis_expands` (the M3a axis was proven only via its structurally-identical `short_maintenance_tiers` sibling). 2. Corrected the borrow-fee disarmed-parity wording from "bit-for-bit" to "numerically identical (1e-12)" in `portfolio_margin.mli` + the renamed `test_borrow_fee_empty_tiers_numerically_identical_flat`. The M3a per-position reassociation `(Σnᵢ)·r → Σ(nᵢ·r)` is not IEEE-bit-identical; goldens hold. ### Test plan - `test_short_buyin.ml` (8) — R1 default/disarmed no-ops, armed selects only HTB, zero-threshold no-op, Friday-armed tagged cover at mark, weekly-cadence Monday no-op, unmarked-short skip, no-positions no-op. - `test_margin_accounting.ml` (+1 `is_buyin_htb` gate; round-trip extended to the buy-in fields). - `test_variant_matrix.ml` (+3 axis-expansion tests). - Verified green locally: `dune build` (exit 0), `dune build @fmt` (clean, no diff), `dune runtest trading/simulation/test trading/portfolio/test trading/backtest/walk_forward/test` (exit 0 — the three suites touching this change: portfolio +39, simulation incl `test_short_buyin` +8, walk_forward axis tests), and `linter_file_length` (exit 0). Magic-number scan of the new source is clean (all named constants / semantic zeros; `short_buyin.ml` has no numeric literals). Goldens unchanged (default-off, bit-equal at defaults). - The full `dune runtest` + full `devtools/checks` were CPU-starved by a live local sweep chain and not completed locally — **CI is authoritative for the full surface**. ### Not in scope M4 (validation protocol — parity gates, squeeze stress cells, leverage surface via experiment-gap-closing + confirmation grid). No default flips and no levered number quoted here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: difan <di.fan.cpa@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Automated daily orchestrator run (run-1 of 2026-07-20, GHA run 29719142253). Zero-dispatch full pass. Main GREEN on `b4ef7900` (clean-tree `dune build` + `dune runtest` both exit 0; fmt_check/magic_numbers OK; integrity + index_size exit 0). One open PR #2009 (maintainer LOCAL, deferred per collision-avoidance); every other track deep-warehouse-data-gated / human-gated / active-LOCAL. Reconciled resistance-v2 (#2013 merged + lever f #2015) and margin-realism (M3a/M3b #2016/#2017) rows. See `dev/daily/2026-07-20.md` for the full summary. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: claude-orchestrator <noreply@github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: GitHub Actions Bot <bot@github-actions>
Margin realism M3a — short-side squeeze robustness (deterministic half)
The deterministic half of M3 (plan:
dev/plans/levered-longshort-margin-realism-2026-07-14.md§M3): three default-off short-side mechanisms, each an R1 no-op at its default and an R2-searchable axis. Bit-identical to pre-M3a at every default (parity pinned by unit tests — no golden re-pin needed, no levered number quoted; that is M4). Buy-in stress mode is M3b (deferred, documented seam); validation protocol is M4 (deferred).What each mechanism does
Tier-table primitive — new pure module
Short_margin_tiers(trading/trading/portfolio/lib/): a price-banded, order-independent, piecewise-constant lookup.tier_value ~tiers ~flat_fallback ~pricepicks the tightest band strictly coveringprice, else the flat fallback. Empty table → flat fallback → bit-identical no-op. Thresholds live in tests/example configs, never baked in code.HTB tiered borrow rate —
Margin_configgainsshort_borrow_rate_tiers : Short_margin_tiers.tier list [@sexp.default []]+borrow_fee_annual_for_price/daily_borrow_rate_for_price.Portfolio_margin.accrue_daily_borrow_feenow accrues per short position at its marked price using the tiered daily rate. Empty table → every price resolves to the flat 50bps → per-position sum equals the legacysum_short_notional * flat_daily_ratebit-for-bit (distributivity).Maintenance tier table —
Margin_configgainsshort_maintenance_tiers : … [@sexp.default []]+maintenance_pct_for_price.Portfolio_margin.check_maintenance_marginuses the price-tiered threshold (sub-$5 → 100%, ~$5-17 → ≈83%, ≥ ~$17 → 30% base perdev/notes/long-short-margin-mechanics-2026-06-12.md) so low-priced HTB shorts flag for force-cover sooner. Empty table → flat 25% → bit-identical.Borrow-availability entry gate — new module
Short_borrow_gate(trading/trading/weinstein/strategy/lib/, purefilter+ bar-readerapply), re-exported onWeinstein_strategy. Drops SHORT candidates whose trailing dollar-ADV (no-lookahead) is below the borrow-supply floor; longs untouched; a missing reading never drops. Wired as the last gate inEntry_assembly.assemble. Configshort_borrow_min_dollar_adv : float [@sexp.default 0.0]onweinstein_strategy_config(+ re-declaredweinstein_strategy.mlirecord). Dollar-ADV is the borrow-supply proxy (no locate feed).R1/R2 compliance per knob (experiment-flag-discipline)
[@sexp.default <no-op>]: the two tier lists default[],short_borrow_min_dollar_advdefaults0.0. Each default is the exact pre-M3a behaviour. Back-compat parse pinned:test_pre_m3a_margin_config_sexp_parses_with_empty_tiers,test_pre_m3a_sexp_parses_short_borrow_default.short_borrow_min_dollar_advand nestedmargin_config.short_{borrow_rate,maintenance}_tiersall resolve throughOverlay_validator. Axis-expansion tests:test_short_borrow_min_dollar_adv_axis_expands,test_short_maintenance_tiers_axis_expands(a sexp-valued tier-list axis, mirroring the nestedw_overhead_supplyprecedent).Bit-identical-at-defaults
Parity pinned by
test_borrow_fee_empty_tiers_bit_equal_flat,test_maintenance_flat_does_not_flag_cheap_short, and the config default-no-op tests. No golden re-pin was required because the disarmed lookups are bit-identical by construction.Purity / layering
Tier lookup + availability predicate are pure functions in their own modules; the portfolio layer (
Short_margin_tiers,Margin_config,Portfolio_margin) does not depend on the strategy layer. Bar-cadence caveat (intraweek borrow recall / gap squeeze invisible; stress paths = M3b/M4) documented in the new.mlis.Test plan / how it was run
Run against an isolated worktree;
dune build(full project) green (exit 0),dune build @fmtclean. Targeted suites all pass:trading/portfolio/test— incl. newtest_short_margin_tiers(6) +test_margin_accounting(+6: tiered borrow fee per-price, empty-tiers flat parity, tiered maintenance flags a cheap short the flat 25% doesn't + flat-parity, config round-trip + pre-M3a back-compat).trading/weinstein/strategy/test— incl. newtest_short_borrow_gate(4) + extendedtest_long_buying_power(config default-no-op / round-trip / pre-M3a parse forshort_borrow_min_dollar_adv).trading/backtest/walk_forward/test— incl. the 2 new R2 axis tests.The full
dune runtest/devtools/checkslinter suite was run against a live multi-hour sweep sharing the container; CI is authoritative for the full lint surface.Deferred (explicitly not in this PR)
🤖 Generated with Claude Code