← Back to list

swing_hl_breakout VALIDATED PASS

Auto-discovered strategy

Symbol: BTC | Exchange: Bitfinex | Role: momentum

6/6
Profitable Years
+73.5%
Total Return
51.4%
Avg Win Rate
0.89
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +11.4% 38.9% 18 17.8% 0.52
2021 +3.7% 41.2% 17 16.2% 0.15
2022 +2.3% 50.0% 2 4.5% 0.29
2023 +18.6% 53.3% 15 6.8% 1.06
2024 +20.8% 53.3% 15 6.6% 1.50
2025 +16.6% 71.4% 7 3.8% 1.81

Performance Chart

Loading chart...

Walk-Forward Validation PASS

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

AI Review Score: 72/100

complexity execution
## Swing Higher Low Breakout Strategy Review ### Strengths 1. **Proper Regime Filter**: Uses EMA50/200 crossover to stay flat in bear markets - good risk management principle 2. **No Magic Numbers**: Parameters (50, 200, 20, 40) follow standard values from the rules 3. **Adequate Warmup**: 210 bars properly covers EMA200 requirement 4. **Real Swing Detection**: Uses 3-bar confirmation for local extrema - conceptually sound 5. **Multiple Exit Conditions**: Structure break, regime change, time-based, stops - shows defensive thinking 6. **Volume Confirmation**: Requires 110% of average volume for breakout validation 7. **Reasonable Risk/Reward**: 4.5% stop / 9.0% target gives 2:1 ratio ### Issues Identified #### 1. **Complexity Flag - Too Many Entry Conditions (7 total)** The strategy checks: - Bull regime (EMA50 > EMA200) - Higher low structure - Higher high structure - Breakout above swing high - Volume > 110% average - Bullish candle close - Minimum 2 swing lows + 2 swing highs This exceeds the 5-6 condition guideline. More conditions = higher risk of curve-fitting to historical data. The strategy might be "learning" specific train period patterns rather than capturing robust market behavior. **Recommendation**: Simplify to core conditions. Consider removing either volume confirmation OR bullish candle requirement. #### 2. **Execution Flag - Swing Detection Logic Issue** The swing detection scans `range(i-40, i-3)` and checks 3 bars on each side. This creates a potential lookahead issue: ```python for k in range(j-3, j+4): # Checks j+1, j+2, j+3 if k != j and 0 <= k < len(bars): if bars[k].low < bars[j].low: ``` When `j = i-3`, the code checks bars at `i-2, i-1, i` (future bars relative to the swing point). While the strategy only looks at bars up to `i-3` as swing candidates, the confirmation logic itself may reference bars that weren't "settled" in real-time. **Impact**: Minor - the 3-bar buffer before `i` likely prevents true lookahead, but the logic could be clearer. #### 3. **Edge Case Handling** Exit logic recalculates swing lows over `range(i-20, i-3)` but falls back to `min(lows[i-10:i])` if none found. This fallback could trigger false exits in choppy markets where swing detection fails. ### Validation Performance Analysis The validation metrics show the strategy passed minimum requirements for momentum role: - Validation return: +1.21% (exceeds -15% minimum) - Total return: 73.53% suggests strong train performance - Sharpe: 0.89 (exceeds -0.5 minimum) The modest validation gain (+1.21%) compared to strong train performance (+36.2% claimed) suggests the strategy may have benefited from train period characteristics. However, it didn't fail catastrophically, indicating some robustness. ### Statistical Significance Train period: 19 trades total - adequate for minimum 3 trades requirement. Validation period trade count not provided, but passing validation suggests at least 3 trades occurred. **Concentration Risk**: Not assessable without per-trade breakdown. Should verify top 3 trades don't exceed 40% of total PnL. ### Code Quality - Clear docstring with strategy concept - Proper warmup calculation - Readable variable names - Good comments explaining logic - Minor issue: nested loop complexity in swing detection (O(n²) but acceptable for 40-bar window) ### Overall Assessment This is a **fundamentally sound strategy with execution concerns**. The core concept (structure-based breakouts in bull markets) is robust. However, the 7-condition entry and swing detection complexity introduce fragility. The strategy likely works but may underperform expectations in live markets due to: 1. Overly specific entry requirements reducing trade frequency 2. Potential subtle timing issues in swing confirmation 3. Risk that train period had ideal "swing trading" conditions **Score Justification**: 72/100 = Good tier. Passes validation requirements, shows defensive design, but complexity and execution concerns prevent excellence rating.
Reviewed: 2026-01-14T04:29:16.828289

Source Code

"""
Swing Higher Low Breakout Strategy
==================================

Detects genuine swing points (local extrema) and trades breakouts
in established uptrends with proper higher-high, higher-low structure.

Key features:
- Real swing detection (3-bar confirmation on each side)
- Higher low + higher high confirmation
- Volume-confirmed breakout above recent swing high
- EMA50/200 regime filter (only trade in bull markets)
- Tight stop loss (4.5%) for bear market survival

TRAIN RESULTS (2024-01 to 2025-06):
  2024: +20.8% | 15 trades | 53% WR | 6.6% DD | Sharpe 1.50
  2025H1: +15.3% | 4 trades | 75% WR | 0.8% DD | Sharpe 2.56
  Total: +36.2%

Designed to survive bear markets by:
1. Staying flat when EMA50 < EMA200
2. Using tight 4.5% stop losses
3. Exiting on structure break (close below swing low)
4. Time-based exit (max 20 bars)

Symbol: tBTCUSD
Exchange: bitfinex
Timeframe: 4h
"""

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


def init_strategy():
    return {
        'name': 'swing_hl_breakout',
        'role': 'momentum',
        'warmup': 210,  # EMA200 + 10 bars buffer
        'subscriptions': [
            {'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {}
    }


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

    Entry Logic:
    - Bull regime: EMA50 > EMA200
    - Find swing lows and highs in last 40 bars
    - Higher Low: most recent swing low > previous swing low
    - Higher High: most recent swing high > previous swing high
    - Breakout: close > most recent swing high
    - Volume: current volume > 110% of 20-bar average
    - Momentum: bullish candle (close > open)

    Exit Logic:
    - Structure break: close < recent swing low
    - Regime change: EMA50 < EMA200
    - Time exit: held for 20 bars
    - Stop loss: 4.5%
    - Take profit: 9.0%
    """
    key = ('tBTCUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']

    # warmup=210 in init_strategy ensures we have 200+ bars for EMA200

    closes = [b.close for b in bars[:i+1]]
    highs = [b.high for b in bars[:i+1]]
    lows = [b.low for b in bars[:i+1]]
    volumes = [b.volume for b in bars[:i+1]]

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

    ema50_now = ema50_vals[i]
    ema200_now = ema200_vals[i]

    # CRITICAL: Only trade in bull regime
    bull_regime = ema50_now > ema200_now

    actions = []

    if key not in positions:
        # STAY FLAT in bear markets
        if not bull_regime:
            return []

        # Find swing points in last 40 bars
        # Swing = local extrema with 3-bar confirmation on each side
        swing_lows = []
        swing_highs = []

        for j in range(i-40, i-3):
            if j < 3:
                continue

            # Check if j is a swing low
            is_swing_low = True
            for k in range(j-3, j+4):
                if k != j and 0 <= k < len(bars):
                    if bars[k].low < bars[j].low:
                        is_swing_low = False
                        break
            if is_swing_low:
                swing_lows.append((j, bars[j].low))

            # Check if j is a swing high
            is_swing_high = True
            for k in range(j-3, j+4):
                if k != j and 0 <= k < len(bars):
                    if bars[k].high > bars[j].high:
                        is_swing_high = False
                        break
            if is_swing_high:
                swing_highs.append((j, bars[j].high))

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

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

        # Get last 2 of each
        last_two_lows = swing_lows[-2:]
        last_two_highs = swing_highs[-2:]

        # Higher Low: more recent swing low > previous swing low
        higher_low = last_two_lows[1][1] > last_two_lows[0][1]

        # Higher High: more recent swing high > previous swing high
        higher_high = last_two_highs[1][1] > last_two_highs[0][1]

        # Breakout: close above most recent swing high
        recent_swing_high = last_two_highs[-1][1]
        breakout = bars[i].close > recent_swing_high

        # Volume confirmation: above 110% of average
        avg_vol = sum(volumes[i-20:i]) / 20
        vol_ok = volumes[i] > avg_vol * 1.1

        # Momentum: bullish candle
        bullish_close = bars[i].close > bars[i].open

        # ENTRY: All conditions must be true
        if higher_low and higher_high and breakout and vol_ok and bullish_close:
            actions.append({
                'action': 'open_long',
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
                'size': 1.0,
                'stop_loss_pct': 4.5,  # Tight stop for bear survival
                'take_profit_pct': 9.0,  # 2:1 risk/reward
            })
    else:
        # === EXIT LOGIC ===
        pos = positions[key]
        bars_held = i - pos.entry_bar

        # Find recent swing lows for exit reference
        swing_lows = []
        for j in range(i-20, i-3):
            if j < 3:
                continue
            is_swing_low = all(bars[j].low <= bars[k].low
                              for k in range(j-3, j+4) if k != j and 0 <= k < len(bars))
            if is_swing_low:
                swing_lows.append(bars[j].low)

        recent_swing_low = min(swing_lows) if swing_lows else lows[i-10]

        # Exit conditions
        break_structure = bars[i].close < recent_swing_low
        regime_exit = not bull_regime
        time_exit = bars_held >= 20

        if break_structure or regime_exit or time_exit:
            actions.append({
                'action': 'close_long',
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
            })

    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)