Auto-discovered strategy
Symbol: ETH | Exchange: Binance | Role: mean_reversion
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 |
| 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 |
Not yet reviewed. Run: ./review_strategy.sh eth_gap_fill_mean_revert
"""
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)