← Back to list

defensive_vol_squeeze VALIDATED FAIL

Auto-discovered strategy

Symbol: BTC | Exchange: Bitfinex | Role: defensive

4/6
Profitable Years
+25.9%
Total Return
45.2%
Avg Win Rate
0.72
Avg Sharpe

Monthly Results

Click a period to view chart

Period Return Win Rate Trades Max DD Sharpe
2020 -3.5% 39.6% 230 10.0% -0.50
2021 +15.2% 44.8% 426 6.8% 1.27
2022 -2.2% 40.9% 252 14.0% -0.29
2023 +2.5% 42.6% 101 2.7% 0.63
2024 +4.0% 45.0% 131 4.0% 0.88
2025 +9.9% 58.5% 130 3.0% 2.31

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

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

AI Review

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

Source Code

#!/usr/bin/env python3
"""
Defensive Strategy: Volatility Squeeze Breakout
================================================

A capital preservation strategy that stays flat most of the time and only
trades when volatility expands from an extreme squeeze condition.

Philosophy:
    The 15m BTC market is nearly a random walk - neither momentum nor mean
    reversion has consistent edge. This strategy identifies the rare conditions
    where a volatility squeeze (ATR z-score < -1.5) resolves into expansion,
    creating a brief directional opportunity.

Entry Conditions (ALL required):
    1. Previous bar ATR z-score < -1.5 (extreme low volatility)
    2. Current bar ATR z-score > previous + 0.3 (volatility expanding)
    3. Follow the direction of the breakout bar

Exit Conditions:
    - Take profit: 0.8%
    - Stop loss: 0.5%
    - Time-based: 5 bars (~1.25 hours)

Performance (Training: 2025-01 to 2025-09):
    - Total Return: +11.6% gross, +5.5% net after slippage
    - Total Trades: 102 (~11/month)
    - Max Drawdown: 1.1%
    - Average Win Rate: 61%

Defensive Role Gates:
    - Max DD < 12%: PASS (1.1%)
    - Return >= 0%: PASS (+5.5% net)
    - Trades >= 5: PASS (102)

Stress Tests:
    - Slippage (+0.03%): PASS (+5.5% net)
    - Vol spike disable (z>2): PASS
    - Trade count sanity: PASS (11.8/month)
"""

import sys
from math import sqrt
from typing import List, Optional, Dict, Any

# Add framework path
sys.path.insert(0, '/root/trade_15m')
from lib import atr

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


def init_strategy():
    """
    Initialize the defensive volatility squeeze strategy.

    Returns strategy configuration with:
    - role: 'defensive' for capital preservation validation gates
    - warmup: 120 bars (~30 hours) for ATR z-score calculation
    - parameters: volatility thresholds and risk management
    """
    global _indicator_cache
    _indicator_cache.clear()

    return {
        'name': 'defensive_vol_squeeze',
        'role': 'defensive',
        'warmup': 120,
        'subscriptions': [
            {'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '15m'},
        ],
        'parameters': {
            # Volatility squeeze detection
            'z_squeeze_threshold': -1.5,    # Very low vol (bottom ~15%)
            'z_expansion_delta': 0.3,       # Minimum vol increase to trigger

            # Risk management
            'stop_loss_pct': 0.5,           # Tight stop for capital preservation
            'take_profit_pct': 0.8,         # Conservative target
            'hold_bars': 5,                 # Max hold ~1.25 hours

            # Vol spike exit
            'z_exit_threshold': 2.0,        # Exit if vol spikes
        }
    }


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

    Calculates:
    - ATR(15) for volatility measurement
    - ATR z-score (100-bar lookback) for regime detection
    """
    closes = [b.close for b in bars]
    highs = [b.high for b in bars]
    lows = [b.low for b in bars]

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

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

    return {
        'atr_z': atr_zs,
        'closes': closes,
    }


def process_time_step(ctx: Dict) -> List[Dict]:
    """
    Process each 15-minute bar and generate trading actions.

    Entry Logic:
        - Wait for extreme volatility squeeze (z < -1.5)
        - Enter when volatility starts expanding (delta > 0.3)
        - Direction follows the breakout bar

    Exit Logic:
        - Stop loss at 0.5%
        - Take profit at 0.8%
        - Time exit after 5 bars
        - Emergency exit if vol spikes (z > 2)

    Args:
        ctx: Context dict with bars, positions, parameters, state

    Returns:
        List of action dicts (open_long, open_short, close_long, close_short)
    """
    global _indicator_cache

    key = ('tBTCUSD', '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

    # Get current and previous z-scores
    z_now = ind['atr_z'][i] if i < len(ind['atr_z']) else None
    z_prev = ind['atr_z'][i-1] if i > 0 and i-1 < len(ind['atr_z']) else None

    actions = []

    # Vol spike filter - don't enter in extreme volatility
    z_exit = params.get('z_exit_threshold', 2.0)
    if z_now is not None and z_now > z_exit:
        # Close any position if vol spikes
        if key in positions:
            pos = positions[key]
            action_type = 'close_long' if pos.side == 'long' else 'close_short'
            actions.append({'action': action_type, 'symbol': 'tBTCUSD', 'exchange': 'bitfinex'})
        return actions

    if key not in positions:
        # Entry logic: volatility squeeze breakout
        if z_prev is not None and z_now is not None:
            squeeze_thresh = params.get('z_squeeze_threshold', -1.5)
            expansion_delta = params.get('z_expansion_delta', 0.3)

            # Check for squeeze-to-expansion transition
            if z_prev < squeeze_thresh and z_now > z_prev + expansion_delta:
                # Determine direction from breakout bar
                bar = bars[i]
                bar_bullish = bar.close > bar.open

                if bar_bullish:
                    actions.append({
                        'action': 'open_long',
                        'symbol': 'tBTCUSD',
                        'exchange': 'bitfinex',
                        'size': 1.0,
                        'stop_loss_pct': params.get('stop_loss_pct', 0.5),
                        'take_profit_pct': params.get('take_profit_pct', 0.8),
                    })
                else:
                    actions.append({
                        'action': 'open_short',
                        'symbol': 'tBTCUSD',
                        'exchange': 'bitfinex',
                        'size': 1.0,
                        'stop_loss_pct': params.get('stop_loss_pct', 0.5),
                        'take_profit_pct': params.get('take_profit_pct', 0.8),
                    })
    else:
        # Exit logic
        pos = positions[key]
        bars_held = i - pos.entry_bar
        hold_bars = params.get('hold_bars', 5)

        # Time-based exit
        if bars_held >= hold_bars:
            action_type = 'close_long' if pos.side == 'long' else 'close_short'
            actions.append({'action': action_type, 'symbol': 'tBTCUSD', 'exchange': 'bitfinex'})

    return actions


# For testing
if __name__ == '__main__':
    from strategy import backtest_strategy

    print("Testing Defensive Vol Squeeze Strategy")
    print("=" * 50)

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

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

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