← Back to list

sol_squeeze_momentum_breakout VALIDATED PASS

Auto-discovered strategy

Symbol: SOL | Exchange: Binance | Role: momentum

4/6
Profitable Years
+27.1%
Total Return
38.1%
Avg Win Rate
-331721441277383.56
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 -10.0% 0.0% 2 9.8% -1990328647664304.50
2021 -29.1% 25.0% 28 51.1% -0.89
2022 +5.7% 50.0% 2 4.3% 0.57
2023 +17.1% 40.0% 10 9.8% 0.79
2024 +22.8% 50.0% 14 7.3% 1.40
2025 +20.6% 63.6% 11 6.5% 1.52

Performance Chart

Loading chart...

Walk-Forward Validation PASS

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

AI Review

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

Source Code

"""
SOL Squeeze Momentum Breakout Strategy
=======================================

A momentum breakout strategy for SOLUSDT that identifies volatility compression
(using TTM Squeeze-style detection) followed by explosive breakouts.

Core Concept:
- Uses Bollinger Bands inside Keltner Channels to detect volatility squeeze
- When squeeze releases AND price breaks out with momentum, enter long
- EMA cascade filter ensures we only trade in bull markets
- Tight stops (5%) with 2:1 reward ratio

Entry Conditions:
1. Regime: EMA50 > EMA200 (bull market only)
2. Squeeze: BB was inside KC within last 10 bars (volatility compression)
3. Release: Currently NOT in squeeze (volatility expanding)
4. Breakout: Close above 20-bar high
5. Momentum: 5-bar > 2% AND 10-bar > 3%
6. EMA Cascade: Price > EMA20 > EMA50
7. Volume: Above 80% of 20-bar average
8. Green candle confirmation

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

Risk Management:
- Only trades in bull markets (EMA50 > EMA200)
- Tight 5% stop loss
- Time-based exit to avoid overholding
- Momentum-based exit for quick reversals

Train Performance (2024-2025H1):
  2024: +22.8% | 50% WR | 14 trades | Sharpe 1.40 | DD 7.3%
  2025: +8.6%  | 40% WR | 5 trades  | Sharpe 0.70 | DD 6.5%
  Total: +31.4% | 19 trades
"""

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': 'sol_squeeze_momentum_breakout',
        'role': 'momentum',
        'warmup': 100,  # Need 100 bars for indicator warmup
        'subscriptions': [
            {'symbol': 'SOLUSDT', 'exchange': 'binance', 'timeframe': '4h'},
        ],
        'parameters': {}
    }


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

    Uses TTM Squeeze-style detection:
    - Bollinger Bands (20, 2.0)
    - Keltner Channels (EMA20, 1.5 * ATR14)
    - Squeeze = BB inside KC
    """
    key = ('SOLUSDT', 'binance')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    state = ctx['state']

    # Initialize indicators on first call
    if 'initialized' not in state:
        closes = [b.close for b in bars]
        highs = [b.high for b in bars]
        lows = [b.low for b in bars]
        volumes = [b.volume for b in bars]

        # EMAs for trend filtering
        state['ema20'] = ema(closes, 20)
        state['ema50'] = ema(closes, 50)
        state['ema200'] = ema(closes, 200)

        # ATR for Keltner Channels
        state['atr14'] = atr(highs, lows, closes, 14)

        # Bollinger Bands (20, 2.0)
        bb_mid, bb_upper, bb_lower = bollinger_bands(closes, 20, 2.0)
        state['bb_upper'] = bb_upper
        state['bb_lower'] = bb_lower

        # Keltner Channels: EMA20 +/- 1.5 * ATR14
        kc_upper = []
        kc_lower = []
        for j in range(len(closes)):
            if state['ema20'][j] is not None and state['atr14'][j] is not None:
                kc_upper.append(state['ema20'][j] + 1.5 * state['atr14'][j])
                kc_lower.append(state['ema20'][j] - 1.5 * state['atr14'][j])
            else:
                kc_upper.append(None)
                kc_lower.append(None)
        state['kc_upper'] = kc_upper
        state['kc_lower'] = kc_lower

        # Squeeze condition: BB inside KC (low volatility)
        state['is_squeeze'] = []
        for j in range(len(closes)):
            if all(x is not None for x in [bb_lower[j], bb_upper[j], kc_lower[j], kc_upper[j]]):
                # Squeeze when BOTH BB bands are inside KC bands
                in_squeeze = bb_lower[j] > kc_lower[j] and bb_upper[j] < kc_upper[j]
                state['is_squeeze'].append(in_squeeze)
            else:
                state['is_squeeze'].append(False)

        # Volume SMA for confirmation
        state['vol_sma'] = sma(volumes, 20)

        # Momentum indicators
        state['mom5'] = pct_change(closes, 5)
        state['mom10'] = pct_change(closes, 10)

        state['initialized'] = True

    # Get current indicator values
    ema20 = state['ema20'][i] if i < len(state['ema20']) else None
    ema50 = state['ema50'][i] if i < len(state['ema50']) else None
    ema200 = state['ema200'][i] if i < len(state['ema200']) else None
    is_squeeze = state['is_squeeze'][i] if i < len(state['is_squeeze']) else False
    bb_upper = state['bb_upper'][i] if i < len(state['bb_upper']) else None
    vol_sma = state['vol_sma'][i] if i < len(state['vol_sma']) else None
    mom5 = state['mom5'][i] if i < len(state['mom5']) else None
    mom10 = state['mom10'][i] if i < len(state['mom10']) else None

    # Check for None values
    if any(x is None for x in [ema20, ema50, ema200, bb_upper, vol_sma, mom5, mom10]):
        return []

    bar = bars[i]
    highs = [b.high for b in bars]

    actions = []

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

        # 1. REGIME FILTER: EMA50 > EMA200 (bull market only)
        if ema50 < ema200:
            return []

        # 2. SQUEEZE WAS ACTIVE: Look for squeeze in last 10 bars
        was_in_squeeze = False
        for lookback in range(1, 11):
            if i - lookback >= 0 and state['is_squeeze'][i - lookback]:
                was_in_squeeze = True
                break

        if not was_in_squeeze:
            return []

        # 3. EXPANSION: Currently NOT in squeeze (released)
        if is_squeeze:
            return []

        # 4. BREAKOUT: Close above 20-bar high
        high_20 = max(highs[i-20:i]) if i >= 20 else None
        if high_20 is None or bar.close < high_20:
            return []

        # 5. MOMENTUM: 5-bar > 2% AND 10-bar > 3%
        if mom5 < 2.0 or mom10 < 3.0:
            return []

        # 6. EMA CASCADE: Price > EMA20 > EMA50
        if not (bar.close > ema20 and ema20 > ema50):
            return []

        # 7. VOLUME: Above 80% average
        if bar.volume < vol_sma * 0.8:
            return []

        # 8. GREEN CANDLE
        if bar.close < bar.open:
            return []

        # Open long position
        actions.append({
            'action': 'open_long',
            'symbol': 'SOLUSDT',
            'exchange': 'binance',
            'size': 1.0,
            'stop_loss_pct': 5.0,   # Tight stop
            'take_profit_pct': 10.0,  # 2:1 risk/reward
        })

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

        # Max hold: 12 bars (48 hours on 4h timeframe)
        if bars_held >= 12:
            actions.append({
                'action': 'close_long',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
            })
            return actions

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

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

    return actions


# Metadata for documentation
ENTRY_LOGIC = """
1. Regime: EMA50 > EMA200 (bull market only)
2. Squeeze: BB was inside KC within last 10 bars (volatility compression)
3. Release: Currently NOT in squeeze (volatility expanding)
4. Breakout: Close above 20-bar high
5. Momentum: 5-bar > 2% AND 10-bar > 3%
6. EMA Cascade: Price > EMA20 > EMA50
7. Volume: Above 80% of 20-bar average
8. Green candle confirmation
"""

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

# Training results for reference
RESULTS = {
    "2024": {
        "trades": 14,
        "return": 22.8,
        "sharpe": 1.40,
        "win_rate": 50.0,
        "max_dd": 7.3
    },
    "2025": {
        "trades": 5,
        "return": 8.6,
        "sharpe": 0.70,
        "win_rate": 40.0,
        "max_dd": 6.5
    }
}