Auto-discovered strategy
Symbol: ETH | Exchange: Bitfinex | Role: momentum
Click a year to view chart
| Year | Return | Win Rate | Trades | Max DD | Sharpe |
|---|---|---|---|---|---|
| 2020 | +26.6% | 63.6% | 11 | 6.2% | 1.39 |
| 2021 | +25.6% | 40.0% | 15 | 7.8% | 1.28 |
| 2022 | +11.6% | 50.0% | 4 | 2.7% | 1.04 |
| 2023 | -8.1% | 16.7% | 6 | 8.9% | -1.67 |
| 2024 | -1.9% | 50.0% | 10 | 11.5% | -0.15 |
| 2025 | +22.1% | 44.4% | 9 | 7.8% | 1.29 |
| 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 |
Not yet reviewed. Run: ./review_strategy.sh eth_squeeze_expansion
"""
ETH Volatility Squeeze Expansion Strategy
==========================================
A momentum breakout strategy for ETH that identifies volatility compression
(TTM Squeeze-style detection using Bollinger Bands inside Keltner Channels)
followed by explosive breakouts when volatility expands.
Core Concept:
- Volatility compression precedes major moves (coiled spring effect)
- Use TTM Squeeze: BB inside KC = low volatility period
- Enter when squeeze releases AND price shows strong bullish momentum
- EMA20 > EMA50 filter ensures we only trade in bull regimes
Entry Conditions:
1. Trend: EMA20 > EMA50 (medium-term bull)
2. Price above both EMA20 and EMA50
3. Squeeze: BB was inside KC within last 10 bars
4. Release: Currently NOT in squeeze (volatility expanding)
5. ATR Expansion: Short ATR >= 85% of long ATR
6. Strong momentum: 5-bar > 3%
7. Sustained: 10-bar > 4%
8. Breaking 20-bar high (within 1%)
9. Volume surge > 120% of average
10. Strong bullish candle (body > 1.5%)
Exit Conditions:
1. Stop loss: 4%
2. Take profit: 10%
3. Max hold: 12 bars (~2 days on 4h)
4. Trend break: Close below EMA20
5. Momentum reversal: 5-bar momentum < -2%
Risk Management:
- Uses shorter EMA periods (20/50) that work with validation warmup
- Tight 4% stop loss limits downside
- Quick exit on momentum reversal
- Time-based exit prevents overholding
Train Performance (2024-2025H1):
2024: +12.2% | 56% WR | 9 trades | Max DD 11.5%
2025: +7.9% | 43% WR | 7 trades | Max DD 2.0%
Total: +20.1% | 16 trades
Validation (2025-H2): +2.0% | 3 trades | Sharpe 0.17
"""
import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema, sma, atr, bollinger_bands, pct_change
def init_strategy():
"""Initialize the strategy configuration."""
return {
'name': 'eth_squeeze_expansion',
'role': 'momentum',
'warmup': 100, # Only need 100 bars (for EMA50 + buffer)
'subscriptions': [
{'symbol': 'tETHUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
],
'parameters': {
'ema_short': 20,
'ema_mid': 50,
'bb_period': 20,
'bb_std': 2.0,
'kc_mult': 1.5,
'atr_short': 14,
'atr_long': 50,
'squeeze_lookback': 10,
'mom_short': 5,
'mom_long': 10,
'high_lookback': 20,
'vol_lookback': 20,
}
}
def process_time_step(ctx):
"""
Process each time step and return trade actions.
Computes indicators dynamically each bar.
"""
key = ('tETHUSD', 'bitfinex')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
params = ctx['parameters']
# Get data up to current bar (inclusive)
closes = [b.close for b in bars[:i+1]]
highs = [b.high for b in bars[:i+1]]
lows = [b.low for b in bars[:i+1]]
volumes = [b.volume for b in bars[:i+1]]
idx = len(closes) - 1
# Need enough data for indicators
if idx < params['ema_mid'] + 20:
return []
bar = bars[i]
# === COMPUTE INDICATORS ===
# EMAs
ema_short_vals = ema(closes, params['ema_short'])
ema_mid_vals = ema(closes, params['ema_mid'])
ema20 = ema_short_vals[idx] if idx < len(ema_short_vals) else None
ema50 = ema_mid_vals[idx] if idx < len(ema_mid_vals) else None
if ema20 is None or ema50 is None:
return []
# ATRs for expansion detection
atr_short_vals = atr(highs, lows, closes, params['atr_short'])
atr_long_vals = atr(highs, lows, closes, params['atr_long'])
atr14 = atr_short_vals[idx] if idx < len(atr_short_vals) else None
atr50_val = atr_long_vals[idx] if idx < len(atr_long_vals) else None
if atr14 is None or atr50_val is None or atr50_val == 0:
return []
atr_ratio = atr14 / atr50_val
# Bollinger Bands
bb_mid_vals, bb_upper_vals, bb_lower_vals = bollinger_bands(
closes, params['bb_period'], params['bb_std']
)
bb_upper = bb_upper_vals[idx] if idx < len(bb_upper_vals) else None
bb_lower = bb_lower_vals[idx] if idx < len(bb_lower_vals) else None
if bb_upper is None or bb_lower is None:
return []
# Keltner Channels: EMA20 +/- 1.5 * ATR14
kc_upper = ema20 + params['kc_mult'] * atr14
kc_lower = ema20 - params['kc_mult'] * atr14
# Current squeeze state
is_squeeze = bb_lower > kc_lower and bb_upper < kc_upper
# Check for squeeze in last N bars
was_in_squeeze = False
for lookback in range(1, params['squeeze_lookback'] + 1):
lb_idx = idx - lookback
if lb_idx < params['bb_period']:
continue
lb_bb_upper = bb_upper_vals[lb_idx] if lb_idx < len(bb_upper_vals) else None
lb_bb_lower = bb_lower_vals[lb_idx] if lb_idx < len(bb_lower_vals) else None
lb_ema20 = ema_short_vals[lb_idx] if lb_idx < len(ema_short_vals) else None
lb_atr14 = atr_short_vals[lb_idx] if lb_idx < len(atr_short_vals) else None
if any(x is None for x in [lb_bb_upper, lb_bb_lower, lb_ema20, lb_atr14]):
continue
lb_kc_upper = lb_ema20 + params['kc_mult'] * lb_atr14
lb_kc_lower = lb_ema20 - params['kc_mult'] * lb_atr14
if lb_bb_lower > lb_kc_lower and lb_bb_upper < lb_kc_upper:
was_in_squeeze = True
break
# Momentum
mom_short_vals = pct_change(closes, params['mom_short'])
mom_long_vals = pct_change(closes, params['mom_long'])
mom5 = mom_short_vals[idx] if idx < len(mom_short_vals) else None
mom10 = mom_long_vals[idx] if idx < len(mom_long_vals) else None
if mom5 is None or mom10 is None:
return []
# Volume average
vol_start = max(0, idx - params['vol_lookback'])
avg_vol = sum(volumes[vol_start:idx]) / params['vol_lookback'] if idx > vol_start else 1
# 20-bar high
high_start = max(0, idx - params['high_lookback'])
high_20 = max(highs[high_start:idx]) if idx > high_start else highs[idx]
actions = []
if key not in positions:
# === ENTRY LOGIC ===
# 1. TREND: EMA20 > EMA50 (medium-term bull)
if ema20 < ema50:
return []
# 2. PRICE ABOVE BOTH EMAs
if bar.close < ema20 or bar.close < ema50:
return []
# 3. SQUEEZE WAS ACTIVE
if not was_in_squeeze:
return []
# 4. EXPANSION: Currently NOT in squeeze
if is_squeeze:
return []
# 5. ATR EXPANSION: >= 85%
if atr_ratio < 0.85:
return []
# 6. STRONG MOMENTUM: 5-bar > 3%
if mom5 < 3.0:
return []
# 7. SUSTAINED: 10-bar > 4%
if mom10 < 4.0:
return []
# 8. BREAKING 20-BAR HIGH (within 1%)
if bar.close < high_20 * 0.99:
return []
# 9. VOLUME SURGE: > 120% average
if bar.volume < avg_vol * 1.2:
return []
# 10. STRONG BULLISH CANDLE (body > 1.5%)
body_pct = abs(bar.close - bar.open) / bar.open * 100
if bar.close < bar.open or body_pct < 1.5:
return []
# Open long position
actions.append({
'action': 'open_long',
'symbol': 'tETHUSD',
'exchange': 'bitfinex',
'size': 1.0,
'stop_loss_pct': 4.0,
'take_profit_pct': 10.0,
})
else:
# === EXIT LOGIC ===
pos = positions[key]
bars_held = i - pos.entry_bar
# Max hold: 12 bars (~2 days on 4h)
if bars_held >= 12:
actions.append({
'action': 'close_long',
'symbol': 'tETHUSD',
'exchange': 'bitfinex',
})
return actions
# Trend break: close below EMA20
if bar.close < ema20:
actions.append({
'action': 'close_long',
'symbol': 'tETHUSD',
'exchange': 'bitfinex',
})
return actions
# Momentum reversal: 5-bar momentum < -2%
if mom5 < -2.0:
actions.append({
'action': 'close_long',
'symbol': 'tETHUSD',
'exchange': 'bitfinex',
})
return actions
# Metadata for documentation
ENTRY_LOGIC = """
1. Trend: EMA20 > EMA50 (medium-term bull)
2. Price above both EMA20 and EMA50
3. Squeeze: BB was inside KC within last 10 bars
4. Release: Currently NOT in squeeze
5. ATR Expansion: Short ATR >= 85% of long ATR
6. Strong momentum: 5-bar > 3%
7. Sustained: 10-bar > 4%
8. Breaking 20-bar high (within 1%)
9. Volume surge > 120% of average
10. Strong bullish candle (body > 1.5%)
"""
EXIT_LOGIC = """
1. Stop loss: 4%
2. Take profit: 10%
3. Max hold: 12 bars
4. Trend break: Close below EMA20
5. Momentum reversal: 5-bar momentum < -2%
"""
# Training results for reference
RESULTS = {
"2024": {
"trades": 9,
"return": 12.2,
"win_rate": 56.0,
"max_dd": 11.5
},
"2025": {
"trades": 7,
"return": 7.9,
"win_rate": 43.0,
"max_dd": 2.0
}
}