Skip to content

View original

IntradayVolatilityExpansionBreakoutMajorsLS15m

Hypotheses

Binance USD-M Intraday Volatility-Expansion Breakout (Majors, 15-Minute, ATR-Gated Large-Move Filter, ATR-Trailed, Long-Short)

Hypotheses

A short-horizon, long-short trend/breakout strategy on 15-minute bars across the three most liquid Binance USD-M majors (BTC, ETH, SOL), designed to fill the under-represented short_1m_15m horizon bucket WITHOUT falling into the fee-domination trap that kills naive intraday bar strategies. The core idea: trade only GENUINE volatility expansions — bars that break the recent intraday range by a LARGE multiple of ATR — so every entry follows a move that already dwarfs the ~0.10% round-trip fee, and the strict filter keeps trade count low (a few per asset per week, not hundreds per day). This is the same validated trend/breakout primitive as the factory's promoted edge (the Sharpe-1.48 trend basket), just at an intraday cadence to capture the short-horizon continuation that daily strategies miss. It is deliberately on the only reliably-backtestable surface — Binance USD-M majors, pure OHLCV, full history — avoiding every infrastructure block hit this session (Deribit options, COIN-M inverse sizing, Hyperliquid alt bars, liquidations live-WS-only, multi-leg funding primary-only) and every refuted class (no funding, no basis, no ratio MR, no options). Long-short (per-asset directional breakouts) improves the portfolio's 87%-long skew; the ATR-gated large-move requirement is the explicit fee-domination defense; few parameters resist overfitting.

Hypotheses

This was a clean restart with the code unchanged, and an unchanged hash fails verification outright, so I looked for a real defect rather than making a cosmetic edit. I found one in order sizing. Quantities were rounded with a single default precision of 2 decimals for every asset, which is both coarser than the venue permits and outright destructive at the small end: I confirmed that Python's round(0.0049, 2) is 0.0, so any BTC entry below roughly 0.005 BTC collapsed to zero and was silently discarded by the existing `if qty <= 0: return` guard, with no log line and no diagnostic counter. That is not a hypothetical — risk-parity sizing divides a fixed risk budget by the ATR-based stop distance, so exactly the high-volatility regimes this strategy is built to trade produce the smallest quantities, and the effect compounds after any drawdown shrinks equity. I checked the catalog's own instrument definitions rather than assuming: BTCUSDT size_precision 5, ETHUSDT 4, SOLUSDT 3. The fix is a small cached helper, _prec_for(), that rounds with the instrument's own size_precision, falling back to the configured qty_precision map and then to default_qty_precision; it resolves once per asset so the per-bar path stays O(1). I also set the correct per-asset precisions explicitly in the config as a belt-and-braces fallback. Everything else is byte-for-byte the code that already cleared the earlier layers: the Donchian breakout gate, the ATR expansion filter, the trailing stop and max-hold exits, the risk-parity budget, the per-position and gross caps, the incremental ATR/Donchian caching that fixed an earlier wall-clock timeout, and the inert single-position hooks. Imports are unchanged and no banned import was introduced. I added _param_bounds covering every parameter the optimizer can move so it cannot search values outside the intended ranges.

Hypotheses

Repeat liquidation — not optimizable, and already abandoned once for the same cause. The account LIQUIDATED on the exact same date (2020-12-30) as iteration 1, again blowing up in year one (total_return -100%, max_drawdown 100%, then flat/dead for ~5.7 years), which proves the iteration-2 order-size-precision change was a red herring that never addressed the root cause. The mechanism is a 15m over-trading fee catastrophe: 15,962 trades with commission 20.23% of gross ($80k) plus $68k impact, avg_trade_return_pct -0.023% (net negative), profit_factor 0.70, Sharpe -1.06 (CI [-2.99, -0.86]), PSR 0.0, return_kurtosis 1982, negative in every vol regime — the ATR-expansion gate does not cut trade count enough to escape fee domination, and the risk sizing lets losses compound into a blow-up. This is the L18/L30 pure-OHLCV 15m fee graveyard; no parameter change rescues an account that went to zero. Failure pattern: risk_reject/fee_edge liquidated 15m over-trading breakout, repeat of the iteration-1 abandonment.

Implementation

Long-short intraday volatility-expansion breakout on 15-minute bars across the three most liquid Binance USD-M majors (BTC, ETH, SOL), pure OHLCV. Each asset is managed independently and event-driven from its own bar arrival. An entry requires two conditions together: the bar must break the prior channel_length-bar Donchian range (a new N-bar high goes long, a new N-bar low goes short) AND the bar's directional body must exceed expansion_atr_mult x ATR, so only genuine volatility expansions — moves that already dwarf the ~0.10% round-trip fee — can trade. That double filter is the fee-domination defense and keeps the trade count to a few per asset per week rather than hundreds per day. Winners are ridden with an ATR trailing stop anchored to the best excursion since entry and capped by a 32-bar (~8h) max hold. Sizing is risk-parity: each book is sized so its distance-to-stop risk equals risk_frac of equity, capped per position at max_position_frac and across the portfolio at 1.0x gross, so no leverage is used. ATR and the Donchian channel are maintained incrementally and cached once per bar per asset, keeping per-bar cost O(1) with no numpy on the hot path.

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.

Backtest Review

Clean incremental ATR/Donchian engineering; the large trade count makes the negative result statistically decisive

Backtest Review

Account LIQUIDATED again on the SAME date (2020-12-30) as iteration 1: total_return -100%, max_drawdown 100%, CAGR -100% — the precision fix did not touch the root cause

Backtest Review

Fee/impact catastrophe: commission 20.23% of gross ($80k) plus $68k impact over 15,962 trades; avg_trade_return_pct -0.023% (negative), profit_factor 0.70

Backtest Review

Decisively negative: Sharpe -1.06 (CI [-2.99, -0.86]), PSR 0.0, return_kurtosis 1982, 28 consecutive losses, negative in every vol regime

Backtest Review

L18/L30 pure-OHLCV 15m over-trading fee graveyard; the ATR-expansion gate does not keep trade count low enough to escape fee domination

Analysis

Code↔hypothesis misalignment found by the semantic auditor — the code does NOT implement the hypothesis. Re-code the strategy to implement the hypothesis EXACTLY (instrument, timeframe, direction, the named edge/mechanic, sizing). Concrete issues: Hypothesis pre-registers the filter's selectivity as a design property ("the strict filter keeps trade count low (a few per asset per week, not hundreds per day)"), but the implemented gate is not that selective: total_trades=16069, and `total_trades = len(positions_report)` in src/backtesting/metrics.py:724 counts closed round trips, not fills. Catalog history for the three declared legs is BTCUSDT 15m 2019-12-31→2026-09-02, ETHUSDT similar, SOLUSDT 2020-09-14→2026-09-02 ≈ 19 asset-years ≈ 1000 asset-weeks, giving ~16 round trips per asset per week (~2.3/day/asset) — roughly 5x the stated cadence, and higher still if equity was wiped before the end of the window (total_return=-100%). The close>donch_high AND |close-open| >= 1.0*ATR gate at expansion_atr_mult=1.0 admits ~2% of bar events, so the code does not deliver the trade-frequency property the hypothesis specifies.

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['assets', 'risk_frac', 'atr_period', 'trail_mult', 'min_notional', 'max_hold_bars', 'qty_precision', 'channel_length', 'gross_cap_frac', 'max_position_frac', 'expansion_atr_mult', 'default_qty_precision'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: super(type, obj): obj must be an instance or subtype of type (bar timestamp: 1735689600000) - steady_downtrend: TypeError: super(type, obj): obj must be an instance or subtype of type (bar timestamp: 1735689600000) - flat_ranging: TypeError: super(type, obj): obj must be an instance or subtype of type (bar timestamp: 1735689600000) - volatility_spike: TypeError: super(type, obj): obj must be an instance or subtype of type (bar timestamp: 1735689600000) - zero_volume: TypeError: super(type, obj): obj must be an instance or subtype of type (bar timestamp: 1735689600000) - price_gap: TypeError: super(type, obj): obj must be an instance or subtype of type (bar timestamp: 1735689600000)

Iteration History

Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit. 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).
Strategy report

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