Auto-discovered strategy
Symbol: SOL | Exchange: Binance | Role: momentum
Click a year to view chart
| Year | Return | Win Rate | Trades | Max DD | Sharpe |
|---|---|---|---|---|---|
| 2020 | -10.0% | 0.0% | 2 | 9.8% | -1990328647664304.50 |
| 2021 | -29.1% | 25.0% | 28 | 51.1% | -0.89 |
| 2022 | +5.7% | 50.0% | 2 | 4.3% | 0.57 |
| 2023 | +17.1% | 40.0% | 10 | 9.8% | 0.79 |
| 2024 | +22.8% | 50.0% | 14 | 7.3% | 1.40 |
| 2025 | +20.6% | 63.6% | 11 | 6.5% | 1.52 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | +2.8% | OK | 2026-01→ongoing | +0.0% | PASS |
Not yet reviewed. Run: ./review_strategy.sh sol_squeeze_momentum_breakout
"""
SOL Squeeze Momentum Breakout Strategy
=======================================
A momentum breakout strategy for SOLUSDT that identifies volatility compression
(using TTM Squeeze-style detection) followed by explosive breakouts.
Core Concept:
- Uses Bollinger Bands inside Keltner Channels to detect volatility squeeze
- When squeeze releases AND price breaks out with momentum, enter long
- EMA cascade filter ensures we only trade in bull markets
- Tight stops (5%) with 2:1 reward ratio
Entry Conditions:
1. Regime: EMA50 > EMA200 (bull market only)
2. Squeeze: BB was inside KC within last 10 bars (volatility compression)
3. Release: Currently NOT in squeeze (volatility expanding)
4. Breakout: Close above 20-bar high
5. Momentum: 5-bar > 2% AND 10-bar > 3%
6. EMA Cascade: Price > EMA20 > EMA50
7. Volume: Above 80% of 20-bar average
8. Green candle confirmation
Exit Conditions:
1. Take profit: +10%
2. Stop loss: -5%
3. Max hold: 12 bars (48 hours)
4. Trend break: Close below EMA20
5. Momentum reversal: 5-bar momentum < -2%
Risk Management:
- Only trades in bull markets (EMA50 > EMA200)
- Tight 5% stop loss
- Time-based exit to avoid overholding
- Momentum-based exit for quick reversals
Train Performance (2024-2025H1):
2024: +22.8% | 50% WR | 14 trades | Sharpe 1.40 | DD 7.3%
2025: +8.6% | 40% WR | 5 trades | Sharpe 0.70 | DD 6.5%
Total: +31.4% | 19 trades
"""
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': 'sol_squeeze_momentum_breakout',
'role': 'momentum',
'warmup': 100, # Need 100 bars for indicator warmup
'subscriptions': [
{'symbol': 'SOLUSDT', 'exchange': 'binance', 'timeframe': '4h'},
],
'parameters': {}
}
def process_time_step(ctx):
"""
Process each time step and return trade actions.
Uses TTM Squeeze-style detection:
- Bollinger Bands (20, 2.0)
- Keltner Channels (EMA20, 1.5 * ATR14)
- Squeeze = BB inside KC
"""
key = ('SOLUSDT', 'binance')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
state = ctx['state']
# Initialize indicators on first call
if 'initialized' not in state:
closes = [b.close for b in bars]
highs = [b.high for b in bars]
lows = [b.low for b in bars]
volumes = [b.volume for b in bars]
# EMAs for trend filtering
state['ema20'] = ema(closes, 20)
state['ema50'] = ema(closes, 50)
state['ema200'] = ema(closes, 200)
# ATR for Keltner Channels
state['atr14'] = atr(highs, lows, closes, 14)
# Bollinger Bands (20, 2.0)
bb_mid, bb_upper, bb_lower = bollinger_bands(closes, 20, 2.0)
state['bb_upper'] = bb_upper
state['bb_lower'] = bb_lower
# Keltner Channels: EMA20 +/- 1.5 * ATR14
kc_upper = []
kc_lower = []
for j in range(len(closes)):
if state['ema20'][j] is not None and state['atr14'][j] is not None:
kc_upper.append(state['ema20'][j] + 1.5 * state['atr14'][j])
kc_lower.append(state['ema20'][j] - 1.5 * state['atr14'][j])
else:
kc_upper.append(None)
kc_lower.append(None)
state['kc_upper'] = kc_upper
state['kc_lower'] = kc_lower
# Squeeze condition: BB inside KC (low volatility)
state['is_squeeze'] = []
for j in range(len(closes)):
if all(x is not None for x in [bb_lower[j], bb_upper[j], kc_lower[j], kc_upper[j]]):
# Squeeze when BOTH BB bands are inside KC bands
in_squeeze = bb_lower[j] > kc_lower[j] and bb_upper[j] < kc_upper[j]
state['is_squeeze'].append(in_squeeze)
else:
state['is_squeeze'].append(False)
# Volume SMA for confirmation
state['vol_sma'] = sma(volumes, 20)
# Momentum indicators
state['mom5'] = pct_change(closes, 5)
state['mom10'] = pct_change(closes, 10)
state['initialized'] = True
# Get current indicator values
ema20 = state['ema20'][i] if i < len(state['ema20']) else None
ema50 = state['ema50'][i] if i < len(state['ema50']) else None
ema200 = state['ema200'][i] if i < len(state['ema200']) else None
is_squeeze = state['is_squeeze'][i] if i < len(state['is_squeeze']) else False
bb_upper = state['bb_upper'][i] if i < len(state['bb_upper']) else None
vol_sma = state['vol_sma'][i] if i < len(state['vol_sma']) else None
mom5 = state['mom5'][i] if i < len(state['mom5']) else None
mom10 = state['mom10'][i] if i < len(state['mom10']) else None
# Check for None values
if any(x is None for x in [ema20, ema50, ema200, bb_upper, vol_sma, mom5, mom10]):
return []
bar = bars[i]
highs = [b.high for b in bars]
actions = []
if key not in positions:
# === ENTRY LOGIC ===
# 1. REGIME FILTER: EMA50 > EMA200 (bull market only)
if ema50 < ema200:
return []
# 2. SQUEEZE WAS ACTIVE: Look for squeeze in last 10 bars
was_in_squeeze = False
for lookback in range(1, 11):
if i - lookback >= 0 and state['is_squeeze'][i - lookback]:
was_in_squeeze = True
break
if not was_in_squeeze:
return []
# 3. EXPANSION: Currently NOT in squeeze (released)
if is_squeeze:
return []
# 4. BREAKOUT: Close above 20-bar high
high_20 = max(highs[i-20:i]) if i >= 20 else None
if high_20 is None or bar.close < high_20:
return []
# 5. MOMENTUM: 5-bar > 2% AND 10-bar > 3%
if mom5 < 2.0 or mom10 < 3.0:
return []
# 6. EMA CASCADE: Price > EMA20 > EMA50
if not (bar.close > ema20 and ema20 > ema50):
return []
# 7. VOLUME: Above 80% average
if bar.volume < vol_sma * 0.8:
return []
# 8. GREEN CANDLE
if bar.close < bar.open:
return []
# Open long position
actions.append({
'action': 'open_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
'size': 1.0,
'stop_loss_pct': 5.0, # Tight stop
'take_profit_pct': 10.0, # 2:1 risk/reward
})
else:
# === EXIT LOGIC ===
pos = positions[key]
bars_held = i - pos.entry_bar
# Max hold: 12 bars (48 hours on 4h timeframe)
if bars_held >= 12:
actions.append({
'action': 'close_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
})
return actions
# Trend break: close below EMA20
if bar.close < ema20:
actions.append({
'action': 'close_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
})
return actions
# Momentum reversal: 5-bar momentum < -2%
if mom5 < -2.0:
actions.append({
'action': 'close_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
})
return actions
# Metadata for documentation
ENTRY_LOGIC = """
1. Regime: EMA50 > EMA200 (bull market only)
2. Squeeze: BB was inside KC within last 10 bars (volatility compression)
3. Release: Currently NOT in squeeze (volatility expanding)
4. Breakout: Close above 20-bar high
5. Momentum: 5-bar > 2% AND 10-bar > 3%
6. EMA Cascade: Price > EMA20 > EMA50
7. Volume: Above 80% of 20-bar average
8. Green candle confirmation
"""
EXIT_LOGIC = """
1. Take profit: +10%
2. Stop loss: -5%
3. Max hold: 12 bars (48 hours)
4. Trend break: Close below EMA20
5. Momentum reversal: 5-bar momentum < -2%
"""
# Training results for reference
RESULTS = {
"2024": {
"trades": 14,
"return": 22.8,
"sharpe": 1.40,
"win_rate": 50.0,
"max_dd": 7.3
},
"2025": {
"trades": 5,
"return": 8.6,
"sharpe": 0.70,
"win_rate": 40.0,
"max_dd": 6.5
}
}