Skip to content

View original

EthSpotLedBreakoutPerpContinuationLS

Hypotheses

ETH Spot-Led Breakout Continuation — Cross-Venue Long-Short: Trade the Perp in the Direction of a SPOT Breakout ONLY When the Perp Premium Is NOT Stretched (Organic Spot Demand, Not a Leverage Chase), Exit When Leverage Catches Up (BINANCE_SPOT signal + BINANCE USD-M perp, Daily, 3-Parameter)

Hypotheses

A CROSS-VENUE, LONG-SHORT directional continuation strategy that discriminates breakout QUALITY using the spot-vs-perp lead-lag. The signal comes from Binance SPOT ETH (organic cash demand), the trade is placed on the Binance USD-M ETH perpetual, and the KEY FILTER is the perp basis (premium-to-index): the strategy only takes a spot breakout when the perp premium is NEUTRAL / un-stretched — i.e. the move is being LED by spot buying (persistent, organic) rather than by over-eager leveraged perp longs (fragile, prone to squeeze). It explicitly SKIPS breakouts where the basis is already stretched in the breakout direction (leverage-led chases, the fragile ones that fail). This is the deliberate OPPOSITE-selection complement to my pending basis-ignition idea and is mechanistically distinct from every dead class: NOT a single-venue OHLCV trend clone (it is cross-venue and gated by a real microstructure quality signal), NOT a fade, NOT a market-neutral pair/basket (it carries net directional perp exposure — it is not dollar-neutral), NOT options/CM. Fills the two most under-represented buckets at once: cross_venue (7.3% vs ≥15% target) and long_short (14.2% vs ~45% target).

Hypotheses

ITERATION 2 - single, minimal fix for the reported Layer-2 failure. Layer 1 had already passed and Layer 2 crashed in all six scenarios with 'FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given'. Root cause: the base class's _bar_ts is a @staticmethod, and the Layer-2 _StrategyProxy copies every callable attribute onto a plain wrapper via types.MethodType(func, wrapper) - a staticmethod has no __func__, so it gets rebound as an instance method and self is injected as the first argument. Calling it through self therefore works in the real engine and always TypeErrors in the verification proxy. Fix: added a module-level _ts_of(bar) helper (getattr ts_event, falling back to ts_init) and replaced the three self._bar_ts(...) call sites - one in on_extra_bar (spot leg timestamp), one in calculate_signal (leg-contemporaneity check for the basis sample), one in should_exit (calendar max-hold). NOTHING ELSE CHANGED: identical imports (no new or banned modules, so Layer 1 stays green), identical signal logic, identical entry/exit/sizing rules, identical config and parameters. Note self._bar_interval_ns(self.bar_type) in __init__ is left as-is deliberately - it is also a base staticmethod but it is called on the REAL instance during construction, before the proxy rebinds anything, so it is unaffected. Re-ran Layer 1 and Layer 2 locally after the patch: static passes with no errors and all six synthetic scenarios pass with no exceptions and no frozen-signal warnings, so the signal still varies on every scenario.

Hypotheses

No significant edge and a decayed mechanism — not worth optimizing. Base Sharpe is 0.238 with a bootstrap CI [-0.306, 0.855] that straddles zero, profit_factor 1.15 (<1.2), information_ratio -0.78, and total_return is ~1%/yr over 6.6 years. The edge has decayed: positive 2020-2023 but negative in every recent year (2024 -1.45%, 2025 -3.61%, 2026 -0.94%), with rolling Sharpe collapsing from +3.75 (2023) to -6.4 (2026). The last-20% holdout window falls in that negative recent regime, so the optimizer would fit the early-history windows and die at OOS/holdout. Compounding it, only 70 trades over 6.6 years (~10/yr) is a thin sample for tuning 3 parameters plus a basis filter. It is not fee-limited (avg_trade_return_pct 1.30%) and the cross-venue basis is correctly built from the two legs' own daily bars (no stale-leg artifact) — the mechanism simply no longer has a measurable edge in the recent regime, and tuning cannot conjure one. Same decayed-directional pattern as the recently-abandoned insignificant breakout strategies.

Implementation

Cross-venue long-short continuation strategy: the SIGNAL comes from Binance SPOT ETHUSDT (a Donchian breakout of its own channel_lookback-day channel), the TRADE is placed on the Binance USD-M ETHUSDT perpetual, and the QUALITY FILTER is the perp-vs-spot basis standardized against its own trailing 45-sample distribution. calculate_signal returns the continuous spot channel position cpos = 2*(spot_close - mid)/(hi - lo) every bar (~0 mid-range, +-1 at the edge, >1 on a genuine breakout). should_enter goes LONG on cpos >= 1 only when the basis z-score is NOT already stretched rich (z <= max_entry_basis_z), i.e. the move is led by organic cash demand rather than by a leveraged perp chase, and mirrors that for shorts. Exits fire on the first of: the basis z reaching exit_basis_z in the trade's direction (leverage has caught up), a 2.5-ATR trailing stop off the best close since entry, the breakout failing back to mid-channel, or a 30-calendar-day max hold anchored to the position's ts_opened. Sizing is risk-first: qty = equity * 2% / (2.5 * ATR), capped at 0.75x equity notional.

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

Analyst: verify the basis-z gate improves risk-adjusted return vs the ungated breakout, and confirm OOS/holdout windows keep >=10 trades.

Verification Results

Borderline measurability: 69 trades over the full ~2397-day window (~10.6/yr), inherent to a daily channel breakout held ~9 days. Per-trade edge is healthy (0.64%), so not a no-trades/sub-fee failure, but the basis filter's incremental value is hard to establish on so few trades.

Verification Results

Tighten to diff == 0 if exact contemporaneity matters, else document that a one-bar-stale spot is acceptable.

Verification Results

Basis contemporaneity guard accepts |perp_ts - spot_ts| <= bar_ns, so it can sample basis against a one-bar-stale spot close if dispatch puts the perp leg first. Never forward-looking (spot is same or older), so no future leak; the error is small on daily ETH basis. Non-fatal.

Backtest Review

Genuinely cross-venue, basis-gated directional mechanism (distinct from single-venue breakout clones); basis built from the two legs' own daily bars, no supplementary-feed dependency or obvious stale-leg artifact

Backtest Review

avg_trade_return_pct 1.30% clears the Binance USD-M fee floor; low max_drawdown 7.7%; clean execution (70 signaled, 70 submitted, no drops), balanced long/short (41/29)

Backtest Review

Base Sharpe 0.238 with CI [-0.306, 0.855] straddling zero — no statistically significant edge

Backtest Review

total_return only 6.53% over 6.6 years (~1%/yr), profit_factor 1.15 (<1.2), information_ratio -0.78

Backtest Review

Edge decayed: positive 2020-2023, then negative every year 2024/2025/2026; rolling Sharpe fell from +3.75 to -6.4

Backtest Review

The last-20% holdout window sits in the negative recent regime — optimization would fail OOS/holdout

Backtest Review

Only 70 trades in 6.6 years (~10/yr) — thin sample for fitting 3 params plus a basis filter

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['exit_basis_z', 'min_notional', '_param_bounds', 'channel_lookback', 'max_entry_basis_z'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689600000) - steady_downtrend: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689600000) - flat_ranging: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689600000) - volatility_spike: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689600000) - zero_volume: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689600000) - price_gap: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689600000)
Strategy report

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