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