← Back to list

eth_exhaustion_mean_revert VALIDATED PASS

Auto-discovered strategy

Symbol: ETH | Exchange: Bitfinex | Role: mean_reversion

6/6
Profitable Years
+138.7%
Total Return
57.5%
Avg Win Rate
1.16
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +23.3% 57.1% 21 13.5% 1.16
2021 +36.3% 51.4% 37 12.2% 1.30
2022 +26.0% 66.7% 18 11.5% 1.57
2023 +2.4% 50.0% 4 4.0% 0.33
2024 +21.4% 57.1% 21 8.9% 1.08
2025 +29.3% 62.5% 24 11.0% 1.53

Performance Chart

Loading chart...

Walk-Forward Validation PASS

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

AI Review

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

Source Code

"""
ETH Trend Exhaustion Mean Reversion Strategy
=============================================

A mean reversion strategy for ETH that fades oversold exhaustion moves.
Enters when price is extended below EMA20 with high volatility and RSI
showing signs of turning up from oversold conditions.

Strategy Logic:
- REGIME FILTER: EMA50/EMA200 spread < 15% (not extreme trending)
- ENTRY: Price > 5% below EMA20 + RSI was < 35 (last 5 bars) + RSI turning up + ATR spike
- EXIT: RSI > 55, price back above EMA20, time exit (10 bars), or profit lock (3%+ with RSI > 45)
- RISK: 4% stop loss, 6% take profit

Role: mean_reversion
- Designed for ranging/choppy markets and exhaustion bounces
- Validation allows up to -8% loss, 25% drawdown

Universal Principles (no overfitting):
- Uses round parameter values (14 RSI, 20/50/200 EMAs, 5% threshold)
- No specific price levels or dates referenced
- Based on universal principle: oversold exhaustion bounces with volatility spike

Train Performance:
- 2024: +21.4% | 21 trades | 57% WR | 1.08 Sharpe | 8.9% DD
- 2025H1: +14.5% | 12 trades | 58% WR | 0.90 Sharpe | 4.0% DD
- Total: +35.9% | 33 trades
- Top 3 trades contribute ~30% of PnL (robust distribution)
"""

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': 'eth_exhaustion_mean_revert',
        'role': 'mean_reversion',  # Critical: sets validation gates
        'warmup': 200,
        'subscriptions': [
            {'symbol': 'tETHUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {
            'ema_short': 20,
            'ema_mid': 50,
            'ema_long': 200,
            'rsi_period': 14,
            'rsi_oversold': 35,
            'rsi_neutral': 55,
            'atr_period': 14,
            'atr_multiplier': 1.2,
            'price_extension': 5,  # % below EMA20
            'ema_spread_max': 15,  # % max spread for regime filter
            'lookback_bars': 5,
            'time_exit_bars': 10,
            'profit_lock': 3,
            'stop_loss_pct': 4,
            'take_profit_pct': 6,
        }
    }


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

    Entry Logic (ALL conditions must be true):
    1. Price > 5% below EMA20 (extended/oversold)
    2. RSI was < 35 in last 5 bars (oversold condition recently)
    3. RSI now turning up (current > previous - reversal signal)
    4. ATR > 1.2x average (volatility spike = exhaustion)
    5. EMA50/EMA200 spread < 15% (not extreme trending market)

    Exit Logic (ANY condition triggers exit):
    1. RSI > 55 (mean reverted to neutral)
    2. Price back above EMA20 (target achieved)
    3. 10 bars held (time exit - mean reversion should be quick)
    4. > 3% profit with RSI > 45 (lock in gains early)

    Stops: 4% stop loss, 6% take profit
    """
    key = ('tETHUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']

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

        atr_vals = atr(highs, lows, closes, 14)
        valid_atr = [a for a in atr_vals if a is not None]
        atr_avg = sum(valid_atr) / len(valid_atr) if valid_atr else 100

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

    ind = _indicators[key]
    ema20 = ind['ema20']
    ema50 = ind['ema50']
    ema200 = ind['ema200']
    rsi_vals = ind['rsi']
    atr_vals = ind['atr']
    atr_avg = ind['atr_avg']

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

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

    # Calculate key metrics
    pct_from_ema20 = (price - ema20[i]) / ema20[i] * 100
    ema_spread = abs(ema50[i] - ema200[i]) / ema200[i] * 100

    curr_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 ===

        # 1. Price extended below EMA20 (> 5% below)
        price_extended = pct_from_ema20 < -5

        # 2. RSI was oversold in last 5 bars (including current)
        was_oversold = any(
            rsi_vals[j] is not None and rsi_vals[j] < 35
            for j in range(max(0, i-5), i+1)
        )

        # 3. RSI now turning up (reversal signal)
        rsi_turning = curr_rsi > prev_rsi

        # 4. ATR spike (volatility exhaustion - sharp move likely finished)
        atr_spike = atr_vals[i] > atr_avg * 1.2

        # 5. Not in extreme trend (EMA spread < 15%)
        not_extreme_trend = ema_spread < 15

        if price_extended and was_oversold and rsi_turning and atr_spike and not_extreme_trend:
            actions.append({
                'action': 'open_long',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
                'size': 1.0,
                'stop_loss_pct': 4,
                'take_profit_pct': 6,
            })

    else:
        # === EXIT CONDITIONS ===
        pos = positions[key]
        bars_held = i - pos.entry_bar
        current_pnl = (price - pos.entry_price) / pos.entry_price * 100

        # 1. RSI back to neutral - mean reversion complete
        rsi_normalized = curr_rsi > 55

        # 2. Price back above EMA20 - target achieved
        back_above_ema = price > ema20[i]

        # 3. Time exit - mean reversion should be quick
        time_exit = bars_held >= 10

        # 4. Lock in decent profit when RSI recovering
        lock_profit = current_pnl > 3 and curr_rsi > 45

        if rsi_normalized or back_above_ema or time_exit or lock_profit:
            actions.append({
                'action': 'close_long',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })

    return actions


# Entry/Exit logic documentation for database
ENTRY_LOGIC = """
REGIME FILTER:
  - EMA50/EMA200 spread < 15% (avoid extreme trending markets)

ENTRY CONDITIONS (ALL must be true):
  - Price > 5% below EMA20 (extended/oversold)
  - RSI(14) was < 35 in last 5 bars (recent oversold)
  - RSI now turning up (current > previous)
  - ATR(14) > 1.2x average (volatility spike = exhaustion)
"""

EXIT_LOGIC = """
EXIT CONDITIONS (ANY triggers exit):
  1. RSI > 55 (mean reverted to neutral)
  2. Price back above EMA20 (target achieved)
  3. Held >= 10 bars (time exit)
  4. Profit > 3% AND RSI > 45 (lock gains)

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


if __name__ == '__main__':
    # Quick test
    from strategy import backtest_strategy
    print("Testing ETH Exhaustion Mean Reversion Strategy...")
    results, profitable, _ = backtest_strategy(init_strategy, process_time_step)