Skip to content

View original

SpotPerpReturnLeadMomentumLS

Hypotheses

Spot-Perp Return-Lead Directional Momentum, Long-Short (Cross-Venue Single-Asset: ETHUSDT.BINANCE Perp EXECUTED, ETHUSDT.BINANCE_SPOT as LEAD SIGNAL, 4h Bars, Go With the Perp When ORGANIC SPOT Flow Is Leading the Move / Fade When LEVERAGE-Driven Perp Flow Runs Ahead of Spot, Single-Leg Execution to Avoid Two-Leg Fee Drag, 3-Parameter)

Hypotheses

A LONG-SHORT, cross-venue DIRECTIONAL strategy on ETH that uses the RELATIVE return of SPOT versus PERP as an information signal but trades only ONE leg (the perp), so it pays a single round-trip fee — deliberately NOT a delta-neutral spot-perp basis/carry trade (that class dies fee_edge because the two-leg cost exceeds the tiny basis, per L33/L1). The premise is a microstructure lead-lag: moves that are LED BY SPOT reflect organic cash demand/supply and tend to CONTINUE, whereas moves where the PERP runs ahead of spot reflect crowded leverage/speculation and tend to REVERSE. Concretely it measures the rolling spot-return-minus-perp-return spread and uses its sign to decide whether to ride or fade ETH's (large) directional price move on the perp. The spot leg is a SIGNAL SOURCE ONLY — never held — so the edge does not need to overcome the tiny spot-perp basis; it needs only to predict the direction of the perp's next multi-bar swing, which is large relative to fees. Both legs are Binance OHLCV bars with full multi-year history (no options/quarterly/liquidation data-availability wall). Deliberately 3-parameter (lead_lookback, spread_entry_bps, atr_stop_mult) to resist overfitting.

Hypotheses

Iteration 3 fixes both Layer-4 QA findings with a minimal, targeted change, keeping every earlier-passing layer green. (1) POLARITY: the QA is correct — the hypothesis reduces to direction = sign(spot_ret - perp_ret) = sign(spread); the return spread already encodes both the lead/lag and the side, so the previous signal = tanh(perp_ret/mom_scale) * spread_bps double-counted perp direction and inverted on every down-move. Fixed by making signal = spread_bps directly (removed dir_factor and the now-unused mom_scale). I re-derived all four cases: spot-led up spread>0→LONG, spot-led down spread<0→SHORT, perp-led up spread<0→SHORT, perp-led down spread>0→LONG — all correct with sign(spread). (2) SCALE/THRESHOLD: I recalibrated |spread_bps| on clean real ETH data — median ~9 bps at lb=20 (QA's 'single-digit bps' was right; the old 25-bps gate was ~2x too high and further shrunk by |dir_factor|<=1, hence 1 trade/363d). Removing dir_factor eliminates the shrink and lowering the default to spread_entry_bps=12 (~p60) restores tradability: a full enter-when-flat/exit-on-flip-or-stop replay now yields 198 trades over 363 days (win 0.63) and, critically, POSITIVE gross return in all three tested windows (363d, 1yr, 3yr) — the same windows where the inverted signal lost, confirming the polarity fix flipped a loser into a consistent winner. No other logic changed: imports stay clean (Layer 1), _bts remains an instance method (Layer 2 fix retained), signal is still continuous and varies every bar, and entries fire hundreds of times (Layer 3 >=1-trade). Still faithful to the hypothesis — single-leg perp execution, spot as timestamp-matched lead-lag signal, 3 tuned params, ATR-risk sizing decoupled from signal magnitude and notional-capped (L15).

Hypotheses

Negative-expectancy cross-venue spot-perp return-lead directional strategy — not worth 2 hours of optimization. Over 249 trades: total_return -9.54%, expectancy -$53.99/trade, Sharpe -0.279 (CI [-1.04, 0.49]), profit_factor 0.864 (<1.0, avg_loss $796 > avg_win $682), and avg_trade_return_pct 0.043% — below the 0.15% Binance USD-M fee floor (L22). It loses in 5 of 7 years (2020 -1.1%, 2021 -3.3%, 2024 -2.3%, 2025 -1.0%, 2026 -0.46%) and in the calm/normal vol terciles, so the 'spot-led moves continue / perp-led moves reverse' premise has no consistent sign on ETH — the return spread predicts direction no better than a coin flip net of costs. This is the L73 single-asset supplementary-gated directional family; the spot-return lead signal adds no recoverable edge. The design avoided the delta-neutral two-leg fee trap and sizing/drawdown are fine (11.8% DD, no liquidation), but no tuning of lead_lookback/spread_entry_bps/atr_stop_mult can flip a negative per-trade edge positive. Failure pattern: fee_edge/no_edge single-perp cross-venue directional (L73/L22).

Implementation

Cross-venue, long-short DIRECTIONAL momentum on ETH that executes ONLY the perp (ETHUSDT.BINANCE) and uses ETHUSDT.BINANCE_SPOT purely as a lead-lag SIGNAL SOURCE (never held) — one round-trip fee, not two-leg basis drag. Each aligned 4h bar it computes perp_ret and spot_ret over lead_lookback bars (spot close matched by TIMESTAMP, not index) and returns signal = spread_bps = (spot_ret - perp_ret)*1e4. Trade direction is sign(spread): the spot-minus-perp return spread alone encodes both the lead/lag and the correct side — spot-led moves (spread>0 on up, <0 on down) are ridden with the perp; perp-led moves are faded against it. Entry fires when |signal| >= spread_entry_bps: signal>0 -> BUY, signal<0 -> SELL. Exit is an ATR stop (atr_stop_mult x ATR) plus a directional exit when the spread crosses zero (lead-lag no longer supports the side). Positions are risk-sized: risk_frac of equity over the ATR stop distance, capped at leveraged equity notional. Three tuned parameters: lead_lookback, spread_entry_bps, atr_stop_mult.

Verification Results

CLEAN RESTART 2026-09-04 — this run's verdict history and learning records were removed and it was restarted from verification. Its previous abandonment came from the pipeline, not from the market: the Layer-2 harness mis-bound @staticmethod helpers (fixed), QA issued terminal performance verdicts on an unoptimized smoke test (removed — QA now judges correctness only), and sandbox timeouts came from backtest-slot starvation (fixed). The hypothesis and the strategy code are unchanged. Verify the code on its merits; performance is decided later by the full backtest and the optimizer.

Verification Results

Net-negative (-8.35%, PF 0.83) on a full-history 237-trade sample despite corrected polarity, with avg_loss > avg_win even though entry gates at |spread|>=12bps and exit flips at 0. The ATR stop vs directional spread-flip exit may cut winners while stops take fuller losses. Optimizer knobs (spread_entry_bps, atr_stop_mult, lead_lookback) control this — an edge/tuning concern, not a code fault. Search them against net-of-fee per-trade return; abandon if no positive-expectancy point.

Backtest Review

Clean single-leg execution avoids the two-leg fee drag that kills delta-neutral basis trades (design intent sound)

Backtest Review

Adequate sample: 249 trades over 6.7 years of full spot+perp history, no data-availability wall

Backtest Review

Low drawdown (11.8%) and no liquidation — sizing is not the problem

Backtest Review

Negative expectancy: -$53.99/trade, total_return -9.54%, Sharpe -0.279 with CI straddling into deeply negative territory

Backtest Review

profit_factor 0.864 (< 1.0) — the return-lead signal has the wrong sign as often as the right one

Backtest Review

avg_trade_return_pct 0.043% is below the 0.15% Binance USD-M fee floor (L22) — no edge even before the negative gross

Backtest Review

Loses in 5 of 7 years across all vol regimes (calm -0.20 / normal -0.59 Sharpe); the spot-vs-perp lead premise is not predictive on ETH

Backtest Review

L73 single-asset cross-venue supplementary-gated directional family — no robustness the sweep can recover

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['mom_scale', 'risk_frac', 'atr_period', 'min_notional', 'atr_stop_mult', 'lead_lookback', 'spread_entry_bps'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: SpotPerpReturnLeadMomentumLS._bts() takes 1 positional argument but 2 were given (bar timestamp: 1735690920000) - steady_downtrend: TypeError: SpotPerpReturnLeadMomentumLS._bts() takes 1 positional argument but 2 were given (bar timestamp: 1735690920000) - flat_ranging: TypeError: SpotPerpReturnLeadMomentumLS._bts() takes 1 positional argument but 2 were given (bar timestamp: 1735690920000) - volatility_spike: TypeError: SpotPerpReturnLeadMomentumLS._bts() takes 1 positional argument but 2 were given (bar timestamp: 1735690920000) - zero_volume: TypeError: SpotPerpReturnLeadMomentumLS._bts() takes 1 positional argument but 2 were given (bar timestamp: 1735690920000) - price_gap: TypeError: SpotPerpReturnLeadMomentumLS._bts() takes 1 positional argument but 2 were given (bar timestamp: 1735690920000)

Iteration History

Verification failed (Layer 4 — QA review): - SIGNAL-POLARITY BUG: trade direction is INVERTED on every down-move. The hypothesis (spot-led→ride with perp direction; perp-led→fade against) reduces to direction = sign(spot_ret - perp_ret) = sign(spread). But signal = tanh(perp_ret/mom_scale) * spread_bps gives sign(signal) = sign(perp_ret)*sign(spread), double-counting perp direction and flipping on down-moves. Cases: perp -5%/spot -6% (spot-led down, should SHORT) → code (-)*(-) = LONG, wrong; perp -5%/spot -4% (perp-led down, should LONG) → code (-)*(+) = SHORT, wrong. Up-moves correct; both down-move cases inverted. - NEAR-ZERO TRADES / MIS-SCALED THRESHOLD: 1 trade in 363 days, metrics_reliable=false. |signal| >= spread_entry_bps(25) rarely fires because spread_bps is the 20-bar spot-minus-perp RETURN spread (single-digit bps for ETH), shrunk further by |dir_factor|<=1 — mis-scaled by ~10x, untradeable. Fixing polarity (signal=spread_bps) removes the dir_factor shrink but not the scale mismatch.

Iteration History

Verification failed (Layer 4 — QA review): - PERSISTENT NEAR-ZERO TRADES from the spot lookup failing in the engine (not a threshold problem). Sandbox: total_trades=4 over 363 days, metrics_reliable=false. Decisive: removing dir_factor increased signal magnitude and spread_entry_bps was lowered 25→12, so entries should increase, not collapse to 4. calculate_signal is returning 0.0 on nearly every bar via `if sc_now is None or sc_prev is None: return 0.0` — the exact-timestamp match spot_map.get(_bts(perp_bar[-1])) misses because the same-period spot bar isn't yet in _extra_bars when the perp bar triggers the callback (cross-instrument arrival ordering). - The positive-edge evidence doesn't reproduce in the engine. Rationale claims a 198-trade/win-0.63 offline replay and derived spread_entry_bps=12 from it, but the sandbox shows 4 trades — a ~50x gap. The offline harness (pre-aligned series) doesn't match the engine's bar delivery, so the trade count, win rate, and positive-return claims can't be trusted. (4 all-loss trades is noise at n=4, not the L17 signature.)
Strategy report

Backtest and paper results are hypothetical. Trading involves risk of loss.