Date: 2026-09-02
Status: Research proposal (not yet implemented)
Source of "this might work": the 13 crypto day-trading edges tested in
reports/crypto_day_edge_test.md were ALL REJECTED as standalone signals,
but every one of them carries real information. Standalone PFs were 0.10 to
1.21 with negative ret in the vast majority. None had a single tradable
edge in isolation. What's never been tested: combining them as
informationally-independent signals modulating a validated base signal.
The base signal here is gate60 (validated OOS Sharpe 2.48 with the depth-filter on, otherwise ~1.0). The overlay is CARRY × BASIS: a position-sizing multiplier that shrinks the gate60 position when the perp market is crowded/extended and grows it when funding/basis suggest the opposite side is paying you to be there.
Why CARRY × BASIS
- CARRY (
carry_scores, Hyperliquid hourly funding) — extreme positive funding means longs pay shorts (crowded-long); extreme negative means the reverse. The signal is-z(annualized_funding)gated to |z| ≥ 1.5. - BASIS (
basis_scores, Hyperliquid premium) — perp-vs-spot basis reversion. Extreme premium is a stretched basis; fade it.-z(premium)gated to |z| ≥ 2.0.
Both are informationally independent in construction (funding rate ≠ premium index, despite being related), and both have known positive carry properties at the strategy level: being SHORT crowded/extended perps has historically been profitable regardless of direction (you earn the funding/basis even if you're right or wrong about the move). The problem in the day-trading test was they were standalone — they tried to predict direction. As a sizing overlay on a slow trend (gate60), they say "the trend is right, but the entry is overextended; size down."
Hypothesis (pre-registered)
When gate60 is long and CARRY and BASIS are both signalling the same side (e.g. both stretched to the long side), the gate60 long is entering a position that will pay funding AND pay to close the stretched basis. The expected short-term move is still positive (trend wins), but the immediate expected return is reduced. A position-sizing multiplier of 0.5x on those days improves risk-adjusted return without changing the directional thesis.
Conversely, when gate60 is long and CARRY+BASIS are stretched to the short side, gate60 is the funding-recipient (shorts pay longs). The immediate expected return is enhanced. A 1.25-1.5x multiplier on those days is appropriate.
Construction (causal, no look-ahead)
Inputs:
gate60_target[t]∈ {0, 1} fromstrategy.gate60.decide(already validated; lag-1 by construction).carry_z[t]=-z(annualized_funding, look=168h, z_enter=1.5)fromsignals.crypto_edges.carry_scores(Hyperliquid cache).basis_z[t]=-z(premium_index, look=168h, z_enter=2.0)fromsignals.crypto_edges.basis_scores(Hyperliquid cache).
Sizing multiplier s[t] ∈ {0.5, 1.0, 1.25}:
- if
gate60_target[t] == 0:s[t] = 0(no position; overlay is inert) - elif
carry_z[t]andbasis_z[t]are both > 0 (both say "fade the long side", meaning longs are crowded):s[t] = 0.5(half-size; you're paying funding into a stretched basis) - elif
carry_z[t]andbasis_z[t]are both < 0 (both say "fade the short side", shorts are crowded, longs collect funding):s[t] = 1.25(boosted; you're collecting funding) - else (mixed signals or below z_enter):
s[t] = 1.0(no change)
The s[t] is a continuous scaling on the existing gate60 deploy fraction
inside production/signal.py's effective_target and the runner's order
sizing — no new strategy, no new entry/exit mechanics, just a multiplier.
Validation protocol
The same protocol as the depth-filter study (validation/depth_filter_gate60.py):
- Window: 2023-01-15 to 2026-07-31 (the depth-filter study's window).
- ETH daily bars from
validation.common.load_daily_bars("ETH"); BTC from same. - Hyperliquid funding cache for
carry_z,basis_z(both hourly). - gate60 target via
strategy.gate60.decide(frozen, unchanged). - Returns: hold during day D → close_D/open_D - 1, matches the 00:15 UTC live cycle.
- Pre-registered primary metric: Sharpe ratio of (gate60 raw position × s[t]) vs (gate60 raw position, no overlay). Bootstrap p-value for "overlay beats baseline" with n=5000.
- Secondary metrics: total ret, maxDD, profit factor, fraction of days at each sizing bucket.
- Pre-registered gate: pass if OOS Sharpe improves by ≥ 0.10 AND bootstrap p < 0.05 AND maxDD does not increase by > 5pp absolute.
- Robustness: vary
look∈ {84h, 168h, 336h} andz_enter∈ {1.0, 1.5, 2.0}; report full grid; do NOT pick the IS winner and then test it (that's exactly the look-ahead bug class from R11).
Why this might fail (and what to do)
- Both signals are mostly below z_enter on a slow-trend window. Funding is mean-reverting; on a slow trend the funding doesn't get extreme enough. If < 30% of gate60-long days have CARRY or BASIS above their z_enter, the overlay will be inert and the experiment is a wash. → check this in the first 30 minutes of the validation run.
- CARRY and BASIS are too correlated in the post-2022 data. If
corr(carry_z, basis_z) > 0.5, the composite is essentially one-dimensional and the threshold tuning becomes the look-ahead bug. → report the correlation in the study's first table. - Sizing at 0.5x is the wrong magnitude. If the optimal is 0.0 (i.e. fully skip those days), that's a different (better?) study and should be a follow-up. If the optimal is 0.75, then the 0.5x is too conservative and vice versa.
- Multipliers don't compose with the depth-filter. The depth-filter is a separate skip-or-pass decision; the sizing overlay is what to do when the depth-filter passes. They are independent. Document the order of operations explicitly: depth-filter decides pass/skip; CARRY×BASIS decides size; gate60 decides direction.
Code locations to touch (when implementing)
validation/carry_basis_overlay.py(new) — the validation study.production/signal.py— no change; the overlay is upstream.production/depth_filter.py— no change; orthogonal decision.production/equity_fraction— a new field in the runner that multiplies the deploy fraction. The cleanest place isproduction/live_runner.py:_act, which already callsdepth_filter_evaluateand useseffective_target. Add asizing_overlay(raw_target, now, instrument)call right after, which returns amult ∈ {0.5, 1.0, 1.25}. The deploy fraction is multiplied bymultbefore the order sizing math.start_bots.py— register the env flagP_CARRY_BASIS_OVERLAY=0(default off).tests/test_carry_basis_overlay.py(new) — unit tests on the multiplier function (no-lookahead, fail-open on missing aux, threshold-bucket correctness, two-side symmetry).
Why this is a good idea and not just another threshold grid
- Every threshold grid in edge-hunt rounds 10-13 (acceleration, VRP, MAX,
Halloween) was rejected because the "passes IS" was an artifact of
threshold optimization on the same data. The protocol above
pre-registers the thresholds (
z_enter=1.5and2.0from the day edge test's winners, and a robustness grid that is reported, not selected). - The composite combines two signals that are economically orthogonal (funding is a cost; basis is a price differential; they are related but not the same — the test will report the cross-correlation).
- It is implemented as a SIZING overlay on a validated strategy, not as a standalone edge. This means even if the overlay is mildly positive, it lives inside the existing risk envelope and inherits the gate60's kill-switches and the depth-filter's skip logic.
- The test can be run on the same 1292-day window the depth-filter study used, in < 5 minutes of compute. Cost is the binding constraint for the other "good ideas" (paid news history, options IV surface); this one is essentially free.