Auto-discovered strategy
Symbol: BTC | Exchange: Bitfinex | Role: defensive
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 |
| 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 |
Not yet reviewed. Run: ./review_strategy.sh defensive_vol_squeeze
#!/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")