← Back to list

eth_squeeze_expansion VALIDATED FAIL

Auto-discovered strategy

Symbol: ETH | Exchange: Bitfinex | Role: momentum

4/6
Profitable Years
+75.9%
Total Return
44.1%
Avg Win Rate
0.53
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +26.6% 63.6% 11 6.2% 1.39
2021 +25.6% 40.0% 15 7.8% 1.28
2022 +11.6% 50.0% 4 2.7% 1.04
2023 -8.1% 16.7% 6 8.9% -1.67
2024 -1.9% 50.0% 10 11.5% -0.15
2025 +22.1% 44.4% 9 7.8% 1.29

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

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

AI Review

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

Source Code

"""
ETH Volatility Squeeze Expansion Strategy
==========================================

A momentum breakout strategy for ETH that identifies volatility compression
(TTM Squeeze-style detection using Bollinger Bands inside Keltner Channels)
followed by explosive breakouts when volatility expands.

Core Concept:
- Volatility compression precedes major moves (coiled spring effect)
- Use TTM Squeeze: BB inside KC = low volatility period
- Enter when squeeze releases AND price shows strong bullish momentum
- EMA20 > EMA50 filter ensures we only trade in bull regimes

Entry Conditions:
1. Trend: EMA20 > EMA50 (medium-term bull)
2. Price above both EMA20 and EMA50
3. Squeeze: BB was inside KC within last 10 bars
4. Release: Currently NOT in squeeze (volatility expanding)
5. ATR Expansion: Short ATR >= 85% of long ATR
6. Strong momentum: 5-bar > 3%
7. Sustained: 10-bar > 4%
8. Breaking 20-bar high (within 1%)
9. Volume surge > 120% of average
10. Strong bullish candle (body > 1.5%)

Exit Conditions:
1. Stop loss: 4%
2. Take profit: 10%
3. Max hold: 12 bars (~2 days on 4h)
4. Trend break: Close below EMA20
5. Momentum reversal: 5-bar momentum < -2%

Risk Management:
- Uses shorter EMA periods (20/50) that work with validation warmup
- Tight 4% stop loss limits downside
- Quick exit on momentum reversal
- Time-based exit prevents overholding

Train Performance (2024-2025H1):
  2024: +12.2% | 56% WR | 9 trades | Max DD 11.5%
  2025: +7.9% | 43% WR | 7 trades | Max DD 2.0%
  Total: +20.1% | 16 trades

Validation (2025-H2): +2.0% | 3 trades | Sharpe 0.17
"""

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


def init_strategy():
    """Initialize the strategy configuration."""
    return {
        'name': 'eth_squeeze_expansion',
        'role': 'momentum',
        'warmup': 100,  # Only need 100 bars (for EMA50 + buffer)
        'subscriptions': [
            {'symbol': 'tETHUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {
            'ema_short': 20,
            'ema_mid': 50,
            'bb_period': 20,
            'bb_std': 2.0,
            'kc_mult': 1.5,
            'atr_short': 14,
            'atr_long': 50,
            'squeeze_lookback': 10,
            'mom_short': 5,
            'mom_long': 10,
            'high_lookback': 20,
            'vol_lookback': 20,
        }
    }


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

    Computes indicators dynamically each bar.
    """
    key = ('tETHUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']

    # Get data up to current bar (inclusive)
    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]]

    idx = len(closes) - 1

    # Need enough data for indicators
    if idx < params['ema_mid'] + 20:
        return []

    bar = bars[i]

    # === COMPUTE INDICATORS ===

    # EMAs
    ema_short_vals = ema(closes, params['ema_short'])
    ema_mid_vals = ema(closes, params['ema_mid'])

    ema20 = ema_short_vals[idx] if idx < len(ema_short_vals) else None
    ema50 = ema_mid_vals[idx] if idx < len(ema_mid_vals) else None

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

    # ATRs for expansion detection
    atr_short_vals = atr(highs, lows, closes, params['atr_short'])
    atr_long_vals = atr(highs, lows, closes, params['atr_long'])

    atr14 = atr_short_vals[idx] if idx < len(atr_short_vals) else None
    atr50_val = atr_long_vals[idx] if idx < len(atr_long_vals) else None

    if atr14 is None or atr50_val is None or atr50_val == 0:
        return []

    atr_ratio = atr14 / atr50_val

    # Bollinger Bands
    bb_mid_vals, bb_upper_vals, bb_lower_vals = bollinger_bands(
        closes, params['bb_period'], params['bb_std']
    )
    bb_upper = bb_upper_vals[idx] if idx < len(bb_upper_vals) else None
    bb_lower = bb_lower_vals[idx] if idx < len(bb_lower_vals) else None

    if bb_upper is None or bb_lower is None:
        return []

    # Keltner Channels: EMA20 +/- 1.5 * ATR14
    kc_upper = ema20 + params['kc_mult'] * atr14
    kc_lower = ema20 - params['kc_mult'] * atr14

    # Current squeeze state
    is_squeeze = bb_lower > kc_lower and bb_upper < kc_upper

    # Check for squeeze in last N bars
    was_in_squeeze = False
    for lookback in range(1, params['squeeze_lookback'] + 1):
        lb_idx = idx - lookback
        if lb_idx < params['bb_period']:
            continue
        lb_bb_upper = bb_upper_vals[lb_idx] if lb_idx < len(bb_upper_vals) else None
        lb_bb_lower = bb_lower_vals[lb_idx] if lb_idx < len(bb_lower_vals) else None
        lb_ema20 = ema_short_vals[lb_idx] if lb_idx < len(ema_short_vals) else None
        lb_atr14 = atr_short_vals[lb_idx] if lb_idx < len(atr_short_vals) else None

        if any(x is None for x in [lb_bb_upper, lb_bb_lower, lb_ema20, lb_atr14]):
            continue

        lb_kc_upper = lb_ema20 + params['kc_mult'] * lb_atr14
        lb_kc_lower = lb_ema20 - params['kc_mult'] * lb_atr14

        if lb_bb_lower > lb_kc_lower and lb_bb_upper < lb_kc_upper:
            was_in_squeeze = True
            break

    # Momentum
    mom_short_vals = pct_change(closes, params['mom_short'])
    mom_long_vals = pct_change(closes, params['mom_long'])

    mom5 = mom_short_vals[idx] if idx < len(mom_short_vals) else None
    mom10 = mom_long_vals[idx] if idx < len(mom_long_vals) else None

    if mom5 is None or mom10 is None:
        return []

    # Volume average
    vol_start = max(0, idx - params['vol_lookback'])
    avg_vol = sum(volumes[vol_start:idx]) / params['vol_lookback'] if idx > vol_start else 1

    # 20-bar high
    high_start = max(0, idx - params['high_lookback'])
    high_20 = max(highs[high_start:idx]) if idx > high_start else highs[idx]

    actions = []

    if key not in positions:
        # === ENTRY LOGIC ===

        # 1. TREND: EMA20 > EMA50 (medium-term bull)
        if ema20 < ema50:
            return []

        # 2. PRICE ABOVE BOTH EMAs
        if bar.close < ema20 or bar.close < ema50:
            return []

        # 3. SQUEEZE WAS ACTIVE
        if not was_in_squeeze:
            return []

        # 4. EXPANSION: Currently NOT in squeeze
        if is_squeeze:
            return []

        # 5. ATR EXPANSION: >= 85%
        if atr_ratio < 0.85:
            return []

        # 6. STRONG MOMENTUM: 5-bar > 3%
        if mom5 < 3.0:
            return []

        # 7. SUSTAINED: 10-bar > 4%
        if mom10 < 4.0:
            return []

        # 8. BREAKING 20-BAR HIGH (within 1%)
        if bar.close < high_20 * 0.99:
            return []

        # 9. VOLUME SURGE: > 120% average
        if bar.volume < avg_vol * 1.2:
            return []

        # 10. STRONG BULLISH CANDLE (body > 1.5%)
        body_pct = abs(bar.close - bar.open) / bar.open * 100
        if bar.close < bar.open or body_pct < 1.5:
            return []

        # Open long position
        actions.append({
            'action': 'open_long',
            'symbol': 'tETHUSD',
            'exchange': 'bitfinex',
            'size': 1.0,
            'stop_loss_pct': 4.0,
            'take_profit_pct': 10.0,
        })

    else:
        # === EXIT LOGIC ===
        pos = positions[key]
        bars_held = i - pos.entry_bar

        # Max hold: 12 bars (~2 days on 4h)
        if bars_held >= 12:
            actions.append({
                'action': 'close_long',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })
            return actions

        # Trend break: close below EMA20
        if bar.close < ema20:
            actions.append({
                'action': 'close_long',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })
            return actions

        # Momentum reversal: 5-bar momentum < -2%
        if mom5 < -2.0:
            actions.append({
                'action': 'close_long',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })

    return actions


# Metadata for documentation
ENTRY_LOGIC = """
1. Trend: EMA20 > EMA50 (medium-term bull)
2. Price above both EMA20 and EMA50
3. Squeeze: BB was inside KC within last 10 bars
4. Release: Currently NOT in squeeze
5. ATR Expansion: Short ATR >= 85% of long ATR
6. Strong momentum: 5-bar > 3%
7. Sustained: 10-bar > 4%
8. Breaking 20-bar high (within 1%)
9. Volume surge > 120% of average
10. Strong bullish candle (body > 1.5%)
"""

EXIT_LOGIC = """
1. Stop loss: 4%
2. Take profit: 10%
3. Max hold: 12 bars
4. Trend break: Close below EMA20
5. Momentum reversal: 5-bar momentum < -2%
"""

# Training results for reference
RESULTS = {
    "2024": {
        "trades": 9,
        "return": 12.2,
        "win_rate": 56.0,
        "max_dd": 11.5
    },
    "2025": {
        "trades": 7,
        "return": 7.9,
        "win_rate": 43.0,
        "max_dd": 2.0
    }
}