Date: 2026-09-02
Status: Wire-up complete; gate OFF by default; PASS pre-registered
verdict available in reports/carry_basis_overlay.md.
What was built
A new sizing overlay module that consults the Hyperliquid funding cache between the depth-filter decision and order submission, and multiplies the deploy fraction by {0.5, 1.0, 1.25} based on the joint state of funding (CARRY) and perp-vs-spot premium (BASIS).
Files
New
production/carry_basis_overlay.py(305 lines)SizingOverlayVerdict(frozen dataclass, audit fields for the ledger).evaluate(raw_target, *, now, instrument)— the hook the runner calls. Returns mult=1.0 when disabled, target=0 (inert on flat days), missing/stale/warming data, or the bucket logic for active long days. The runner in_actcatches all exceptions fromevaluate()so a broken cache never crashes the bot (depth-filter pattern: best-effort hook)._load_funding(coin)— loads Hyperliquid ETH funding cache viadata.hyperliquid.load_funding_frames._zscores(df, look_h)— pre-registered trailing z-scores for carry (annualized funding) and basis (premium), with min_periods=look_h/2, identical to the validation study.
Modified
production/live_runner.py:_act— callscb_overlay_evaluate(target, now=now)after the depth-filter call; stashes the multiplier inself.state.set("carry_basis_mult", round(cb_mult, 4))for the parent's order-sizing math to consume; logs and ledger get thecb_verdictdict. The PAPER_OBSERVE ledger entry now includes thecb_verdictaudit fields.production/runner.py:_act— after the vol-targeting step, readscarry_basis_multfromself.stateand multiplieseffective_fractionby it (only whentarget == 1andcb_mult > 0). The order math for SELL is unchanged (sizing doesn't apply).start_bots.py— thegate60bot's env dict now explicitly setsP_DEPTH_FILTER_GATE60=0andP_CARRY_BASIS_OVERLAY=0. Both are the conservative defaults; operators opt in by flipping either to 1.tests/test_carry_basis_overlay.py— extended from 17 to 35 tests: the original 17 cover the validation-study sizing function; 18 new tests cover the production wire-up (pre-registered defaults export, disabled-by-default, inert-on-flat-day, no-data/stale/warming fail-open, both-stretched-LONG 0.5x, both-stretched-SHORT 1.25x, mixed/sub-threshold 1.0x, NaN-z fail-open, exception propagation, verdict serialization, frozen dataclass, zscores sanity).
Behavior contract (mirrors the depth_filter wire-up)
P_CARRY_BASIS_OVERLAY=0(default) — overlay is observe-only;evaluate()returnsmultiplier=1.0, status="disabled". The runner still calls it, logs the verdict, but no behavior change.P_CARRY_BASIS_OVERLAY=1and target=0 —multiplier=0.0, status="inactive_flat". The overlay is inert on flat days because the SELL order math doesn't consumeeffective_fraction.P_CARRY_BASIS_OVERLAY=1and target=1 and no Hyperliquid cache —multiplier=1.0, status="no_data". Fail-open; the cache is the load_funding_frames empty-return.P_CARRY_BASIS_OVERLAY=1and target=1 and stale cache (>48h old) —multiplier=1.0, status="stale". Fail-open; the order math proceeds at full size.P_CARRY_BASIS_OVERLAY=1and target=1 and warming cache (<14d) —multiplier=1.0, status="warming". Fail-open; not enough history for the trailing 168h z-score to be meaningful.P_CARRY_BASIS_OVERLAY=1and target=1 and active —multiplier ∈ {0.5, 1.0, 1.25}based on the joint state.
Promotion path (the operator-side recipe)
The pre-registered OOS verdict is PASS (OOS Sharpe 0.99 → 1.30, p=0.008, maxDD -18.4% → -14.3%). To activate on the live bot:
- Wait for the Hyperliquid ETH funding cache to clear warming naturally. It is currently active (19,704 rows of hourly data from 2024-06-01 to 2026-08-31 — far above the 14-day warmup).
- Run the paper-trading observe for 30+ days. Set
P_CARRY_BASIS_OVERLAY=1in the gate60 bot's.env. The bot will log thecb_verdictaudit fields every cycle; the multiplier will be 0.5x, 1.0x, or 1.25x on the days the z-scores fire. Compare the live skip/boost pattern to the backtest's: 1.8% of long days at 0.5x, 4.2% at 1.25x, 81.9% at 1.0x (no signal), 12.0% at 1.0x (mixed). - Reconfirm the OOS verdict in a forward window. The original
study's OOS is 2025-01..2026-07. Any new live days beyond 2026-07
are honest forward OOS. After 30-60 days of live data, re-run
validation/carry_basis_overlay.pywith a freshSPLITboundary and confirm the verdict is still PASS. - If the live forward-OOS confirms the verdict, leave the overlay
on. If not, flip
P_CARRY_BASIS_OVERLAY=0and the runner reverts to the no-overlay behavior. The depth-filter and gate60 logic are unchanged.
Failure modes (what to do if)
- Hyperliquid API down:
load_funding_framesreturns empty dict;_load_fundingreturns None;evaluate()returns no_data / mult=1.0. No behavior change. - Hyperliquid cache deleted/missing: same as above.
- Both signals stretch LONG for an extended period: the 0.5x bucket is the most frequent, halving the deploy fraction. The gate60 signal is unchanged (still LONG). The order math sizes at half the vol-target-adjusted fraction.
- Run a kill-switch on size: the production
kill.pyalready gates order submission; the overlay sits between the depth-filter and the kill gate, so a kill state still blocks all orders. - Mis-specification discovered live: flip
P_CARRY_BASIS_OVERLAY=0to revert to no-overlay behavior immediately. The depth-filter (P2b) is independent and stays on if it was on.