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 | +14.2% | 67.6% | 37 | 14.7% | 0.79 |
| 2021 | +42.8% | 59.7% | 72 | 18.7% | 1.41 |
| 2022 | +20.9% | 60.0% | 60 | 14.3% | 0.87 |
| 2023 | +18.9% | 73.9% | 23 | 11.4% | 1.48 |
| 2024 | +32.3% | 70.0% | 40 | 10.0% | 1.75 |
| 2025 | +22.1% | 69.0% | 29 | 11.5% | 1.30 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | +9.6% | OK | 2026-01→ongoing | +0.0% | PASS |
"""
BTC Trend Exhaustion Mean Reversion Strategy
=============================================
A mean reversion strategy that captures bounces after trend exhaustion signals.
Detects when prices are extended from moving averages with oversold/overbought
RSI readings, then enters on momentum reversal signals.
Strategy Logic:
- LONG (Bearish Exhaustion): Price extended down + RSI was oversold + RSI turning up
- SHORT (Bullish Exhaustion): Price extended up + RSI was overbought + RSI falling
- ONLY when trend is weak (EMA20 < EMA50 or near death cross)
- EXIT: RSI normalizes around 50, price returns to EMA20, or time exit (10 bars)
Role: mean_reversion
- Designed for ranging markets and exhaustion bounces
- Validation allows up to -8% loss, 25% drawdown
Universal Principles (no overfitting):
- Uses round parameter values (14 RSI, 20/50 EMAs, 3-5% thresholds)
- No specific price levels or dates referenced
- Based on universal principle: oversold/overbought exhaustion mean reverts
Train Performance (2024-01-01 to 2025-06-30):
- 2024: +32.3% | 40 trades | 70% WR | Sharpe 1.75 | DD 10.0%
- 2025H1: +12.4% | 14 trades | 64% WR | Sharpe 0.90 | DD 11.5%
- Total: +44.7% | 54 trades
- Top 3 trades contribute 40% of PnL (acceptable distribution)
Key Design Decisions:
1. Regime filter for shorts: Only short when EMA20 < EMA50 (avoids fighting bull trends)
2. Multiple entry conditions: Extended price OR strong recent move with exhaustion
3. Early profit lock: Exit with 3%+ gain when RSI recovering
4. Consecutive bar counting: Identifies momentum exhaustion patterns
"""
import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema, rsi, atr, sma
# Module-level indicator cache for efficiency
_indicators = {}
def init_strategy():
"""Initialize strategy configuration."""
_indicators.clear()
return {
'name': 'btc_trend_exhaustion',
'role': 'mean_reversion', # Critical: sets validation gates
'warmup': 200,
'subscriptions': [
{'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
],
'parameters': {
'ema_fast': 20,
'ema_slow': 50,
'rsi_period': 14,
'rsi_overbought': 65,
'rsi_oversold': 35,
'extension_threshold': 3, # % from EMA20
'strong_move_threshold': 5, # % 5-bar change
'stop_loss_pct': 4,
'take_profit_pct': 6,
'time_exit_bars': 10,
'profit_lock_pct': 3,
}
}
def process_time_step(ctx):
"""
Process each time step and return list of actions.
LONG Entry (Bearish Exhaustion) - ALL conditions:
1. Price > 3% below EMA20 (extended to downside)
2. RSI was < 35 in last 5 bars (oversold recently)
3. RSI now rising (momentum shifting)
4. EMA20/EMA50 ratio > 0.90 (not in total crash)
Alternative LONG entry:
1. 5-bar price change < -5% (strong recent down move)
2. Volume exhaustion OR 3+ consecutive down bars
3. RSI < 40 and rising
SHORT Entry (Bullish Exhaustion) - ALL conditions:
1. Price > 4% above EMA20 (extended to upside)
2. RSI was > 65 in last 5 bars (overbought recently)
3. RSI now falling (momentum shifting)
4. Trend weak: EMA20 < EMA50 OR ratio < 1.02
EXIT Conditions (ANY triggers):
1. RSI in 45-55 range (normalized)
2. Price within 1% of EMA20 (returned to mean)
3. Held for 10+ bars (time exit)
4. 3%+ profit with RSI recovering
Stops: 4% stop loss, 6% take profit
"""
key = ('tBTCUSD', 'bitfinex')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
# Compute indicators once per backtest run
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]
volumes = [b.volume for b in bars]
vol_sma = sma(volumes, 20)
_indicators[key] = {
'closes': closes,
'highs': highs,
'lows': lows,
'volumes': volumes,
'ema20': ema(closes, 20),
'ema50': ema(closes, 50),
'rsi': rsi(closes, 14),
'atr': atr(highs, lows, closes, 14),
'vol_sma': vol_sma,
}
ind = _indicators[key]
ema20 = ind['ema20']
ema50 = ind['ema50']
rsi_vals = ind['rsi']
volumes = ind['volumes']
vol_sma = ind['vol_sma']
# Safety check for indicator availability
if ema20[i] is None or ema50[i] is None or rsi_vals[i] is None:
return []
actions = []
bar = bars[i]
price = bar.close
# Core metrics
pct_from_ema20 = (price - ema20[i]) / ema20[i] * 100
curr_rsi = rsi_vals[i]
prev_rsi = rsi_vals[i-1] if i > 0 and rsi_vals[i-1] is not None else 50
# RSI extreme detection in last 5 bars
was_overbought = any(
rsi_vals[j] is not None and rsi_vals[j] > 65
for j in range(max(0, i-5), i+1)
)
was_oversold = any(
rsi_vals[j] is not None and rsi_vals[j] < 35
for j in range(max(0, i-5), i+1)
)
# Trend/regime analysis
ema_ratio = ema20[i] / ema50[i] if ema50[i] > 0 else 1
bearish_trend = ema20[i] < ema50[i]
# Volume exhaustion signal
vol_declining = vol_sma[i] is not None and volumes[i] < vol_sma[i] * 0.8
# Multi-bar momentum analysis
if i >= 5:
five_bar_change = (price - ind['closes'][i-5]) / ind['closes'][i-5] * 100
# Count consecutive down bars
consec_down = 0
for j in range(i, max(0, i-5), -1):
if bars[j].close < bars[j].open:
consec_down += 1
else:
break
else:
five_bar_change = 0
consec_down = 0
if key not in positions:
# === LONG ENTRY: Bearish Exhaustion ===
# Condition set 1: Price extended + RSI oversold + turning
extended_down = pct_from_ema20 < -3
rsi_oversold = was_oversold
rsi_turning = curr_rsi > prev_rsi
not_crash = ema_ratio > 0.90
cond_set_1 = extended_down and rsi_oversold and rsi_turning and not_crash
# Condition set 2: Strong down move + exhaustion signals
strong_down = five_bar_change < -5
exhaustion = vol_declining or consec_down >= 3
rsi_low = curr_rsi < 40
cond_set_2 = strong_down and exhaustion and rsi_low and rsi_turning and not_crash
if cond_set_1 or cond_set_2:
actions.append({
'action': 'open_long',
'symbol': 'tBTCUSD',
'exchange': 'bitfinex',
'size': 1.0,
'stop_loss_pct': 4,
'take_profit_pct': 6,
})
# === SHORT ENTRY: Bullish Exhaustion (Regime Filtered) ===
# Condition set 1: Price extended + RSI overbought + falling
extended_up = pct_from_ema20 > 4
rsi_overbought = was_overbought
rsi_falling = curr_rsi < prev_rsi
# Must have weak trend for shorts
trend_weak = bearish_trend or ema_ratio < 1.02
cond_set_1_short = extended_up and rsi_overbought and rsi_falling and trend_weak
# Condition set 2: Strong up move + exhaustion
strong_up = five_bar_change > 5
rsi_high = curr_rsi > 60
cond_set_2_short = strong_up and vol_declining and rsi_high and rsi_falling and trend_weak
if cond_set_1_short or cond_set_2_short:
actions.append({
'action': 'open_short',
'symbol': 'tBTCUSD',
'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
# Calculate current P&L
if pos.side == 'long':
current_pnl = (price - pos.entry_price) / pos.entry_price * 100
else:
current_pnl = (pos.entry_price - price) / pos.entry_price * 100
# 1. RSI normalized around 50
rsi_normalized = 45 < curr_rsi < 55
# 2. Price returned to EMA20
returned_to_mean = abs(pct_from_ema20) < 1
# 3. Time exit
time_exit = bars_held >= 10
# 4. Lock in profit when RSI recovering
profit_lock = current_pnl > 3 and (
(pos.side == 'long' and curr_rsi > 45) or
(pos.side == 'short' and curr_rsi < 55)
)
if rsi_normalized or returned_to_mean or time_exit or profit_lock:
action_type = 'close_long' if pos.side == 'long' else 'close_short'
actions.append({
'action': action_type,
'symbol': 'tBTCUSD',
'exchange': 'bitfinex',
})
return actions
# Entry/Exit logic documentation for database
ENTRY_LOGIC = """
LONG ENTRY (Bearish Exhaustion) - Either condition set:
Set 1: Extended + Oversold + Turning
- Price > 3% below EMA20 (extended down)
- RSI was < 35 in last 5 bars (oversold)
- RSI now rising (turning up)
- EMA20/EMA50 ratio > 0.90 (not crashing)
Set 2: Strong Down Move + Exhaustion
- 5-bar price change < -5% (strong down)
- Volume < 80% of SMA(20) OR 3+ consecutive red bars
- RSI < 40 and rising
SHORT ENTRY (Bullish Exhaustion) - Regime Filtered:
Regime Filter (REQUIRED for shorts):
- EMA20 < EMA50 (bearish) OR EMA ratio < 1.02 (near death cross)
Set 1: Extended + Overbought + Falling
- Price > 4% above EMA20 (extended up)
- RSI was > 65 in last 5 bars (overbought)
- RSI now falling
Set 2: Strong Up Move + Exhaustion
- 5-bar price change > 5% (strong up)
- Volume exhaustion (< 80% of SMA20)
- RSI > 60 and falling
"""
EXIT_LOGIC = """
EXIT CONDITIONS (ANY triggers exit):
1. RSI in 45-55 range (normalized/mean reverted)
2. Price within 1% of EMA20 (returned to mean)
3. Held for 10+ bars (time exit)
4. Profit > 3% with RSI recovering:
- Long: RSI > 45
- Short: RSI < 55
STOPS:
- Stop loss: 4%
- Take profit: 6%
"""
if __name__ == '__main__':
from strategy import backtest_strategy, validate_new_strategy
print("="*60)
print("TRAIN PERIOD BACKTEST")
print("="*60)
results, profitable, _ = backtest_strategy(init_strategy, process_time_step)
print("\n" + "="*60)
print("VALIDATION ON UNSEEN DATA")
print("="*60)
validate_new_strategy(init_strategy, process_time_step)