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 | +7.3% | 71.4% | 7 | 3.2% | 1.33 |
| 2021 | -0.5% | 50.0% | 8 | 4.0% | -0.08 |
| 2022 | -21.1% | 34.6% | 26 | 20.8% | -1.60 |
| 2023 | +5.5% | 90.0% | 10 | 1.2% | 2.24 |
| 2024 | -9.7% | 53.3% | 15 | 16.1% | -1.04 |
| 2025 | +3.7% | 60.9% | 23 | 9.6% | 0.44 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | -6.7% | FAIL | 2026-01→ongoing | +0.0% | FAIL |
Not yet reviewed. Run: ./review_strategy.sh stoch_range_divergence_fade
"""
Stochastic Range Divergence Fade - Mean Reversion Strategy
==========================================================
A conservative mean reversion strategy that trades stochastic divergences
ONLY during ranging market conditions with multiple confirmation signals.
Strategy Logic:
- REGIME: EMA50 within 6% of EMA200 (range), EMA50 not far below EMA200
- ENTRY: Stochastic bullish divergence (price makes lower low, stoch makes higher low)
+ K < 30 (oversold) + K turning up
- EXIT: Stochastic > 65, time exit, or breakdown
- RISK: 4% stop loss, 7% take profit
Role: mean_reversion
- Designed to profit in ranging/choppy markets
- Conservative filter avoids trending markets
- Validation allows up to -8% loss, 25% drawdown
Universal Principles (no overfitting):
- Uses round parameter values (14-period Stochastic, 50/200 EMAs)
- No specific price levels or dates referenced
- Based on universal principle: divergences signal exhaustion
"""
import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema
# Module-level indicator cache for efficiency
_indicators = {}
def stochastic(highs, lows, closes, k_period=14, d_period=3):
"""
Calculate Stochastic Oscillator (%K and %D).
%K = 100 * (Close - Lowest Low) / (Highest High - Lowest Low)
%D = SMA(%K, d_period)
"""
n = len(closes)
stoch_k = [None] * n
for i in range(k_period - 1, n):
highest_high = max(highs[i-k_period+1:i+1])
lowest_low = min(lows[i-k_period+1:i+1])
if highest_high - lowest_low > 0:
stoch_k[i] = 100 * (closes[i] - lowest_low) / (highest_high - lowest_low)
else:
stoch_k[i] = 50
stoch_d = [None] * n
for i in range(k_period - 1 + d_period - 1, n):
k_vals = [stoch_k[j] for j in range(i-d_period+1, i+1) if stoch_k[j] is not None]
if len(k_vals) == d_period:
stoch_d[i] = sum(k_vals) / d_period
return stoch_k, stoch_d
def init_strategy():
"""Initialize strategy configuration."""
_indicators.clear()
return {
'name': 'stoch_range_divergence_fade',
'role': 'mean_reversion',
'warmup': 210,
'subscriptions': [
{'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
],
'parameters': {
'stoch_k_period': 14,
'stoch_d_period': 3,
'stoch_oversold': 30,
'stoch_overbought': 65,
'ema_short': 50,
'ema_long': 200,
'range_threshold': 6,
'divergence_lookback': 10,
'stop_loss_pct': 4.0,
'take_profit_pct': 7.0,
}
}
def process_time_step(ctx):
"""
Process each time step and return list of actions.
Entry Logic:
1. EMA50 within 6% of EMA200 (range regime)
2. EMA50 not far below EMA200 (not crashing)
3. Price at 10-bar low (or within 1%)
4. Stochastic bullish divergence (K higher than 10 bars ago despite lower price)
5. Stochastic K < 30 (oversold)
6. K turning up
Exit Logic:
1. Stochastic > 65 (mean reversion complete)
2. Held >= 20 bars and K > 50 (time exit)
3. Price breakdown below EMA200 * 0.94
"""
key = ('tBTCUSD', 'bitfinex')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
params = ctx['parameters']
# Need enough bars for EMA200 + divergence lookback
if i < 210:
return []
# Compute indicators once per backtest run
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]
stoch_k, stoch_d = stochastic(
highs, lows, closes,
k_period=params['stoch_k_period'],
d_period=params['stoch_d_period']
)
_indicators[key] = {
'ema50': ema(closes, params['ema_short']),
'ema200': ema(closes, params['ema_long']),
'stoch_k': stoch_k,
'stoch_d': stoch_d,
}
ind = _indicators[key]
ema50 = ind['ema50']
ema200 = ind['ema200']
stoch_k = ind['stoch_k']
stoch_d = ind['stoch_d']
# Safety check for indicator availability
if ema50[i] is None or ema200[i] is None:
return []
if stoch_k[i] is None:
return []
lookback = params['divergence_lookback']
if i < lookback + 1 or stoch_k[i-1] is None or stoch_k[i-lookback] is None:
return []
actions = []
price = bars[i].close
low = bars[i].low
k_val = stoch_k[i]
prev_k = stoch_k[i-1]
k_lookback_ago = stoch_k[i-lookback]
# === REGIME FILTER ===
ema_diff = abs(ema50[i] - ema200[i]) / ema200[i] * 100
in_range = ema_diff < params['range_threshold']
# Not in strong downtrend
not_crash = ema50[i] > ema200[i] * 0.90
# Find lookback-bar low
lows_lookback = [bars[j].low for j in range(i-lookback, i)]
lowest_lookback = min(lows_lookback)
# Price at recent low
at_low = low <= lowest_lookback * 1.01
# Stochastic bullish divergence: K higher than lookback ago despite lower price
bullish_divergence = k_val > k_lookback_ago + 3
# Oversold + turning
is_oversold = k_val < params['stoch_oversold']
k_turning_up = k_val > prev_k
if key not in positions:
# === ENTRY CONDITIONS ===
entry_signal = (
in_range and
not_crash and
at_low and
bullish_divergence and
is_oversold and
k_turning_up
)
if entry_signal:
actions.append({
'action': 'open_long',
'symbol': 'tBTCUSD',
'exchange': 'bitfinex',
'size': 1.0,
'stop_loss_pct': params['stop_loss_pct'],
'take_profit_pct': params['take_profit_pct'],
})
else:
# === EXIT CONDITIONS ===
pos = positions[key]
bars_held = i - pos.entry_bar
# Overbought - mean reversion complete
is_overbought = k_val > params['stoch_overbought']
# Time exit
time_exit = bars_held >= 20 and k_val > 50
# Breakdown
breakdown = price < ema200[i] * 0.94
if is_overbought or time_exit or breakdown:
actions.append({
'action': 'close_long',
'symbol': 'tBTCUSD',
'exchange': 'bitfinex',
})
return actions
# Entry/Exit logic documentation
ENTRY_LOGIC = """
REGIME FILTER:
- EMA50 within 6% of EMA200 (ranging market)
- EMA50 > EMA200 * 0.90 (not crashing)
ENTRY SIGNAL:
- Price at 10-bar low (or within 1%)
- Stochastic bullish divergence (K higher than 10 bars ago)
- Stochastic %K < 30 (oversold)
- %K turning up (reversal starting)
"""
EXIT_LOGIC = """
EXIT when ANY:
1. Stochastic %K > 65 (mean reversion complete)
2. Held >= 20 bars AND %K > 50 (time exit)
3. Price < EMA200 * 0.94 (breakdown)
STOPS: 4% stop loss, 7% take profit
"""
if __name__ == '__main__':
print("\n" + "="*60)
print("STOCHASTIC RANGE DIVERGENCE FADE - BACKTEST")
print("="*60)
from strategy import backtest_strategy
results, profitable, _ = backtest_strategy(
init_strategy,
process_time_step,
verbose=True
)
print("\n--- Detailed Results ---")
for year, metrics in results.items():
print(f" {year}: Return={metrics['return']:+.1f}% | Sharpe={metrics['sharpe']:.2f} | MaxDD={metrics['max_dd']:.1f}% | Trades={metrics['trades']} | WR={metrics['win_rate']:.0f}%")