Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions blueprints/us-equities/adaptive-paper/README-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,9 @@ schedule/ladder contract and
for the design record. Leverage above 1x is paper-only, requires a
preflight-proven account multiplier at least equal to the requested
leverage (`runner._check_margin_entitlement`), and remains unqualified
until each rung's `leverage-ladder-1x/2x/4x` gate row shows
`needs_attention == 0`.
until each rung's `leverage-ladder-1x/2x/4x` gate row's flip condition holds:
`needs_attention == 0` and achieved exposure above the rung's threshold (see
the gate-condition paragraph below).

F2 (2026-09-22 residual review, reachability): a rung's `max_leverage`,
schedule cells and drawdown ladder establish the safety **envelope** in
Expand Down Expand Up @@ -214,11 +215,52 @@ sizing math, which stay a ceiling, not a target, per
`agent-lab/docs/decisions/2026-09-22-leverage-schedule-and-entitlement.md`.
A rung's gate row is not, by itself, evidence that the rung's exposure was
ever achieved -- that evidence is this recorded achieved-leverage receipt.
No gate row in `catalogs/us-equities/gates-20260922.json` currently reads
`peak_achieved_leverage`/`seconds_above_next_lower_rung_ceiling`, so a gate
row can still flip to established on a receipt whose achieved exposure never
exceeded a lower rung's own ceiling; the rung configs' `notes` have been
corrected to say so instead of claiming the opposite.

Gate condition (2026-09-24, audit gap #5): until this date no gate row read
`peak_achieved_leverage`/`seconds_above_next_lower_rung_ceiling`, so a rung
could flip on `needs_attention == 0` alone from a trial whose achieved
exposure never exceeded the next-lower rung's cap. Each
`leverage-ladder-1x/2x/4x` row's `flip_condition` is now an `all_of`
(`scripts/trading_gates.py` gained `all_of` and a `greater_than` comparison
that reads the runner's decimal strings and JSON numbers exactly) over the
rung receipt preregistered in the gate note -- `{"schema_version": 1,
"kind": "leverage_ladder_rung_receipt", "rung", "needs_attention",
"source": {"paper_output_path", "paper_output_sha256",
"certified_run_status"}, "leverage"}`, where `leverage` is the certified
run's `paper-output.json` `leverage` block copied verbatim: the receipt's
`schema_version`, `kind` and `rung`, `/needs_attention == 0`,
`/source/certified_run_status == "passed"`, a `source_matches` binding (the
named in-tree `paper-output.json` must hash to the recorded sha256 and its
`/leverage` and `/status` must equal the receipt's `/leverage` and
`/source/certified_run_status` exactly, so the leverage block and the passed
status come from one hashed run; fix round 2026-09-24),
`/leverage/config_max_leverage` equal to the rung, `/leverage/
next_lower_rung_ceiling` equal to the rung's threshold,
`/leverage/peak_achieved_leverage` greater than it and
`/leverage/seconds_above_next_lower_rung_ceiling` greater than 0. The
thresholds are 1x for the 2x rung and 2x for the 4x rung; the 1x rung has no
lower rung, so `leverage.next_lower_rung_ceiling(1)` now returns the
documented minimum exposure `leverage.ONE_X_MINIMUM_EXPOSURE` (0.5x; the 1x
config's entry budget permits up to 0.9x) instead of `None`, and the 1x
receipt's `seconds_above_next_lower_rung_ceiling` accumulates above 0.5x. It
is a receipt threshold, not a sizing target. `tests/test_trading_gates.py`
(`LeverageLadderFlipConditionTests`) and `tests/test_adaptive_paper_runner.py`
(`AchievedLeverageGateTraceTests`, which folds per-tick traces through
`runner._leverage_achievement_step`) show that a clean trial that never
exceeds the lower cap does not satisfy its rung. These are offline unit tests
(synthetic receipts and traces); no paper trial was run for this change and no
rung receipt exists yet. Limits: `seconds_above_next_lower_rung_ceiling` is a
per-tick approximation, and no minimum duration above the threshold is set --
any positive time satisfies the checker, so how long the rung was used is
judged at the manual qualification before a dated flip commit.
`needs_attention` counts every run at the rung and is not bound to the
hashed file, so it too is checked against the rung's trial directories at
that qualification. The binding needs the certified `paper-output.json`
committed in the tree; it proves the receipt matches those bytes, not that
the bytes came from a real broker session. `runner.py`
was left byte-identical so the native-fault receipt's engine-source binding
still holds; its inline comments that say the 1x threshold is `None` and that
no gate row reads these fields predate this change and are superseded here.

2026-09-22 leverage fix round 1 (LEV-RI-A/CX-P1/EH-1/CX-P2, all `major`/
`blocker` findings against G-e/F2): `strategies_v1._decide_core`'s
Expand Down
3 changes: 2 additions & 1 deletion blueprints/us-equities/adaptive-paper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ validated `leverage-schedule-v1-20260922` policy (see README-safety.md) is
present in config, a preflight-proven account multiplier at least equal to the
requested leverage has been confirmed, and this ladder's gate rows
(`leverage-ladder-1x/2x/4x` in `catalogs/us-equities/gates-20260922.json`) each
show `needs_attention == 0`.
show `needs_attention == 0` together with a recorded peak achieved leverage and
time above the next-lower rung's cap (0.5x for the 1x rung).

The allocator uses one portfolio owner across all families, explicit cost
hurdles, an exposure reserve below the ledger cap, a cooldown and minimum hold.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"rotation": {
"enabled": false
},
"notes": "G-e leverage-schedule-v1-20260922, rung 1x. Gate row 'leverage-ladder-1x' in catalogs/us-equities/gates-20260922.json flips on needs_attention == 0 at blueprints/us-equities/adaptive-paper/ladder/1x/receipt.json; see agent-lab/docs/decisions/2026-09-22-leverage-schedule-and-entitlement.md for the design record. leverage_policy is the canonical leverage-schedule-v1-20260922 block (identical across every rung; see leverage.CANONICAL_V1_BLOCK), never widened per rung -- only the top-level max_leverage dial and the per-order sizing below differ from config-sip.json. max_order_quantity and max_order_qty_mode='notional' are widened together with max_gross_exposure_usd/max_order_notional_usd (=capital_usd*1 / gross/max_held_symbols) so the schedule's ceiling is more reachable than with the config-sip.json shipped per-order sizing (gross there is bounded at about 10 x min(price,$1000), well under even the 1x rung). F2 (2026-09-22 leverage fix round 1): max_order_quantity is 100, not 10 -- PolicyConfig.max_shares (strategies_v1.py) and RiskLimits.max_order_qty (safety.py) share the same 100-share hard ceiling, and a max_order_quantity of 10 silently undercut max_order_qty_mode='notional' for most of the symbol universe (floor(max_order_notional_usd/price) exceeds 10 shares for anything cheaper than about $100-400/share at this rung's notional), so entries never reached the sizing this config's max_order_notional_usd/max_gross_exposure_usd imply. F2 (2026-09-22 leverage fix round 2): max_gross_loss_usd/max_drawdown_usd now scale with this rung's leverage multiple, config-sip.json's base $25 x 1 = $25 at 1x (unchanged; see config-leverage-2x.json/config-leverage-4x.json's notes for the $50/$100 scaled values at those rungs) so that the fraction of the cap a given entry-spread mark-to-market loss consumes is identical across every rung -- ordinary entry spread cost alone (max_spread_bps=15 marking to the bid) no longer trips the hard halt at a smaller fraction of the ramp for a higher-leverage rung than it does here, and the drawdown ladder's own step-down (4x->2x at 25% drawdown, ->1x at 50%, ->0 at 75%, all fractions of max_drawdown_usd) gets an equal dollar-fraction of headroom before drawdown_cap_reached/gross_loss trips at every rung. F2 (2026-09-22 leverage fix round 3, residual review finding F2 -- reachability): this scales the drawdown/loss caps to the rung's configured ceiling, not to leverage actually achieved -- a rung establishes the safety envelope at that cap (the max_leverage ceiling and the drawdown ladder's own step-downs bound what a run is PERMITTED to reach); it does not by itself guarantee a run's actual gross-to-equity exposure reaches that cap, and achieved exposure may stay below it. Every leveraged run's outcome/receipt now records the achieved peak gross-to-equity leverage, the ceiling in force at that moment, and (at rungs above 1x) the time spent above the next-lower rung's own ceiling (outcome['leverage'].peak_achieved_leverage/ceiling_at_peak_achieved_leverage/seconds_above_next_lower_rung_ceiling; see leverage.next_lower_rung_ceiling and runner._leverage_achievement_step), so this rung's gate row ('leverage-ladder-1x' in catalogs/us-equities/gates-20260922.json) is not, by itself, evidence the rung's exposure was ever achieved -- that evidence is this recorded receipt (README-safety.md's Leverage schedule section); the 1x rung has no lower rung to compare against (leverage.next_lower_rung_ceiling(D(\"1\")) is None, so seconds_above_next_lower_rung_ceiling never accumulates here). tests/test_adaptive_paper_runner.py's test_rung_numeric_relationships now asserts max_gross_loss_usd == max_drawdown_usd == config-sip.json's $25 x this rung's multiple instead of locking every rung to the unscaled $25 flat value. Leverage above 1x is unqualified until this gate row's needs_attention count is 0 and a preflight-proven account multiplier >= 1 is confirmed (runner.py's validate_preflight, under the policy). Request ceilings are not fill targets. Predictive catalysts, Smart Router algorithms and live trading are unqualified. rotation.enabled stays false: registry.json currently ships only evidence_class SYN (synthetic, not accepted; see blueprints/us-equities/acceptance-wave/research-protocol.json's regime_selector field), so flipping this on today would run the selector framework against the same single inline policy with no real alternative to rotate to.",
"notes": "G-e leverage-schedule-v1-20260922, rung 1x. Gate row 'leverage-ladder-1x' in catalogs/us-equities/gates-20260922.json flips only when blueprints/us-equities/adaptive-paper/ladder/1x/receipt.json shows needs_attention == 0 and its leverage block records next_lower_rung_ceiling == 0.5 (the documented 0.5x minimum exposure, leverage.ONE_X_MINIMUM_EXPOSURE, since 1x has no lower rung), peak_achieved_leverage > 0.5 and seconds_above_next_lower_rung_ceiling > 0 (audit gap #5, 2026-09-24); see agent-lab/docs/decisions/2026-09-22-leverage-schedule-and-entitlement.md for the design record. leverage_policy is the canonical leverage-schedule-v1-20260922 block (identical across every rung; see leverage.CANONICAL_V1_BLOCK), never widened per rung -- only the top-level max_leverage dial and the per-order sizing below differ from config-sip.json. max_order_quantity and max_order_qty_mode='notional' are widened together with max_gross_exposure_usd/max_order_notional_usd (=capital_usd*1 / gross/max_held_symbols) so the schedule's ceiling is more reachable than with the config-sip.json shipped per-order sizing (gross there is bounded at about 10 x min(price,$1000), well under even the 1x rung). F2 (2026-09-22 leverage fix round 1): max_order_quantity is 100, not 10 -- PolicyConfig.max_shares (strategies_v1.py) and RiskLimits.max_order_qty (safety.py) share the same 100-share hard ceiling, and a max_order_quantity of 10 silently undercut max_order_qty_mode='notional' for most of the symbol universe (floor(max_order_notional_usd/price) exceeds 10 shares for anything cheaper than about $100-400/share at this rung's notional), so entries never reached the sizing this config's max_order_notional_usd/max_gross_exposure_usd imply. F2 (2026-09-22 leverage fix round 2): max_gross_loss_usd/max_drawdown_usd now scale with this rung's leverage multiple, config-sip.json's base $25 x 1 = $25 at 1x (unchanged; see config-leverage-2x.json/config-leverage-4x.json's notes for the $50/$100 scaled values at those rungs) so that the fraction of the cap a given entry-spread mark-to-market loss consumes is identical across every rung -- ordinary entry spread cost alone (max_spread_bps=15 marking to the bid) no longer trips the hard halt at a smaller fraction of the ramp for a higher-leverage rung than it does here, and the drawdown ladder's own step-down (4x->2x at 25% drawdown, ->1x at 50%, ->0 at 75%, all fractions of max_drawdown_usd) gets an equal dollar-fraction of headroom before drawdown_cap_reached/gross_loss trips at every rung. F2 (2026-09-22 leverage fix round 3, residual review finding F2 -- reachability): this scales the drawdown/loss caps to the rung's configured ceiling, not to leverage actually achieved -- a rung establishes the safety envelope at that cap (the max_leverage ceiling and the drawdown ladder's own step-downs bound what a run is PERMITTED to reach); it does not by itself guarantee a run's actual gross-to-equity exposure reaches that cap, and achieved exposure may stay below it. Every leveraged run's outcome/receipt now records the achieved peak gross-to-equity leverage, the ceiling in force at that moment, and (at rungs above 1x) the time spent above the next-lower rung's own ceiling (outcome['leverage'].peak_achieved_leverage/ceiling_at_peak_achieved_leverage/seconds_above_next_lower_rung_ceiling; see leverage.next_lower_rung_ceiling and runner._leverage_achievement_step), so this rung's gate row ('leverage-ladder-1x' in catalogs/us-equities/gates-20260922.json) is not, by itself, evidence the rung's exposure was ever achieved -- that evidence is this recorded receipt (README-safety.md's Leverage schedule section); the 1x rung has no lower rung, so since 2026-09-24 (audit gap #5) leverage.next_lower_rung_ceiling(D(\"1\")) returns the documented 0.5x minimum exposure and seconds_above_next_lower_rung_ceiling accumulates while achieved leverage is above 0.5x (this rung's entry budget, 10000 - 1000 USD, permits up to 0.9x). tests/test_adaptive_paper_runner.py's test_rung_numeric_relationships now asserts max_gross_loss_usd == max_drawdown_usd == config-sip.json's $25 x this rung's multiple instead of locking every rung to the unscaled $25 flat value. Leverage above 1x is unqualified until this gate row's flip condition holds (needs_attention count 0 with achieved exposure above the rung threshold) and a preflight-proven account multiplier >= 1 is confirmed (runner.py's validate_preflight, under the policy). Request ceilings are not fill targets. Predictive catalysts, Smart Router algorithms and live trading are unqualified. rotation.enabled stays false: registry.json currently ships only evidence_class SYN (synthetic, not accepted; see blueprints/us-equities/acceptance-wave/research-protocol.json's regime_selector field), so flipping this on today would run the selector framework against the same single inline policy with no real alternative to rotate to.",
"max_order_qty_mode": "notional",
"leverage_policy": {
"version": "leverage-schedule-v1-20260922",
Expand Down
Loading
Loading