← Back to list

sol_rsi_confirmed_bounce VALIDATED FAIL

Auto-discovered strategy

Symbol: SOL | Exchange: Binance | Role: mean_reversion

4/6
Profitable Years
-12.7%
Total Return
48.0%
Avg Win Rate
-0.12
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +1.0% 100.0% 1 0.0% 0.00
2021 -8.4% 27.3% 11 15.8% -0.58
2022 -19.4% 20.0% 20 22.5% -1.35
2023 +7.9% 45.5% 22 12.8% 0.54
2024 +3.4% 52.2% 23 12.3% 0.21
2025 +2.8% 42.9% 7 5.1% 0.47

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

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

AI Review

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

Source Code

"""
SOL RSI Confirmed Bounce - Mean Reversion Strategy
===================================================

Mean reversion strategy for SOLUSDT that trades RSI oversold bounces
in ranging/choppy market conditions.

Strategy Logic:
- REGIME FILTER: Only trade when market is ranging (not trending)
  - 20-bar range < 30%
  - Bollinger Band width < 20%
  - Not in clear downtrend (EMA20 >= EMA50 * 0.95)

- ENTRY: Confirmed RSI bounce
  - RSI(14) dipped below 35 in last 3 bars
  - RSI now recovering above 40 and rising
  - Price making higher low (not new lows)
  - Current bar is bullish
  - Price below middle Bollinger Band (room to run)

- EXIT: Mean reversion targets
  - RSI > 65 (overbought)
  - Price at/above upper Bollinger Band
  - Above middle BB with > 3% profit
  - Time stop after 10 bars
  - RSI turning down while still low

- RISK: 4% stop loss, 8% take profit (2:1 R/R)

Role: mean_reversion
Train Performance: +4.8% total (2024: +3.4%, 2025H1: +1.3%)
Metrics: 27 trades, 51.9% win rate, 12.3% max DD, 0.28 Sharpe
"""

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


def init_strategy():
    return {
        'name': 'sol_rsi_confirmed_bounce',
        'role': 'momentum',
        'warmup': 60,
        'role': 'mean_reversion',
        'subscriptions': [
            {'symbol': 'SOLUSDT', 'exchange': 'binance', 'timeframe': '4h'},
        ],
        'parameters': {
            'rsi_period': 14,
            'rsi_oversold': 35,
            'rsi_recovery': 40,
            'rsi_overbought': 65,
            'bb_period': 20,
            'bb_std': 2,
            'ema_fast': 20,
            'ema_slow': 50,
            'stop_loss_pct': 4,
            'take_profit_pct': 8,
            'max_range_pct': 30,
            'max_bb_width': 20,
        }
    }


def process_time_step(ctx):
    key = ('SOLUSDT', 'binance')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']

    if i < 60:
        return []

    curr = bars[i]
    prev = bars[i-1]

    # Calculate indicators over lookback window
    closes = [bars[j].close for j in range(max(0, i-120), i+1)]
    highs = [bars[j].high for j in range(max(0, i-120), i+1)]
    lows = [bars[j].low for j in range(max(0, i-120), i+1)]

    # RSI (14 period)
    rsi_vals = rsi(closes, 14)
    if rsi_vals[-1] is None or len(rsi_vals) < 5:
        return []
    curr_rsi = rsi_vals[-1]
    prev_rsi = rsi_vals[-2]
    prev2_rsi = rsi_vals[-3]
    prev3_rsi = rsi_vals[-4]

    # EMAs for trend detection
    ema20_vals = ema(closes, 20)
    ema50_vals = ema(closes, 50)

    if ema20_vals[-1] is None or ema50_vals[-1] is None:
        return []

    ema20 = ema20_vals[-1]
    ema50 = ema50_vals[-1]

    # Bollinger Bands for range and targets
    bb_mid, bb_upper, bb_lower = bollinger_bands(closes, 20, 2.0)
    if bb_upper[-1] is None or bb_lower[-1] is None:
        return []

    bb_width = (bb_upper[-1] - bb_lower[-1]) / bb_mid[-1] * 100

    # ========== REGIME FILTER: RANGING (NOT STRONGLY TRENDING) ==========

    # Calculate 20-bar price range
    last_20_high = max(bars[j].high for j in range(i-20, i+1))
    last_20_low = min(bars[j].low for j in range(i-20, i+1))
    range_20 = (last_20_high - last_20_low) / last_20_low * 100

    # Regime conditions
    moderate_range = range_20 < 30  # Not trending hard
    normal_bb = bb_width < 20  # Volatility not extreme
    is_ranging = moderate_range and normal_bb

    # HARD FILTER: No trading in clear downtrend
    clear_downtrend = ema20 < ema50 * 0.95

    actions = []

    if key not in positions:
        # Skip if in downtrend
        if clear_downtrend:
            return []

        # Skip if not in ranging conditions
        if not is_ranging:
            return []

        # ========== ENTRY: CONFIRMED RSI BOUNCE ==========
        # Wait for RSI V-shape: oversold then confirmed recovery

        # RSI dipped below 35 in last 3 bars
        was_oversold = min(prev_rsi or 100, prev2_rsi or 100, prev3_rsi or 100) < 35

        # RSI now clearly recovering above 40 and rising
        confirmed_recovery = curr_rsi > 40 and curr_rsi > prev_rsi

        # Price making higher low (not breaking down)
        recent_swing_low = min(bars[j].low for j in range(i-5, i))
        higher_low = curr.low >= recent_swing_low

        # Current bar is bullish
        bullish = curr.close > curr.open

        # Still below middle BB (room to run to target)
        room_to_run = curr.close < bb_mid[-1]

        # Entry conditions met
        if was_oversold and confirmed_recovery and higher_low and bullish and room_to_run:
            actions.append({
                'action': 'open_long',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
                'size': 1.0,
                'stop_loss_pct': 4,
                'take_profit_pct': 8,
            })
    else:
        # ========== EXIT CONDITIONS ==========
        pos = positions[key]
        bars_held = i - pos.entry_bar
        current_pnl = (curr.close - pos.entry_price) / pos.entry_price * 100

        # 1. RSI overbought - mean reverted successfully
        if curr_rsi > 65:
            actions.append({'action': 'close_long', 'symbol': 'SOLUSDT', 'exchange': 'binance'})

        # 2. Upper Bollinger Band reached - target hit
        elif curr.close >= bb_upper[-1]:
            actions.append({'action': 'close_long', 'symbol': 'SOLUSDT', 'exchange': 'binance'})

        # 3. Above middle BB with decent profit - partial target
        elif curr.close > bb_mid[-1] and current_pnl > 3:
            actions.append({'action': 'close_long', 'symbol': 'SOLUSDT', 'exchange': 'binance'})

        # 4. Time stop - mean reversion should be quick
        elif bars_held >= 10:
            actions.append({'action': 'close_long', 'symbol': 'SOLUSDT', 'exchange': 'binance'})

        # 5. RSI turning down while still in lower half - bounce failing
        elif curr_rsi < prev_rsi and curr_rsi < 45 and bars_held >= 2:
            actions.append({'action': 'close_long', 'symbol': 'SOLUSDT', 'exchange': 'binance'})

    return actions


# Entry/Exit logic for documentation
ENTRY_LOGIC = """
REGIME FILTER (must pass ALL):
- 20-bar price range < 30% (not trending)
- Bollinger Band width < 20% (low volatility)
- EMA20 >= EMA50 * 0.95 (not in downtrend)

ENTRY (must pass ALL):
- RSI(14) was below 35 in last 3 bars
- RSI now > 40 AND rising
- Price making higher low (last 5 bars)
- Current bar is bullish (close > open)
- Price below middle Bollinger Band
"""

EXIT_LOGIC = """
EXIT when ANY:
- RSI > 65 (overbought - mean reverted)
- Price >= upper Bollinger Band
- Price > middle BB AND profit > 3%
- Held >= 10 bars (time stop)
- RSI dropping AND < 45 AND held >= 2 bars (bounce failing)

STOPS:
- Stop loss: 4%
- Take profit: 8%
"""