← Back to list

hhhl_structure_momentum VALIDATED PASS

Auto-discovered strategy

Symbol: BTC | Exchange: Binance | Role: momentum

5/6
Profitable Years
+86.3%
Total Return
47.5%
Avg Win Rate
1.04
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +22.9% 50.0% 12 9.1% 1.14
2021 +15.2% 50.0% 12 11.0% 0.71
2022 -5.0% 0.0% 1 5.0% 0.00
2023 +15.2% 50.0% 14 10.9% 0.77
2024 +25.4% 63.6% 11 4.7% 1.77
2025 +12.6% 71.4% 7 2.2% 1.86

Performance Chart

Loading chart...

Walk-Forward Validation PASS

1/1 Windows Profitable
+5.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 +5.1% OK 2026-01→ongoing +0.0% PASS

AI Review

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

Source Code

"""
Higher High / Higher Low Structure Momentum Strategy
=====================================================

Trades breakouts in confirmed uptrends using market structure analysis.
Identifies genuine swing points and only enters when HH/HL pattern is present.

CONCEPT:
Market structure analysis is a core price action technique. In an uptrend:
- Price makes Higher Highs (HH): Each swing high exceeds the previous
- Price makes Higher Lows (HL): Each swing low exceeds the previous
This strategy enters on breakouts above swing highs when this pattern exists.

ENTRY CONDITIONS (5 total):
1. Bull regime: EMA50 > EMA200
2. Price above EMA50: Confirmed uptrend
3. Higher High: Latest swing high > Previous swing high
4. Higher Low: Latest swing low > Previous swing low
5. Breakout: Close breaks above latest swing high
6. Volume: Above 1.1x 20-bar average
7. Momentum: 2 consecutive higher closes

EXIT CONDITIONS:
1. Structure break: Close < recent swing low
2. Close below EMA50: Trend weakening
3. Regime change: EMA50 < EMA200
4. Time exit: 20 bars maximum hold
5. Stop loss: 5%
6. Take profit: 10%

SWING POINT DETECTION:
A swing high/low requires 3 bars on each side with lower/higher values.
This ensures we're trading genuine local extrema, not noise.

TRAIN RESULTS (2024-01 to 2025-06):
  2024:   +25.4% | 11 trades | 64% WR | 4.7% DD | Sharpe 1.77
  2025H1: +5.3%  | 3 trades  | 67% WR | 2.2% DD | Sharpe 1.09
  Total: +30.7%

ROBUSTNESS FEATURES:
- Uses only relative indicators (EMAs, swing points, volume ratios)
- Round parameters: 50, 200, 20, 3, 5%, 10%
- Bull-market filter prevents trades in downtrends
- Structure-based exit preserves profits on reversal
- Consecutive higher closes reduces false breakouts

Symbol: BTCUSDT
Exchange: binance
Timeframe: 4h
Role: momentum
"""

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


def init_strategy():
    return {
        'name': 'hhhl_structure_momentum',
        'role': 'momentum',
        'warmup': 200,  # EMA200 + buffer
        'subscriptions': [
            {'symbol': 'BTCUSDT', 'exchange': 'binance', 'timeframe': '4h'},
        ],
        'parameters': {}
    }


def find_swing_points(bars, i, lookback=50, confirmation=3):
    """
    Find confirmed swing highs and lows.

    A swing point requires 'confirmation' bars on each side with lower highs
    (for swing high) or higher lows (for swing low).

    Args:
        bars: List of Bar objects
        i: Current bar index
        lookback: How many bars to look back
        confirmation: Bars required on each side for confirmation

    Returns:
        (swing_highs, swing_lows): Lists of (bar_index, price) tuples
    """
    swing_highs = []
    swing_lows = []

    start = max(confirmation, i - lookback)
    end = i - confirmation

    for j in range(start, end):
        # Check if j is a swing high
        is_high = all(
            bars[j].high >= bars[k].high
            for k in range(j - confirmation, j + confirmation + 1)
            if k != j and 0 <= k < len(bars)
        )
        if is_high:
            swing_highs.append((j, bars[j].high))

        # Check if j is a swing low
        is_low = all(
            bars[j].low <= bars[k].low
            for k in range(j - confirmation, j + confirmation + 1)
            if k != j and 0 <= k < len(bars)
        )
        if is_low:
            swing_lows.append((j, bars[j].low))

    return swing_highs, swing_lows


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

    ENTRY LOGIC:
    1. Bull regime: EMA50 > EMA200
    2. Price > EMA50 (confirmed uptrend)
    3. Find last 2 swing highs and 2 swing lows in 50-bar window
    4. Higher High: latest swing high > previous swing high
    5. Higher Low: latest swing low > previous swing low
    6. Breakout: close > latest swing high
    7. Volume: above 1.1x 20-bar average
    8. Momentum: 2 consecutive higher closes

    EXIT LOGIC:
    1. Structure break: close < recent swing low
    2. Close below EMA50
    3. Regime change: EMA50 < EMA200
    4. Time exit: 20 bars
    5. Stop loss: 5%
    6. Take profit: 10%
    """
    key = ('BTCUSDT', 'binance')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']

    closes = [b.close for b in bars]
    volumes = [b.volume for b in bars]

    # Calculate EMAs
    ema50_vals = ema(closes, 50)
    ema200_vals = ema(closes, 200)

    ema50 = ema50_vals[i]
    ema200 = ema200_vals[i]

    if ema50 is None or ema200 is None:
        return []

    # Regime filter: bull market
    bull_regime = ema50 > ema200
    price_above_ema50 = bars[i].close > ema50

    # Find swing points
    swing_highs, swing_lows = find_swing_points(bars, i, lookback=50, confirmation=3)

    actions = []

    if key not in positions:
        # STAY FLAT in bear markets or when price is below EMA50
        if not (bull_regime and price_above_ema50):
            return []

        # Need at least 2 swing highs and 2 swing lows for HH/HL pattern
        if len(swing_highs) < 2 or len(swing_lows) < 2:
            return []

        # Sort by time
        swing_highs.sort(key=lambda x: x[0])
        swing_lows.sort(key=lambda x: x[0])

        # Get most recent 2 swings
        recent_highs = swing_highs[-2:]
        recent_lows = swing_lows[-2:]

        # HH: latest swing high > previous swing high
        higher_high = recent_highs[1][1] > recent_highs[0][1]

        # HL: latest swing low > previous swing low
        higher_low = recent_lows[1][1] > recent_lows[0][1]

        # Breakout above latest swing high
        latest_swing_high = recent_highs[-1][1]
        breakout = bars[i].close > latest_swing_high

        # Volume confirmation: above 1.1x average
        avg_vol = sum(volumes[i-20:i]) / 20 if i >= 20 else volumes[i]
        vol_confirm = volumes[i] > avg_vol * 1.1

        # Momentum: 2 consecutive higher closes
        momentum = closes[i] > closes[i-1] > closes[i-2]

        # ENTRY: All conditions must be true
        if higher_high and higher_low and breakout and vol_confirm and momentum:
            actions.append({
                'action': 'open_long',
                'symbol': 'BTCUSDT',
                'exchange': 'binance',
                'size': 1.0,
                'stop_loss_pct': 5.0,
                'take_profit_pct': 10.0,
                'metadata': {'entry_swing_low': recent_lows[-1][1]}
            })
    else:
        # === EXIT LOGIC ===
        pos = positions[key]
        bars_held = i - pos.entry_bar

        # Get current swing lows for exit reference
        _, current_swing_lows = find_swing_points(bars, i, lookback=20, confirmation=3)

        # Use entry swing low as reference, update if newer swing forms
        ref_swing_low = pos.metadata.get('entry_swing_low', 0)
        if current_swing_lows:
            current_swing_lows.sort(key=lambda x: x[0])
            ref_swing_low = current_swing_lows[-1][1]

        # Exit conditions
        structure_break = bars[i].close < ref_swing_low
        below_ema50 = bars[i].close < ema50
        regime_exit = ema50 < ema200
        time_exit = bars_held >= 20

        if structure_break or below_ema50 or regime_exit or time_exit:
            actions.append({
                'action': 'close_long',
                'symbol': 'BTCUSDT',
                'exchange': 'binance',
            })

    return actions


# Allow direct execution for testing
if __name__ == "__main__":
    from strategy import backtest_strategy
    results, profitable, _ = backtest_strategy(init_strategy, process_time_step, verbose=True)