Skip to content
Skip to content
← all research
STUDY

"CARRY x BASIS sizing overlay — production wire-up"

2026-09-03

·

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 _act catches all exceptions from evaluate() so a broken cache never crashes the bot (depth-filter pattern: best-effort hook).
    • _load_funding(coin) — loads Hyperliquid ETH funding cache via data.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 — calls cb_overlay_evaluate(target, now=now) after the depth-filter call; stashes the multiplier in self.state.set("carry_basis_mult", round(cb_mult, 4)) for the parent's order-sizing math to consume; logs and ledger get the cb_verdict dict. The PAPER_OBSERVE ledger entry now includes the cb_verdict audit fields.
  • production/runner.py:_act — after the vol-targeting step, reads carry_basis_mult from self.state and multiplies effective_fraction by it (only when target == 1 and cb_mult > 0). The order math for SELL is unchanged (sizing doesn't apply).
  • start_bots.py — the gate60 bot's env dict now explicitly sets P_DEPTH_FILTER_GATE60=0 and P_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() returns multiplier=1.0, status="disabled". The runner still calls it, logs the verdict, but no behavior change.
  • P_CARRY_BASIS_OVERLAY=1 and target=0 — multiplier=0.0, status="inactive_flat". The overlay is inert on flat days because the SELL order math doesn't consume effective_fraction.
  • P_CARRY_BASIS_OVERLAY=1 and 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=1 and 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=1 and 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=1 and 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:

  1. 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).
  2. Run the paper-trading observe for 30+ days. Set P_CARRY_BASIS_OVERLAY=1 in the gate60 bot's .env. The bot will log the cb_verdict audit 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).
  3. 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.py with a fresh SPLIT boundary and confirm the verdict is still PASS.
  4. If the live forward-OOS confirms the verdict, leave the overlay on. If not, flip P_CARRY_BASIS_OVERLAY=0 and 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_frames returns empty dict; _load_funding returns 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.py already 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=0 to revert to no-overlay behavior immediately. The depth-filter (P2b) is independent and stays on if it was on.

All trading described is paper (simulated). Not investment advice.

← all research