← Back to list

rsi_chop_oversold_bounce VALIDATED PASS

Auto-discovered strategy

Symbol: BTC | Exchange: Bitfinex | Role: mean_reversion

6/6
Profitable Years
+91.9%
Total Return
77.2%
Avg Win Rate
1.55
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +10.9% 85.7% 14 4.0% 1.58
2021 +10.3% 81.8% 11 4.0% 1.26
2022 +13.8% 81.2% 32 6.0% 1.39
2023 +14.4% 77.3% 22 8.1% 1.17
2024 +13.5% 65.2% 23 6.6% 1.06
2025 +28.9% 72.0% 25 2.4% 2.87

Performance Chart

Loading chart...

Walk-Forward Validation PASS

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

AI Review

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

Source Code

"""
RSI Chop Oversold Bounce - Mean Reversion Strategy
===================================================

A mean reversion strategy that trades RSI oversold bounces ONLY during
choppy/ranging market conditions (when EMA50 and EMA200 are close together).

Strategy Logic:
- REGIME FILTER: Only trade when EMA50 and EMA200 are within 5% (chop regime)
- ENTRY: RSI < 35 and turning up, or RSI crossing back above 30
- EXIT: RSI > 65, regime breaks, price breakdown, or time-based exit
- RISK: 4% stop loss, 6% take profit

Role: mean_reversion
- Designed to profit in ranging/choppy markets
- Avoids trending markets where mean reversion fails
- Validation allows up to -8% loss, 25% drawdown

Universal Principles (no overfitting):
- Uses round parameter values (14 RSI, 50/200 EMAs, 5% threshold)
- No specific price levels or dates referenced
- Based on universal market principle: RSI mean reversion works in ranges
"""

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

# Module-level indicator cache
_indicators = {}


def init_strategy():
    """Initialize strategy configuration."""
    _indicators.clear()
    return {
        'name': 'rsi_chop_oversold_bounce',
        'role': 'mean_reversion',  # Critical: sets validation gates
        'warmup': 200,
        'subscriptions': [
            {'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {
            'rsi_period': 14,
            'rsi_oversold': 35,
            'rsi_overbought': 65,
            'ema_short': 50,
            'ema_long': 200,
            'chop_threshold': 5,  # % - max EMA difference for "chop" regime
            'stop_loss_pct': 4.0,
            'take_profit_pct': 6.0,
        }
    }


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

    Entry Logic:
    1. EMAs within 5% of each other (chop regime)
    2. Price not deeply below EMAs (not falling knife)
    3. RSI < 35 and turning up, OR RSI crossing above 30

    Exit Logic:
    1. RSI > 65 (overbought - mean reversion complete)
    2. EMA difference > 8% (regime broken - trend starting)
    3. Price breakdown below min(EMA50, EMA200) * 0.96
    4. Held > 25 bars and RSI > 50 (time exit)
    """
    key = ('tBTCUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']

    # Need enough bars for 200 EMA - now handled by framework via 'warmup' field
    # if i < 200:
    #     return []

    # Compute indicators once per backtest run (efficiency)
    if key not in _indicators:
        closes = [b.close for b in bars]
        highs = [b.high for b in bars]
        lows = [b.low for b in bars]

        _indicators[key] = {
            'ema50': ema(closes, 50),
            'ema200': ema(closes, 200),
            'rsi': rsi(closes, 14),
        }

    ind = _indicators[key]
    ema50 = ind['ema50']
    ema200 = ind['ema200']
    rsi_vals = ind['rsi']

    # Safety check for indicator availability
    if i >= len(ema50) or ema50[i] is None or ema200[i] is None or rsi_vals[i] is None:
        return []

    actions = []
    price = bars[i].close

    # === STRICT REGIME FILTER ===
    # ONLY trade during chop: EMAs must be within 5%
    ema_diff = abs(ema50[i] - ema200[i]) / ema200[i] * 100
    in_chop = ema_diff < 5

    # Don't catch a falling knife - reject if price far below both EMAs
    min_ema = min(ema50[i], ema200[i])
    deep_below = price < min_ema * 0.92

    current_rsi = rsi_vals[i]
    prev_rsi = rsi_vals[i-1] if rsi_vals[i-1] is not None else 50

    if key not in positions:
        # === ENTRY CONDITIONS ===
        # RSI oversold and turning up
        rsi_oversold = current_rsi < 35
        rsi_turning = current_rsi > prev_rsi

        # Classic signal: RSI crosses back above 30
        rsi_cross_30 = prev_rsi < 30 and current_rsi >= 30

        # Enter on RSI turn in oversold OR cross above 30
        if in_chop and not deep_below and ((rsi_oversold and rsi_turning) or rsi_cross_30):
            actions.append({
                'action': 'open_long',
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
                'size': 1.0,
                'stop_loss_pct': 4.0,
                'take_profit_pct': 6.0,
            })

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

        # 1. RSI overbought - mean reversion complete
        rsi_overbought = current_rsi > 65

        # 2. Regime broken - EMAs spreading apart (trend developing)
        regime_broken = ema_diff > 8

        # 3. Price breakdown - stop the bleeding
        breakdown = price < min_ema * 0.96

        # 4. Time-based exit - if held long and RSI normalized
        time_exit = bars_held >= 25 and current_rsi > 50

        if rsi_overbought or regime_broken or breakdown or time_exit:
            actions.append({
                'action': 'close_long',
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
            })

    return actions


# Entry/Exit logic documentation for database
ENTRY_LOGIC = """
REGIME: EMA50 and EMA200 within 5% (chop market)
ENTRY CONDITIONS:
  - RSI(14) < 35 AND RSI turning up (current > previous)
  - OR: RSI crossing above 30 from below
  - NOT: Price deeply below both EMAs (> 8% below)
"""

EXIT_LOGIC = """
EXIT CONDITIONS (any one triggers exit):
  1. RSI > 65 (overbought - mean reversion complete)
  2. EMA difference > 8% (trend developing - regime broken)
  3. Price < min(EMA50, EMA200) * 0.96 (breakdown)
  4. Held >= 25 bars AND RSI > 50 (time exit)

STOPS: 4% stop loss, 6% take profit
"""