Strategies
Hyperliquid Majors 4H Dual-EMA Time-Series (Absolute) Trend Long/Short Basket (ATR-Trailing, 3-Parameter, Managed-Futures Style)
Outcome: Abandoned
HyperliquidMajors4hDualEmaTrendLongShortAtrBasket
Backtest gate: - metrics unreliable: 636 trades / 956 data days is too little evidence (need >=5 trades over >=10 days) — increase signal frequency or widen the window
Hypothesis
Iteration 4 fixes ONLY the reported Layer-3 sandbox_timeout. The prior code recomputed both EMAs and the ATR by looping the entire 500-bar buffer inside calculate_signal()/on_extra_bar() on every bar - roughly 1,500 heavy .as_double() ops per bar event across ~72,000 bar events on five 4H legs, which blew the 300s wall clock. The indicators are now INCREMENTAL: per-instrument state holds the two EMA values, a deque(maxlen=atr_period) of true ranges with a running sum, and the previous close; it is seeded once (a single O(len(buffer)) pass the first time a leg has min_bars) and then advanced in O(1) per new bar, guarded by the bar timestamp so a repeated call for the same bar never double-updates. The ATR remains the same simple mean of the last atr_period true ranges; the EMAs converge to the same values from the one-time seed. Signal logic, entry/exit rules, sizing, config, imports and class structure are otherwise unchanged, so the Layer 1/1.5/2 passes are preserved.
backtest_failed:data: the full backtest could not run 3 times — test_agent.py", line 424, in _ensure_funding_for_perp_legs raise RuntimeError( RuntimeError: Funding data unavailable for perp leg 'AVAXUSD.HYPERLIQUID' — on-demand collection failed or the venue has no funding history (backtest blocked for accuracy; set SF_DISABLE_ONDEMAND_FUNDING=1 to bypass) Pipeline/data failure, not a verdict on the strategy; revive once the cause is fixed.
Implementation
Long/short absolute-momentum (time-series trend) basket on five liquid Hyperliquid perpetuals (BTCUSD, ETHUSD, SOLUSD, AVAXUSD, BNBUSD) on 4-HOUR bars. Each leg is traded independently: long while the 20-EMA is above the 50-EMA, short while below, exiting on the EMA cross flip or an ATR(14) x 3 adverse move from entry. Legs are sized to equal volatility risk (equity * risk_frac * price / (atr_stop_mult * ATR)), capped at 18% of equity each. OHLCV only - no supplementary data.
Verification Results
If a trailing stop is intended, track the position's peak favorable price and stop when price retraces atr_stop_mult*ATR from that peak, rather than from entry.
Hypothesis title says 'ATR-Trailing' stop, but the exit implements a FIXED ATR stop measured from the fill price (entry - price > atr_stop_mult*ATR), not a trailing high-water stop. The config description accurately calls it an 'ATR(14) x 3 adverse move from entry', so intent is documented. The structural risk controls the hypothesis names (ATR stop + EMA cross-flip exit) are both present, so this is not a blocking mismatch — but a true trailing stop would lock in more of a winning trend and better matches the title.
Verify base_template routes warmup through the override; if not, rename to a strategy-private helper to avoid silent shadowing.
min_bars_required() overrides a framework method (static analyzer base_shadow warning). The override returns a valid int (>= ema_slow + 5) and is used for warmup gating; behavior appears intended and correct, but confirm the base template consumes this override rather than its own computation for the warmup gate.
Sandbox smoke test is fragile-positive: profit_factor 1.002, Sharpe 0.497, win_rate 29%, max_drawdown 45.8%. Regime attribution shows only the 'calm' tercile is profitable (+53%) while 'normal' (-31%) and 'stressed' (-27%) both lose — the edge is concentrated in one vol regime. Capacity is low (capacity_usd ~$109K, impact_cost_pct 95.7%, impact_folded) driven by ATR-sizing five legs up to 18% each (avg_position_pct ~19.6%, exposure ~487%); on thinner majors (AVAX/BNB) impact could erode the thin per-trade edge in a full backtest. avg_trade_return_pct 0.44% clears the fee floor, so it is worth a real backtest — but PF barely above 1 and regime concentration are the first things the analyst should stress on full history.
Analysis
Backtest gate: - metrics unreliable: 636 trades / 956 data days is too little evidence (need >=5 trades over >=10 days) — increase signal frequency or widen the window
Iteration History
Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['ema_fast', 'ema_slow', 'min_notional', 'atr_trail_mult'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: HlMajorsDualEmaTrendLongShortBasket._ema() takes 2 positional arguments but 3 were given (bar timestamp: 1735692480000) - steady_downtrend: TypeError: HlMajorsDualEmaTrendLongShortBasket._ema() takes 2 positional arguments but 3 were given (bar timestamp: 1735692480000) - flat_ranging: TypeError: HlMajorsDualEmaTrendLongShortBasket._ema() takes 2 positional arguments but 3 were given (bar timestamp: 1735692480000) - volatility_spike: TypeError: HlMajorsDualEmaTrendLongShortBasket._ema() takes 2 positional arguments but 3 were given (bar timestamp: 1735692480000) - zero_volume: TypeError: HlMajorsDualEmaTrendLongShortBasket._ema() takes 2 positional arguments but 3 were given (bar timestamp: 1735692480000) - price_gap: TypeError: HlMajorsDualEmaTrendLongShortBasket._ema() takes 2 positional arguments but 3 were given (bar timestamp: 1735692480000)
Verification failed (Layer 1 — static analysis): - No class extending FactoryStrategy found (line None) - Required method not implemented: should_enter (line None) - Required method not implemented: calculate_signal (line None) - Required method not implemented: position_size (line None) - Required method not implemented: should_exit (line None)
sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (2400 days of BTCUSD.HYPERLIQUID-4-HOUR-LAST-EXTERNAL). This almost always means per-bar work that scales with history — e.g. rescanning the full funding/supplementary series, or rebuilding a list and calling min()/sorted() inside calculate_signal()/on_bar() on every bar. Precompute sorted timestamp arrays ONCE in __init__ and use bisect, or cache lookups keyed by timestamp, so per-bar cost is O(log n) not O(n).
Backtest Review
- Sharpe
- 0.24
- Total return
- -4.11%
- Max drawdown
- 46.38%
- Trades
- 636
- Win rate
- 28.3%
- Profit factor
- 0.95
Backtest and paper results are hypothetical. Trading involves risk of loss.