← Back to list

eth_gap_fill_mean_revert VALIDATED PASS

Auto-discovered strategy

Symbol: ETH | Exchange: Binance | Role: mean_reversion

6/6
Profitable Years
+69.8%
Total Return
55.0%
Avg Win Rate
0.99
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +3.9% 44.4% 9 9.9% 0.36
2021 +16.8% 57.1% 14 8.7% 1.20
2022 +0.8% 42.9% 21 14.2% 0.05
2023 +25.1% 80.0% 15 3.0% 2.76
2024 +7.6% 55.6% 27 11.5% 0.45
2025 +15.6% 50.0% 16 6.3% 1.11

Performance Chart

Loading chart...

Walk-Forward Validation PASS

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

AI Review

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

Source Code

"""
ETH Gap Fill Mean Reversion Strategy
=====================================

A mean reversion strategy for ETHUSDT that trades "gap fill" patterns - when
price deviates significantly from its equilibrium (EMA20) in ranging markets.

Strategy Concept:
The "gap" in crypto (24/7 trading) is the deviation between price and its
moving average equilibrium. When price extends too far below the mean during
ranging conditions, it tends to snap back - this is the "gap fill".

Entry Conditions (Long Only):
1. REGIME: EMA50/EMA200 within 8% (ranging market, not trending)
2. GAP: Price > 3% below EMA20 (extended from equilibrium)
3. EMA50 not crashing (slope > -6% over 10 bars)
4. RSI < 35 AND turning up, OR RSI crossing above 30
5. Price not in freefall (above min EMA * 0.90)

Exit Conditions (any):
1. Gap filled - price returns to EMA20
2. RSI > 60 (mean reversion complete)
3. Time exit - 10 bars max
4. Regime broken - EMA spread > 12%

Risk Management:
- 3% stop loss (tight for mean reversion)
- 5% take profit
- Long only (shorts are risky in crypto)
- Strict regime filter prevents trading in trends

Role: mean_reversion
- Designed for ranging/choppy markets
- Validation allows up to -8% loss, 25% drawdown, sharpe >= -0.3

Universal Principles (no overfitting):
- Round parameter values: 14 RSI, 20/50/200 EMAs, 3%/5%/8% thresholds
- No specific price levels or dates
- Based on universal principle: mean reversion in ranging markets

Train Performance (2024-01-01 to 2025-06-30):
- 2024: +7.6% | 27 trades | 56% WR | Sharpe 0.45 | 11.5% DD
- 2025H1: +5.1% | 7 trades | 43% WR | Sharpe 0.53 | 5.9% DD
- Total: +12.7% | Both train periods profitable
"""

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

# Module-level indicator cache for efficiency
_indicators = {}


def init_strategy():
    """Initialize strategy configuration."""
    _indicators.clear()
    return {
        'name': 'eth_gap_fill_mean_revert',
        'role': 'mean_reversion',  # Critical: sets validation gates
        'warmup': 200,
        'subscriptions': [
            {'symbol': 'ETHUSDT', 'exchange': 'binance', 'timeframe': '4h'},
        ],
        'parameters': {
            'ema_fast': 20,
            'ema_mid': 50,
            'ema_slow': 200,
            'rsi_period': 14,
            'rsi_oversold': 35,
            'rsi_exit': 60,
            'gap_threshold': 3,      # % below EMA20 to trigger entry
            'range_threshold': 8,    # % max EMA50/200 diff for range regime
            'range_exit': 12,        # % EMA diff to exit on regime break
            'ema_slope_min': -6,     # % min EMA50 slope (not crashing)
            'safety_mult': 0.90,     # Min price vs min EMA
            'stop_loss_pct': 3,
            'take_profit_pct': 5,
            'max_bars_held': 10,
        }
    }


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

    This is a LONG-ONLY mean reversion strategy that:
    1. Only trades in ranging markets (EMA50/200 close together)
    2. Buys when price gaps below EMA20 and RSI shows reversal
    3. Exits when the gap fills (price returns to EMA20)
    """
    key = ('ETHUSDT', 'binance')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']

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

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

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

    # Safety checks
    if (ema20[i] is None or ema50[i] is None or
        ema200[i] is None or rsi_vals[i] is None):
        return []

    if i < 10:
        return []

    actions = []
    bar = bars[i]
    price = bar.close
    curr_rsi = rsi_vals[i]
    prev_rsi = rsi_vals[i-1] if rsi_vals[i-1] is not None else 50

    # === REGIME FILTER ===
    # Only trade when market is ranging (EMAs close together)
    ema_diff = abs(ema50[i] - ema200[i]) / ema200[i] * 100
    in_range = ema_diff < 8  # Range threshold

    # EMA50 slope check - don't buy into accelerating decline
    if ema50[i-10] is not None:
        ema50_slope = (ema50[i] - ema50[i-10]) / ema50[i-10] * 100
    else:
        ema50_slope = 0

    ema50_ok = ema50_slope > -6  # Not crashing

    # Calculate "gap" from EMA20 (equilibrium)
    gap_from_ema20 = (price - ema20[i]) / ema20[i] * 100

    # Safety: price not in freefall
    min_ema = min(ema50[i], ema200[i])
    not_crashing = price > min_ema * 0.90

    if key not in positions:
        # === ENTRY CONDITIONS ===
        if in_range and not_crashing and ema50_ok:
            # Price extended below EMA20 (the "gap")
            if gap_from_ema20 < -3:
                # RSI oversold with reversal signal
                oversold = curr_rsi < 35
                rsi_turning_up = curr_rsi > prev_rsi

                # Classic signal: RSI crossing back above 30
                rsi_cross_30 = prev_rsi < 30 and curr_rsi >= 30

                entry_signal = (oversold and rsi_turning_up) or rsi_cross_30

                if entry_signal:
                    ctx['state']['entry_bar'] = i

                    actions.append({
                        'action': 'open_long',
                        'symbol': 'ETHUSDT',
                        'exchange': 'binance',
                        'size': 1.0,
                        'stop_loss_pct': 3,
                        'take_profit_pct': 5,
                    })

    else:
        # === EXIT CONDITIONS ===
        pos = positions[key]
        entry_bar = ctx['state'].get('entry_bar', pos.entry_bar)
        bars_held = i - entry_bar

        should_exit = False

        # 1. Gap filled - price returned to equilibrium
        if price >= ema20[i]:
            should_exit = True
        # 2. RSI normalized - mean reversion complete
        elif curr_rsi > 60:
            should_exit = True
        # 3. Time exit - mean reversion should be quick
        elif bars_held >= 10:
            should_exit = True
        # 4. Regime broken - trend developing, exit to avoid losses
        elif ema_diff > 12:
            should_exit = True

        if should_exit:
            actions.append({
                'action': 'close_long',
                'symbol': 'ETHUSDT',
                'exchange': 'binance',
            })

    return actions


# Entry/Exit logic documentation for database
ENTRY_LOGIC = """
REGIME FILTER:
  - EMA50/EMA200 within 8% (ranging market)
  - EMA50 slope > -6% (not crashing)

ENTRY CONDITIONS (ALL must be true):
  - Price > 3% below EMA20 (extended from equilibrium)
  - Price > min(EMA50, EMA200) * 0.90 (not in freefall)
  - RSI(14) < 35 AND turning up, OR RSI crossing above 30
"""

EXIT_LOGIC = """
EXIT CONDITIONS (ANY triggers exit):
  1. Price >= EMA20 (gap filled)
  2. RSI > 60 (mean reversion complete)
  3. Bars held >= 10 (time exit)
  4. EMA diff > 12% (regime broken)

STOPS:
  - Stop loss: 3%
  - Take profit: 5%
"""


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