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