← Back to list

eth_hhll_breakout VALIDATED FAIL

Auto-discovered strategy

Symbol: ETH | Exchange: Bitfinex | Role: momentum

2/6
Profitable Years
+91.0%
Total Return
40.0%
Avg Win Rate
-0.04
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +98.5% 100.0% 3 0.0% 8.53
2021 -11.9% 20.0% 5 28.4% -0.34
2022 -34.8% 0.0% 3 31.0% -9.75
2023 -1.5% 33.3% 3 19.3% -0.06
2024 +43.0% 66.7% 3 9.6% 1.42
2025 -2.2% 20.0% 5 32.9% -0.05

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

0/1 Windows Profitable
-14.1% 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 -14.1% FAIL 2026-01→ongoing +0.0% FAIL

AI Review

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

Source Code

"""
ETH HHLL Breakout Strategy
===========================

A momentum strategy that enters on N-bar high breakouts when the market
exhibits bullish price structure (Higher Highs and Higher Lows).

Core Logic:
- Entry: 50-bar high breakout with confirmed HH+HL structure in bull regime
- Exit: Structure break (close below swing low) or stop/target hit
- Risk: Dynamic stop below swing low, 3:1 reward/risk target

Key Principles:
- Uses RELATIVE indicators only (EMAs, bar counts, % moves)
- ROUND parameters (15, 20, 50, 100)
- Regime filter prevents trading against macro trend
- Structure-based entries avoid chasing

Train Period Results (2024-01 to 2025-06):
- 2024: +43.0% | 3 trades | 67% WR | 9.6% DD
- 2025: +20.3% | 3 trades | 33% WR | 14.7% DD
- Total: +63.3% | 6 trades | Max DD 14.7%
"""

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


def init_strategy():
    """Initialize the HHLL Breakout strategy"""
    return {
        'name': 'eth_hhll_breakout',
        'role': 'momentum',  # Trend-following, allowed to lose bounded in bear markets
        'warmup': 200,       # Need EMA100 and swing lookback to be valid
        'subscriptions': [
            {'symbol': 'tETHUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {
            'breakout_period': 50,   # N-bar high breakout
            'swing_period': 15,      # Swing detection period
            'ema_fast': 20,          # Fast EMA for regime
            'ema_slow': 100,         # Slow EMA for regime
            'min_swing_pct': 3,      # Min % for valid swing point
        }
    }


def ema_calc(prices, period):
    """Calculate Exponential Moving Average"""
    if len(prices) < period:
        return [None] * len(prices)
    result = [None] * (period - 1)
    k = 2 / (period + 1)
    sma_val = sum(prices[:period]) / period
    result.append(sma_val)
    for j in range(period, len(prices)):
        result.append(prices[j] * k + result[-1] * (1 - k))
    return result


def get_swing_points(bars, idx, period, min_pct):
    """
    Find swing highs and lows with minimum percentage requirement.

    A swing high is a local maximum over 2*period bars.
    A swing low is a local minimum over 2*period bars.

    Returns: (swing_highs, swing_lows) as lists of (index, price) tuples
    """
    highs = [b.high for b in bars]
    lows = [b.low for b in bars]

    swing_highs = []
    swing_lows = []

    # Find swing highs - scan backward from current bar
    for i in range(idx - period, max(period, idx - 200), -1):
        if i < period or i >= len(bars) - period:
            continue

        # Check if this is a local maximum
        window_highs = highs[i-period:i+period+1]
        if highs[i] == max(window_highs):
            # Verify swing has minimum size
            nearby_low = min(lows[max(0, i-period):i+period+1])
            swing_size = (highs[i] - nearby_low) / nearby_low * 100

            if swing_size >= min_pct:
                # Don't cluster swings too close together
                if not swing_highs or abs(i - swing_highs[-1][0]) >= period:
                    swing_highs.append((i, highs[i]))
                    if len(swing_highs) >= 2:
                        break

    # Find swing lows - scan backward from current bar
    for i in range(idx - period, max(period, idx - 200), -1):
        if i < period or i >= len(bars) - period:
            continue

        # Check if this is a local minimum
        window_lows = lows[i-period:i+period+1]
        if lows[i] == min(window_lows):
            # Verify swing has minimum size
            nearby_high = max(highs[max(0, i-period):i+period+1])
            swing_size = (nearby_high - lows[i]) / lows[i] * 100

            if swing_size >= min_pct:
                if not swing_lows or abs(i - swing_lows[-1][0]) >= period:
                    swing_lows.append((i, lows[i]))
                    if len(swing_lows) >= 2:
                        break

    return swing_highs, swing_lows


def process_time_step(ctx):
    """
    Process each bar and generate trading signals.

    Entry Conditions (ALL must be true):
    1. Bull regime: EMA20 > EMA100 AND price > EMA20
    2. Bullish structure: Higher High + Higher Low confirmed
    3. 50-bar high breakout

    Exit Conditions (ANY triggers exit):
    1. Structure break: Close below swing low (with 2% buffer)
    2. Stop loss hit (dynamic, based on swing low)
    3. Take profit hit (3x stop distance)
    """
    key = ('tETHUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    state = ctx['state']
    params = ctx['parameters']

    closes = [b.close for b in bars]
    highs = [b.high for b in bars]

    # Calculate EMAs for regime filter
    ema_fast = ema_calc(closes, params['ema_fast'])
    ema_slow = ema_calc(closes, params['ema_slow'])

    if ema_fast[i] is None or ema_slow[i] is None:
        return []

    # REGIME FILTER: Only trade in bull regime
    # This prevents entering during downtrends
    bull_regime = ema_fast[i] > ema_slow[i] and closes[i] > ema_fast[i]

    current_price = closes[i]
    current_high = highs[i]

    breakout_period = params['breakout_period']
    if i < breakout_period:
        return []

    # N-bar high breakout detection
    prev_high = max(highs[i-breakout_period:i])
    breakout = current_high > prev_high

    # Get swing structure for HH/HL detection
    swing_highs, swing_lows = get_swing_points(
        bars, i, params['swing_period'], params['min_swing_pct']
    )

    # Check for Higher High and Higher Low (bullish price structure)
    # swing_highs[0] is most recent, swing_highs[1] is previous
    has_hh = len(swing_highs) >= 2 and swing_highs[0][1] > swing_highs[1][1]
    has_hl = len(swing_lows) >= 2 and swing_lows[0][1] > swing_lows[1][1]
    bullish_structure = has_hh and has_hl

    # Initialize state for tracking
    if 'entry_swing_low' not in state:
        state['entry_swing_low'] = None

    actions = []

    if key not in positions:
        # ENTRY LOGIC
        if bull_regime and bullish_structure and breakout:
            # Set stop below most recent swing low
            entry_swing_low = swing_lows[0][1] if swing_lows else current_price * 0.95
            stop_dist = (current_price - entry_swing_low) / current_price * 100
            stop_pct = max(5, stop_dist)  # At least 5% stop

            # Only enter if stop is reasonable
            if stop_pct < 15:
                actions.append({
                    'action': 'open_long',
                    'symbol': 'tETHUSD',
                    'exchange': 'bitfinex',
                    'size': 1.0,
                    'stop_loss_pct': stop_pct,
                    'take_profit_pct': stop_pct * 3,  # 3:1 R/R
                })
                state['entry_swing_low'] = entry_swing_low
    else:
        # EXIT LOGIC: Structure break (close below swing low with 2% buffer)
        entry_swing_low = state.get('entry_swing_low')

        if entry_swing_low and closes[i] < entry_swing_low * 0.98:
            actions.append({
                'action': 'close_long',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })
            state['entry_swing_low'] = None

    return actions


# For standalone testing
if __name__ == '__main__':
    from strategy import backtest_strategy

    results, profitable, _ = backtest_strategy(init_strategy, process_time_step)
    print(f"\nProfitable years: {profitable}/2")