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)
Backtest and paper results are hypothetical. Trading involves risk of loss.