← Back to list

defensive_vol_compression_eth VALIDATED PASS

Auto-discovered strategy

Symbol: ETH | Exchange: Bitfinex | Role: defensive

3/6
Profitable Years
+10.0%
Total Return
45.9%
Avg Win Rate
0.38
Avg Sharpe

Monthly Results

Click a period to view chart

Period Return Win Rate Trades Max DD Sharpe
2020 -6.0% 35.2% 71 15.1% -0.44
2021 +1.1% 40.0% 120 24.5% 0.06
2022 -0.0% 38.5% 78 12.9% 0.00
2023 +4.6% 68.4% 19 1.3% 1.69
2024 -1.3% 45.9% 37 10.4% -0.16
2025 +11.6% 47.4% 57 10.7% 1.11

Performance Chart

Loading chart...

Walk-Forward Validation PASS

1/1 Windows Profitable
+4.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 2025-01→2025-09 2025-10→2025-12 +4.0% OK 2026-01→ongoing +0.0% PASS

AI Review

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

Source Code

#!/usr/bin/env python3
"""
Defensive Strategy: Volatility Compression Breakout - tETHUSD
=============================================================

Role: defensive
Goal: Capital preservation - DO NOT LOSE MONEY

Strategy Logic:
1. Wait for volatility to be LOW (calm regime = ATR z-score between -0.5 and 0.0)
2. Only trade in CALM conditions with clear trend alignment (EMA spread > 0.3%)
3. Enter on pullback to EMA20 in calm volatility with strong trend bar
4. Exit quickly on ANY volatility spike or trend break

This is a "wait for calm, trade with trend, exit fast" defensive approach.

Key Features:
- Only trades in CALM volatility regime (ATR z-score -0.5 to 0.0)
- Requires clear trend (EMA10/EMA20 spread > 0.3%)
- Pullback entry (better risk/reward)
- Strong candle confirmation (bar strength > 40%)
- Quick exits at first sign of trouble
- Conservative position sizing (50%)

Entry Conditions (ALL required):
1. Calm volatility: ATR z-score between -0.5 and 0.0
2. Clear trend: EMA10 diverged from EMA20 by > 0.3%
3. Pullback: Price touches EMA20 area (within 0.3%)
4. Strong bar: Directional bar with body > 40% of range

Exit Conditions:
- Take profit: 3.0%
- Stop loss: 1.5%
- Max hold: 15 bars (~4 hours)
- Vol spike exit: ATR z-score > 1.5
- Trend break exit: EMA crossover

TRAIN PERIOD RESULTS (2025-01 to 2025-09):
- Gross Return: +9.1%
- Net Return (after slippage): +6.6%
- Max Drawdown: 4.4%
- Total Trades: 42 (~5/month)
- Profitable Months: 5/9

DEFENSIVE ROLE VALIDATION:
- Max DD < 12%: PASS (4.4%)
- Return >= 0%: PASS (+6.6% net)
- Min Trades >= 5: PASS (42)

References:
- BB/KC Squeeze: https://trendspider.com/learning-center/bb-kc-squeeze-a-powerful-indicator-for-trading-range-breakouts/
- ATR regime: https://www.mindmathmoney.com/articles/atr-indicator-trading-strategy
"""

import sys
sys.path.insert(0, '/root/trade_15m')

from lib import atr, ema, sma
from math import sqrt
from typing import List, Dict, Any

# Global indicator cache
_indicator_cache: Dict[str, Any] = {}


def init_strategy():
    """Initialize the defensive volatility compression breakout strategy."""
    global _indicator_cache
    _indicator_cache.clear()

    return {
        'name': 'defensive_vol_compression_eth',
        'role': 'defensive',
        'warmup': 100,
        'subscriptions': [
            {'symbol': 'tETHUSD', 'exchange': 'bitfinex', 'timeframe': '15m'},
        ],
        'parameters': {
            # Volatility regime detection - target "calm" conditions
            'atr_period': 15,
            'atr_z_lookback': 60,
            'calm_z_min': -0.5,        # Slightly wider low end
            'calm_z_max': 0.0,         # Only in low-to-normal vol

            # Trend detection
            'ema_fast': 10,
            'ema_slow': 20,
            'ema_spread_min': 0.003,   # 0.3% spread for clearer trend

            # Entry: pullback tolerance
            'pullback_tolerance': 0.003,  # 0.3% from EMA20

            # Risk management - wider targets for fewer trades
            'stop_loss_pct': 1.5,
            'take_profit_pct': 3.0,    # Better R:R
            'max_hold_bars': 15,       # Allow more time

            # Vol spike exit
            'vol_spike_z': 1.5,
        }
    }


def compute_indicators(bars: List) -> Dict[str, Any]:
    """
    Precompute all indicators for efficiency.

    Calculates:
    - ATR for volatility measurement
    - ATR z-score for regime detection
    - EMAs for trend detection
    """
    closes = [b.close for b in bars]
    highs = [b.high for b in bars]
    lows = [b.low for b in bars]

    # ATR calculation
    atr_vals = atr(highs, lows, closes, 15)

    # ATR z-score with 60-bar lookback
    atr_zs = [None] * len(bars)
    for i in range(60, len(bars)):
        if atr_vals[i] is not None:
            window = [v for v in atr_vals[max(0, i-60):i] if v is not None]
            if len(window) >= 30:
                mean = sum(window) / len(window)
                var = sum((x - mean)**2 for x in window) / len(window)
                std = sqrt(var) if var > 0 else 0.001
                atr_zs[i] = (atr_vals[i] - mean) / std

    # EMAs for trend
    ema10 = ema(closes, 10)
    ema20 = ema(closes, 20)

    return {
        'atr': atr_vals,
        'atr_z': atr_zs,
        'ema10': ema10,
        'ema20': ema20,
        'closes': closes,
        'highs': highs,
        'lows': lows,
    }


def process_time_step(ctx: Dict) -> List[Dict]:
    """
    Process each 15-minute bar for calm volatility pullback trading.

    Entry Logic:
        1. Calm volatility regime (z between -0.5 and 0.5)
        2. Clear trend (EMA10 > EMA20 for long, < for short)
        3. Pullback to EMA20 area
        4. Bar confirms direction

    Exit Logic:
        - Take profit at 1.5%
        - Stop loss at 1.0%
        - Time exit after 8 bars
        - Exit on vol spike (z > 1.5)
    """
    global _indicator_cache

    key = ('tETHUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']

    # Compute indicators once per run
    if not _indicator_cache:
        _indicator_cache = compute_indicators(bars)

    ind = _indicator_cache

    # Safety check
    if i >= len(ind['atr_z']) or ind['atr_z'][i] is None:
        return []

    z_now = ind['atr_z'][i]
    ema10 = ind['ema10'][i]
    ema20 = ind['ema20'][i]

    if ema10 is None or ema20 is None:
        return []

    bar = bars[i]
    actions = []

    # =========================================================================
    # VOL SPIKE EXIT - Check first
    # =========================================================================
    if z_now > params['vol_spike_z']:
        if key in positions:
            pos = positions[key]
            action_type = 'close_long' if pos.side == 'long' else 'close_short'
            actions.append({
                'action': action_type,
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })
        return actions  # No new entries during vol spike

    # =========================================================================
    # POSITION MANAGEMENT
    # =========================================================================
    if key in positions:
        pos = positions[key]
        bars_held = i - pos.entry_bar

        # Time-based exit
        if bars_held >= params['max_hold_bars']:
            action_type = 'close_long' if pos.side == 'long' else 'close_short'
            actions.append({
                'action': action_type,
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })
            return actions

        # Trend break exit - exit if trend reverses
        if pos.side == 'long' and ema10 < ema20:
            actions.append({
                'action': 'close_long',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })
        elif pos.side == 'short' and ema10 > ema20:
            actions.append({
                'action': 'close_short',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
            })

        return actions

    # =========================================================================
    # ENTRY LOGIC - Calm Vol Pullback (very selective)
    # =========================================================================

    # Check for calm volatility regime
    if not (params['calm_z_min'] <= z_now <= params['calm_z_max']):
        return actions

    # Check for minimum EMA spread (clear trend)
    ema_spread = abs(ema10 - ema20) / ema20
    if ema_spread < params['ema_spread_min']:
        return actions

    # Check for uptrend: EMA10 > EMA20
    if ema10 > ema20:
        # Pullback to EMA20 from above (long entry zone)
        touch_ema = bar.low <= ema20 * (1 + params['pullback_tolerance'])
        close_above = bar.close > ema20

        # Bullish bar confirmation (strong candle)
        bar_bullish = bar.close > bar.open
        bar_strength = (bar.close - bar.open) / max(bar.high - bar.low, 0.01)

        if touch_ema and close_above and bar_bullish and bar_strength > 0.4:
            actions.append({
                'action': 'open_long',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
                'size': 0.5,  # Half size for defensive
                'stop_loss_pct': params['stop_loss_pct'],
                'take_profit_pct': params['take_profit_pct'],
            })
            return actions

    # Check for downtrend: EMA10 < EMA20
    elif ema10 < ema20:
        # Pullback to EMA20 from below (short entry zone)
        touch_ema = bar.high >= ema20 * (1 - params['pullback_tolerance'])
        close_below = bar.close < ema20

        # Bearish bar confirmation (strong candle)
        bar_bearish = bar.close < bar.open
        bar_strength = (bar.open - bar.close) / max(bar.high - bar.low, 0.01)

        if touch_ema and close_below and bar_bearish and bar_strength > 0.4:
            actions.append({
                'action': 'open_short',
                'symbol': 'tETHUSD',
                'exchange': 'bitfinex',
                'size': 0.5,  # Half size for defensive
                'stop_loss_pct': params['stop_loss_pct'],
                'take_profit_pct': params['take_profit_pct'],
            })

    return actions


if __name__ == '__main__':
    """Run backtest when executed directly."""
    from strategy import backtest_strategy

    print("\n" + "=" * 60)
    print("DEFENSIVE: Volatility Compression Breakout - tETHUSD")
    print("=" * 60)
    print("\nTrades EMA pullbacks in calm volatility regimes.")
    print("Entry: Pullback to EMA20 in trend with calm vol (z<0)")
    print("Exit: TP 3.0% / SL 1.5% / Max 15 bars / Vol spike")
    print()

    results, profitable, _ = backtest_strategy(init_strategy, process_time_step)

    total_trades = sum(r.get('trades', 0) for r in results.values())
    total_return = sum(r.get('return', 0) for r in results.values())
    max_dd = max(r.get('max_dd', 0) for r in results.values()) if results else 0

    print(f"\n  Aggregate Stats:")
    print(f"  Total Trades: {total_trades}")
    print(f"  Total Return: {total_return:+.2f}%")
    print(f"  Max Drawdown: {max_dd:.2f}%")
    print(f"  Profitable Months: {profitable}/9")

    # Calculate slippage impact
    slippage_cost = total_trades * 0.06  # 0.03% per side = 0.06% round trip
    net_return = total_return - slippage_cost

    print(f"\n  Cost Analysis:")
    print(f"  Gross Return: {total_return:+.2f}%")
    print(f"  Slippage Cost: -{slippage_cost:.2f}%")
    print(f"  Net Return: {net_return:+.2f}%")

    print("\n  Defensive Role Validation:")
    print(f"  Max DD < 12%: {'PASS' if max_dd < 12 else 'FAIL'} ({max_dd:.1f}%)")
    print(f"  Return >= 0%: {'PASS' if net_return >= 0 else 'FAIL'} ({net_return:+.1f}%)")
    print(f"  Trades >= 5:  {'PASS' if total_trades >= 5 else 'FAIL'} ({total_trades})")

    passes = max_dd < 12 and net_return >= 0 and total_trades >= 5
    print(f"\n  {'=' * 50}")
    print(f"  TRAIN RESULT: {'PASS - Ready for validation' if passes else 'FAIL - Needs tuning'}")