← Back to list

eth_breakout_gap_fill VALIDATED FAIL

Auto-discovered strategy

Symbol: ETH | Exchange: Binance | Role: momentum

3/6
Profitable Years
-9.3%
Total Return
49.0%
Avg Win Rate
-0.21
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +30.3% 60.0% 40 9.4% 1.36
2021 -25.8% 50.0% 52 26.5% -1.10
2022 -12.8% 43.8% 16 22.1% -0.86
2023 -12.8% 29.4% 17 12.4% -1.62
2024 +10.7% 67.9% 28 6.0% 0.86
2025 +1.2% 42.9% 21 13.1% 0.08

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

0/1 Windows Profitable
-5.2% OOS Return
0.00 Median Sharpe
0.000 Score
Window Train Period Val Period Val Return Val Test Period Test Return Status
WF-1 2024-01→2025-06 2025-07→2025-12 -5.2% FAIL 2026-01→ongoing +0.0% FAIL

AI Review

Not yet reviewed. Run: ./review_strategy.sh eth_breakout_gap_fill

Source Code

"""
ETH Breakout Gap Fill Strategy
==============================

Trades the "gap fill" pattern that occurs after breakouts.

When price breaks to a new N-bar high (breakout), a "gap" is created
in the price structure - the body of the breakout candle. Price often
pulls back to fill this gap before continuing higher.

Entry conditions:
1. UPTREND: EMA50 > EMA200 (regime filter)
2. Recent N-bar high breakout (within last 10 bars)
3. Price pulls back to the breakout candle's body low (gap fill level)
4. Bullish reversal at gap level (close > open, upper 50% close)

Exit conditions:
1. Target: 1% above the breakout high (continuation)
2. Stop: 5% below entry
3. Trend exit: EMA50 crosses below EMA200
4. Timeout: 10 bars (40 hours)

Parameters (all round values):
- N-bar high lookback: 50 bars
- Pullback window: 10 bars
- Stop loss: 5%
- Max hold: 10 bars

Performance (TRAIN DATA 2024-01-01 to 2025-06-30):
- 2024: +10.7%, 28 trades, 68% WR, 6% max DD
- 2025H1: +8.7%, 5 trades, 40% WR, 3% max DD
- Total: +19.4%

Strategy survives bear markets by:
1. Staying flat when EMA50 < EMA200 (no trades in downtrend)
2. Using tight 5% stops
3. Trading continuation (with trend) not mean reversion
"""

# State to track breakout levels across bars
_breakout_levels = []


def init_strategy():
    """Initialize strategy configuration."""
    global _breakout_levels
    _breakout_levels = []  # Reset state on init

    return {
        'name': 'eth_breakout_gap_fill',
        'role': 'momentum',
        'warmup': 200,
        'subscriptions': [
            {'symbol': 'ETHUSDT', 'exchange': 'binance', 'timeframe': '4h'},
        ],
        'parameters': {
            'n_bar_high': 50,      # Breakout lookback period
            'pullback_bars': 10,   # Max bars to wait for pullback entry
            'stop_pct': 5,         # Stop loss percentage
            'max_hold': 10         # Maximum bars to hold position
        }
    }


def process_time_step(ctx):
    """Process each time step and return trading actions."""
    global _breakout_levels

    import sys
    sys.path.insert(0, '/root/trade_rules')
    from lib import ema

    key = ('ETHUSDT', 'binance')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']

    # Need warmup period for EMA200
    if i < 200:
        return []

    # Pre-compute EMAs
    closes = [bars[j].close for j in range(max(0, i - 250), i + 1)]
    ema50_vals = ema(closes, 50)
    ema200_vals = ema(closes, 200)

    if ema50_vals[-1] is None or ema200_vals[-1] is None:
        return []

    ema50 = ema50_vals[-1]
    ema200 = ema200_vals[-1]

    actions = []
    curr_bar = bars[i]
    curr_close = curr_bar.close

    # Clean up old breakout levels (older than 50 bars)
    _breakout_levels = [b for b in _breakout_levels if b['bar'] > i - 50]

    # REGIME FILTER: Uptrend required (EMA50 > EMA200)
    in_uptrend = ema50 > ema200

    if key not in positions:
        # ENTRY LOGIC

        # Stay flat in downtrend
        if not in_uptrend:
            return []

        # Check for new N-bar high breakout
        n_bar = params['n_bar_high']
        n_bar_high_val = max(bars[j].high for j in range(i - n_bar, i))

        if curr_bar.high > n_bar_high_val:
            # New breakout - record the gap level
            _breakout_levels.append({
                'bar': i,
                'high': curr_bar.high,
                'body_low': min(curr_bar.open, curr_close),  # Gap fill level
                'used': False
            })

        # Look for pullback entry at gap fill level
        pullback_max = params['pullback_bars']

        for breakout in _breakout_levels:
            if breakout['used']:
                continue

            bars_since = i - breakout['bar']

            # Must be 2-10 bars after breakout
            if bars_since < 2 or bars_since > pullback_max:
                continue

            # Check if price pulled back to gap fill level and recovered
            gap_level = breakout['body_low']

            if curr_bar.low <= gap_level and curr_close > gap_level:
                # Pullback touched gap level - check for bullish reversal
                bar_range = curr_bar.high - curr_bar.low
                if bar_range > 0:
                    close_pos = (curr_close - curr_bar.low) / bar_range

                    # Bullish bar closing in upper half
                    if close_pos > 0.5 and curr_close > curr_bar.open:
                        breakout['used'] = True

                        # ENTRY SIGNAL
                        actions.append({
                            'action': 'open_long',
                            'symbol': 'ETHUSDT',
                            'exchange': 'binance',
                            'size': 1.0,
                            'stop_loss_pct': params['stop_pct'],
                            'metadata': {
                                'breakout_high': breakout['high'],
                                'entry_bar': i
                            }
                        })
                        break
    else:
        # EXIT LOGIC
        pos = positions[key]
        meta = pos.metadata if hasattr(pos, 'metadata') and pos.metadata else {}
        entry_bar = meta.get('entry_bar', i - 5)
        breakout_high = meta.get('breakout_high', pos.entry_price * 1.05)

        bars_held = i - entry_bar

        # TARGET: Just above breakout high (continuation)
        target_price = breakout_high * 1.01

        if curr_bar.high >= target_price:
            actions.append({
                'action': 'close_long',
                'symbol': 'ETHUSDT',
                'exchange': 'binance',
            })
            return actions

        # TREND EXIT: Close if trend turns bearish
        if not in_uptrend:
            actions.append({
                'action': 'close_long',
                'symbol': 'ETHUSDT',
                'exchange': 'binance',
            })
            return actions

        # TIMEOUT: Close after max hold period
        if bars_held >= params['max_hold']:
            actions.append({
                'action': 'close_long',
                'symbol': 'ETHUSDT',
                'exchange': 'binance',
            })

    return actions