← Back to list

stoch_range_divergence_fade VALIDATED FAIL

Auto-discovered strategy

Symbol: BTC | Exchange: Bitfinex | Role: mean_reversion

3/6
Profitable Years
-14.8%
Total Return
60.0%
Avg Win Rate
0.21
Avg Sharpe

Year-by-Year Results

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

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

0/1 Windows Profitable
-6.7% OOS Return
0.00 Median Sharpe
0.000 Score
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

AI Review

Not yet reviewed. Run: ./review_strategy.sh stoch_range_divergence_fade

Source Code

"""
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}%")