← Back to list

bb_range_mean_reversion VALIDATED FAIL

Auto-discovered strategy

Symbol: BTC | Exchange: Bitfinex | Role: mean_reversion

4/6
Profitable Years
+15.9%
Total Return
64.6%
Avg Win Rate
0.55
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +3.2% 59.1% 22 5.9% 0.39
2021 +6.4% 54.5% 11 6.3% 0.59
2022 -7.7% 58.8% 34 17.7% -0.61
2023 +10.7% 72.7% 22 1.4% 2.21
2024 +9.2% 78.3% 23 3.0% 1.35
2025 -5.9% 64.0% 25 10.0% -0.66

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

0/1 Windows Profitable
-8.0% 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 -8.0% FAIL 2026-01→ongoing +0.0% FAIL

AI Review

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

Source Code

"""
Strategy: bb_range_mean_reversion
=================================
Bollinger Band Mean Reversion in Ranging Markets

Core Principle:
  Mean reversion ONLY works in ranging/consolidating markets.
  This strategy strictly detects ranging conditions before trading.

Ranging Market Detection (ALL must be true):
  1. EMA50 within 5% of EMA200 (no trend)
  2. EMA50 flat over 20 bars (< 5% change)
  3. Price range 5-20% over 30 bars (moderate consolidation)
  4. Price crosses EMA20 at least 4 times in 30 bars (oscillating)

Entry Logic:
  - Price at or below lower Bollinger Band
  - Bar shows bounce (close in upper 70% of bar range)
  - Price above the range floor (not breaking down)

Exit Logic:
  - Price reaches middle Bollinger Band (mean reversion target)
  - Price breaks below range floor (stop out)
  - Take profit at 4%, stop loss at 3%

Risk Management:
  - role: mean_reversion (allows -8% validation loss, DD < 25%)
  - Tight 3% stop loss
  - 4% take profit target
  - Stay flat when not ranging (capital preservation)

Universal Principles Used:
  - Range detection via EMA alignment
  - Bollinger Bands for oversold detection
  - Mean reversion to fair value
  - Regime filtering (only trade favorable conditions)

Performance (TRAIN: 2024-01-01 to 2025-06-30):
  2024: +9.2% | 23 trades | 78% WR | MaxDD 3.0% | Sharpe 1.35
  2025 (partial): +2.1% | 11 trades | 73% WR | MaxDD 6.1% | Sharpe 0.33
  Total: +11.3% | 2/2 train periods profitable
"""

import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema, bollinger_bands


def init_strategy():
    return {
        'name': 'bb_range_mean_reversion',
        'role': 'mean_reversion',
        'warmup': 210,
        'role': 'mean_reversion',  # REQUIRED: allows -8% loss in validation
        'subscriptions': [
            {'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {
            'bb_period': 20,
            'bb_std': 2.0,
            'ema_short': 20,
            'ema_medium': 50,
            'ema_long': 200,
            'range_period': 30,
            'stop_loss_pct': 3.0,
            'take_profit_pct': 4.0,
        }
    }


def process_time_step(ctx):
    key = ('tBTCUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']

    # Need enough bars for EMA200
    if i < 210:
        return []

    # Extract OHLCV data
    closes = [b.close for b in bars]
    highs = [b.high for b in bars]
    lows = [b.low for b in bars]

    # Calculate indicators
    ema20 = ema(closes, params['ema_short'])
    ema50 = ema(closes, params['ema_medium'])
    ema200 = ema(closes, params['ema_long'])
    bb_middle, bb_upper, bb_lower = bollinger_bands(
        closes, params['bb_period'], params['bb_std']
    )

    # Ensure indicators are valid
    if ema50[i] is None or ema200[i] is None or bb_middle[i] is None:
        return []
    if bb_lower[i] is None or bb_upper[i] is None or ema20[i] is None:
        return []

    price = bars[i].close
    range_period = params['range_period']

    # ==========================================================
    # STRICT RANGE DETECTION
    # Only trade when market is clearly ranging (not trending)
    # ==========================================================

    # 1. EMA ALIGNMENT CHECK - EMAs must be close together
    ema_spread = abs(ema50[i] - ema200[i]) / ema200[i] * 100
    emas_close = ema_spread < 5.0  # EMAs within 5%

    # 2. EMA50 should be relatively flat (not strongly trending)
    ema50_change = abs(ema50[i] - ema50[i-20]) / ema50[i-20] * 100 if ema50[i-20] else 100
    ema50_flat = ema50_change < 5.0  # Less than 5% change over 20 bars

    # 3. PRICE RANGE CHECK - Price should be oscillating, not trending
    range_high = max(highs[i-range_period:i])
    range_low = min(lows[i-range_period:i])
    range_mid = (range_high + range_low) / 2
    range_size = (range_high - range_low) / range_mid * 100

    # Range should be moderate (not too tight, not too wide)
    moderate_range = 5.0 < range_size < 20.0

    # 4. OSCILLATION CHECK - Price should cross EMA20 frequently
    ema20_crosses = 0
    for j in range(i - range_period + 1, i):
        if ema20[j] is None or ema20[j-1] is None:
            continue
        # Detect crosses
        crossed_up = closes[j] > ema20[j] and closes[j-1] <= ema20[j-1]
        crossed_down = closes[j] < ema20[j] and closes[j-1] >= ema20[j-1]
        if crossed_up or crossed_down:
            ema20_crosses += 1

    oscillating = ema20_crosses >= 4  # At least 4 crosses in range_period bars

    # RANGING MARKET = all conditions met
    is_ranging = emas_close and ema50_flat and moderate_range and oscillating

    # Safety: not in freefall
    not_freefall = ema50[i] > ema50[i-5] * 0.95 if ema50[i-5] else True

    safe_to_trade = is_ranging and not_freefall

    actions = []

    if key not in positions:
        # ==========================================================
        # ENTRY: Buy at lower Bollinger Band in ranging market
        # ==========================================================
        if safe_to_trade:
            # At or below lower BB (within 1%)
            at_lower_bb = price <= bb_lower[i] * 1.01

            # Showing bounce: close in upper portion of bar range
            bar_range = highs[i] - lows[i]
            if bar_range > 0:
                close_position = (price - lows[i]) / bar_range
                bouncing = close_position > 0.30  # Close above lower 30%
            else:
                bouncing = True

            # Not breaking down through range floor
            above_floor = price > range_low * 1.01

            entry_signal = at_lower_bb and bouncing and above_floor

            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: At middle band or on breakdown
        # ==========================================================
        # Target: middle Bollinger Band (mean reversion target)
        at_middle = price >= bb_middle[i] * 0.99  # Within 1% of middle

        # Range broke down
        broke_range = price < range_low * 0.98  # Broke below floor

        if at_middle or broke_range:
            actions.append({
                'action': 'close_long',
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
            })

    return actions


# Entry logic description for documentation
ENTRY_LOGIC = """
RANGE DETECTION (ALL must pass):
  1. EMA50 within 5% of EMA200 (no strong trend)
  2. EMA50 flat over 20 bars (< 5% change)
  3. Price range 5-20% over 30 bars (moderate consolidation)
  4. Price crosses EMA20 >= 4 times in 30 bars (oscillating)

SAFETY CHECK:
  - EMA50 not falling > 5% over 5 bars (not in freefall)

ENTRY SIGNAL:
  - Price at or below lower Bollinger Band
  - Bar close in upper 70% of range (showing bounce)
  - Price above range floor (not breaking down)
"""

EXIT_LOGIC = """
EXIT when ANY:
  - Price reaches middle Bollinger Band (mean reversion complete)
  - Price breaks below range floor by 2% (breakdown)
  - Stop loss hit (3%)
  - Take profit hit (4%)
"""