Skip to content

View original

SolOpeningRangeBreakoutContinuationLS

Hypotheses

SOL Intraday Opening-Range Breakout Continuation, Long-Short (Binance USD-M, 1H, Session-Anchored 00:00 UTC, Flat End-of-Day, ATR-Buffered Entry, Discrete Capital-Capped)

Hypotheses

A LONG-SHORT, single-instrument, pure-OHLCV INTRADAY SESSION-MOMENTUM continuation strategy on SOLUSDT.BINANCE USD-M perpetual using 1-HOUR bars. Each UTC day it anchors an OPENING RANGE over the first few hours from the 00:00 UTC daily/funding-clock boundary, then enters in the direction of the FIRST confirmed breakout of that range, rides the intraday continuation, and flattens by end of day. It deliberately combines the two empirically-strongest facts the factory has established: (1) intraday CONTINUATION on a PROVEN asset — SOL is the second validated intraday-continuation instrument (its impulse-continuation sibling reached paper_stage ~0.69), so its clean high-beta directional thrusts continue; and (2) a trigger DISTINCT from the single-impulse-bar already used on SOL (a time-anchored opening-range break, capturing documented crypto session-momentum / Asia-US directional drives, rather than a magnitude-outlier bar) → a different trade population, lower correlation with the SOL impulse instance, genuinely additive. It targets SOL specifically because the impulse-continuation family's boundary is now mapped — it holds on ETH/SOL but FAILS on BTC (degrades forward), LINK, and XRP — so a proven asset (SOL) has a far higher prior than another BTC variant or a brand-new mechanism (every new mechanism this batch — funding carry, relative momentum, cross-venue — died). It avoids EVERY dead/blocked path: NOT funding-carry (the entire family is fee-dominated/event-sparse), NOT cross-venue (fee floor), NOT reversion (dead), NOT a daily-trend or relative-momentum majors-basket (correlated re-expressions of promoted AbsoluteMomentum), NOT options (engine), NOT HL intraday (~7-month data wall), NOT sub-1H (fee-dead), NOT a confluence/overlay (filters have degraded everything). It is distinct from the in-pipeline ETH opening-range-breakout (different asset) and the SOL impulse instance (different trigger). Fee-survivable (<=1 trade/day, flat end-of-day -> ~250 trades/yr, low fee incidence; a confirmed session break runs well past the ~0.10% RT) and validatable (SOL Binance USD-M deep 1H history -> ~1500 trades over ~6yr to populate the walk-forward). Engine-safe: discrete enter-once/exit-once, single position, hard-capped notional, no continuous rebalance/vol-scaling. Three core parameters.

Hypotheses

Fixes the Layer-2 failure (`_bar_event_ts() takes 1 positional argument but 2 were given`) with the minimum possible change: the strategy no longer calls ANY helper or staticmethod from the hot path or from `__init__`. The bar interval is now derived once in `__init__` by parsing the bar-type string (`"SOLUSDT.BINANCE-1-HOUR-LAST-EXTERNAL"` -> step 1 x HOUR), and the bar timestamp is read inline via `getattr(bar, 'ts_event', ...)` inside `calculate_signal`. The Layer-2 proxy re-binds every instance callable with `types.MethodType(fn, wrapper)`, which injects `self` into staticmethods and breaks them; by eliminating those calls entirely the proxy has nothing to corrupt. Layer-1 state is preserved (no new imports, same class/method structure) and the passing signal logic is untouched: `calculate_signal` still returns the continuous, unbounded breakout score in buffered-band units ((close - OR_mid) / (OR_half + buffer_atr * ATR)), varying every bar, with the entry decision thresholded in `should_enter` against `entry_threshold` (~1.0 = the band edge). Session mechanics (00:00 UTC anchor, one entry/day, EOD flat), risk (2.5% stop / 5% target), and leverage-aware sizing (`equity * capital_frac * self.config.leverage / price`, consuming the 2x futures leverage) are unchanged. Futures venue is correct: the strategy goes both long and short and uses leverage > 1.

Hypotheses

max_iterations_after_review

Implementation

Long-short intraday opening-range breakout continuation on SOLUSDT.BINANCE USD-M perpetual, 1-HOUR bars. Each UTC day builds an opening range over the first `or_hours` bars from the 00:00 UTC boundary, then takes the first confirmed break of an ATR-buffered band around that range (long above, short below), holds the intraday continuation, and flattens by `eod_flat_hour`. Stop-loss / take-profit guard the trade; no new entries after `entry_cutoff_hour`. One entry and one exit per UTC day, single position, notional capped at `capital_frac * equity * leverage`.

Verification Results

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['or_hours', 'atr_period', 'buffer_atr', 'capital_frac', 'eod_flat_hour', 'stop_loss_pct', 'entry_threshold', 'take_profit_pct', 'entry_cutoff_hour'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: SolOpeningRangeBreakoutContinuationLS._bar_event_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - steady_downtrend: TypeError: SolOpeningRangeBreakoutContinuationLS._bar_event_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - flat_ranging: TypeError: SolOpeningRangeBreakoutContinuationLS._bar_event_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - volatility_spike: TypeError: SolOpeningRangeBreakoutContinuationLS._bar_event_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - zero_volume: TypeError: SolOpeningRangeBreakoutContinuationLS._bar_event_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - price_gap: TypeError: SolOpeningRangeBreakoutContinuationLS._bar_event_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000)

Backtest Review

The session/opening-range state machine appears to work as designed: enter-once-per-day is enforced by _entered_today, the UTC day_key rollover resets the range, and the long/short split is near-balanced (888 LONG / 862 SHORT), consistent with a symmetric breakout trigger.

Backtest Review

The instrument and timeframe are valid and data-rich: 50,969 1H bars over 2020-09-14 -> 2026-07-08, with no data-coverage truncation of the kind that would make the test meaningless.

Backtest Review

The defects identified are concrete, localized, and fixable in position_size() and should_exit() — this is not a premise that has been tested and failed.

Backtest Review

BLOCKING: the account was LIQUIDATED on 2021-01-15, four months into a six-year backtest. liquidated=true, total_return=-100.0, max_drawdown=100.0, cagr=-100.0. equity_curve goes 100000.0 -> 0.0 and never recovers.

Backtest Review

The engine kept generating fills for 5.5 years on a zero-equity account. Per-year: 2020 100 trades/-$89,986 (avg qty 25,352 SOL), 2021 336/-$34,430, 2022 327/-$2,106, 2023 326/-$141, 2024 333/-$98, 2025 328/-$23 (avg qty 0.065 SOL). The final trade is 0.041 SOL for -$0.18. Roughly 1,636 of 1,750 trades are accounting artifacts.

Backtest Review

CONSEQUENCE: every aggregate metric in this report is polluted by those phantom fills and measures nothing about the strategy — win_rate 0.4251, profit_factor 0.4549, sharpe_ratio -0.6068, expectancy -$72.45, avg_trade_return -$72.45, total_trades 1750. Only ~114 trades ran on live capital.

Backtest Review

SIZING BUG: on the FIRST trade, from flat, with equity known to be exactly $100,000, quantity 45,592.705 x entry_price 3.29 = $150,000 notional (engine-reported leverage 1.5), but capital_frac=0.75 with no leverage in config implies an intended $75,000. Exactly 2x. The ratio then CLIMBS as equity collapses: 1.50 -> 3.63 -> 3.96 -> 4.29. Notional does not track shrinking equity. This is the liquidation mechanism. I am NOT asserting the root cause — it has not been traced, and an unverified diagnosis would plant a false lesson.

Backtest Review

STOP IS FICTION: stop_loss_pct=2.5 but trade #1 realized pnl_pct -8.32% and largest_loss is -$12,484. should_exit() polls get_position_pnl_pct() only at 1H bar close, so adverse intraday runs sail past the level. The payoff inverts — avg_win $142.21 < avg_loss $231.20 and tail_ratio 0.2211 — despite a 5.0% TP against a 2.5% SL.

Backtest Review

MECHANISM/RISK-MODEL MISMATCH: the hypothesis promises 'hard-capped notional' and '~250 trades/yr'. The code has no notional cap and delivers ~330 trades/yr (2021: 336, 2022: 327, 2023: 326, 2024: 333, 2025: 328). An opening-range breakout firing on ~90% of days is selecting noise, not breakouts — entry_threshold=1.0 sits exactly at the buffered band edge.

Backtest Review

The only live-capital window (2020-09 -> 2021-01) is un-tradeable: SOL was a newly listed $1.50-$3.30 microcap, total_impact_usd $26,951 is 3x total_commission $8,669, and capacity_usd is null. A $150,000 notional was not executable at the assumed fills.

Backtest Review

Supporting tail evidence, all downstream of the blow-up: return_kurtosis 147.8, annualized_volatility 92.21%, downside_deviation 87.54, cvar_95 11.38% vs var_95 0.76%, information_ratio -2.0513, probabilistic_sharpe 0.0579, max_drawdown_duration_days 1919.96, max_consecutive_losses 12.

Backtest Review

1.5x equity ($150k) vs 0.75x intended ($75k)

Backtest Review

0.75x

Analysis

Full writeup in workspace/discussions/bc006cfb-f6a0-41da-98da-f1ce6987b523/iteration_1_feedback.md. Three blocking fixes, in order: (1) SIZING — the account is liquidated on 2021-01-15, so no optimization is meaningful. Replace the fixed capital fraction with risk-based sizing: risk_budget = equity * risk_per_trade (0.005-0.01, NOT 0.75); stop_distance = price * stop_loss_pct/100; size = risk_budget / stop_distance; hard-cap notional at 1.0 * equity. Set leverage = 1.0. Expose risk_per_trade (not capital_frac) as the sizing parameter for the optimizer. (2) EXITS — submit a protective stop_market() (and optionally a limit() TP) via self.order_factory AT ENTRY TIME so the stop triggers intrabar, instead of the current close-only should_exit() check that fills a bar late (losers realize -3.01% against a 2.5% nominal stop). Keep the 23:00 EOD flat as the time-based exit. (3) SELECTIVITY — the entry population itself has negative expectancy (mean -0.48%/trade, PF 0.71, negative on both long and short), so fixing exits alone will not save it (0.43 x 2.74% - 0.57 x 2.50% = -0.25%/trade even with a perfect stop). Raise entry_threshold to 1.2-2.0 and buffer_atr to 0.4-1.0; require the breakout bar to CLOSE beyond the band AND have range above the opening-range average (a genuine session drive); test or_hours 2-6; set take_profit_pct near the realized winner distribution (~3%) or replace it with an ATR trailing stop so continuation is allowed to run to the EOD flat. ACCEPTANCE TEST before this returns: liquidated=false, max_drawdown < 25%, non-zero returns in EVERY year 2021-2026 (proof the account survived), mean per-trade pnl_pct > +0.15% net of fees, profit_factor > 1.2, and avg winner > avg loser. If mean per-trade pnl_pct is still negative after all three fixes, ABANDON — the trade population is negative on both sides and no stop repair can rescue an entry with negative expectancy.

Outcome Summary

SolOpeningRangeBreakoutContinuationLS tried to combine two facts the factory had already established — that SOL exhibits intraday continuation, and that a time-anchored opening-range break is a distinct trigger from the single-impulse-bar already deployed on the same asset — into a fee-survivable one-trade-per-day session-momentum strategy. It never got the chance to prove or disprove that thesis: the account was liquidated four months into a six-year backtest, and the engine kept filling orders on zero equity for the remaining 5.5 years, leaving 1,750 nominal trades of which only ~114 traded real capital. The backtest reviewer traced the blow-up to sizing that opened 1.5x equity in notional on the first trade and drifted to 4.29x as equity fell, plus a 2.5% stop that a bar-close-only exit check let run to -8.32%; it also flagged that firing on ~90% of days (~330 trades/yr against a promised ~250) meant the entry_threshold of 1.0 was selecting noise at the band edge. Seven iterations failed to clear the gate, and the strategy was abandoned at backtest review under `max_iterations_after_review` — the hypothesis remains untested, since what failed was the implementation, not the premise.

Outcome Summary

Bar-close-only exits do not enforce an intraday stop, and position sizing must be recomputed against current equity every entry — without both, a liquidation early in the window silently converts the remaining years of a backtest into meaningless phantom fills.

Outcome Summary

The backtest-review gate returned an `iterate` verdict rather than passing the strategy to optimization, citing liquidation, a position-sizing bug (first trade opened $150k notional against $100k equity at capital_frac=0.75, and the notional-to-equity ratio climbed from 1.50 to 4.29 as equity collapsed), and a stop-loss that never bound (2.5% configured, -8.32% realized on trade #1 because should_exit() only polls at 1H bar close). After 7 iterations the strategy hit `max_iterations_after_review` and was abandoned; optimization, post-optimization analysis, and risk review were never reached.

Outcome Summary

A long-short SOLUSDT 1H intraday strategy that anchored an opening range to the 00:00 UTC session boundary, entered the first ATR-buffered breakout of that range, and flattened end-of-day, betting that SOL's proven intraday continuation would extend a confirmed session break past trading costs.

Outcome Summary

The backtest liquidated the account on 2021-01-15, roughly four months into a 2020-09 to 2026-07 window, ending at -100.0% total return, 100.0% max drawdown, and a Sharpe of -0.61 across 1,750 recorded trades (888 long / 862 short, 42.5% win rate, profit factor 0.45, expectancy -$72.45). The backtest reviewer found only ~114 of those trades ran on live capital — the remaining ~1,636 were fills on a zero-equity account, so every aggregate metric is uninterpretable.

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['or_hours', 'atr_period', 'buffer_atr', 'capital_frac', 'min_notional', 'eod_flat_hour', 'stop_loss_pct', 'entry_threshold', 'take_profit_pct', 'entry_cutoff_hour'] 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: 1735691100000) - steady_downtrend: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - flat_ranging: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - volatility_spike: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - zero_volume: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000) - price_gap: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691100000)
Strategy report

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