Auto-discovered strategy
Symbol: SOL | Exchange: Binance | Role: momentum
Click a year to view chart
| Year | Return | Win Rate | Trades | Max DD | Sharpe |
|---|---|---|---|---|---|
| 2020 | -15.0% | 22.2% | 9 | 22.6% | -0.80 |
| 2021 | +29.5% | 44.4% | 27 | 37.9% | 0.81 |
| 2022 | +14.6% | 60.0% | 15 | 16.1% | 0.69 |
| 2023 | +32.6% | 50.0% | 34 | 23.2% | 0.96 |
| 2024 | +23.9% | 58.8% | 17 | 6.4% | 1.28 |
| 2025 | +41.5% | 60.0% | 20 | 12.7% | 1.60 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | +10.0% | OK | 2026-01→ongoing | +0.0% | PASS |
"""
Volume Spike Breakout Strategy - SOLUSDT
=========================================
A momentum breakout strategy that identifies volume spikes at breakout points.
Uses relative indicators only to avoid lookahead bias.
CONCEPT:
Volume spikes (2x average) during a breakout above the 20-bar high signal
strong buying interest. We enter long when price is in an uptrend (above EMA50)
but not overextended (within 5 ATR of EMA50).
ENTRY CONDITIONS (all must be true):
1. Volume > 2x of 20-bar SMA (volume spike)
2. Price closes above 20-bar high (breakout)
3. Bullish bar (close > open)
4. Price above EMA50 (uptrend filter)
5. Price within 5 ATR of EMA50 (not overextended)
EXIT CONDITIONS:
1. Stop loss: 5%
2. Take profit: 10%
3. Time-based: 10 bars max hold
4. Trailing: close below EMA20
PARAMETERS (all round numbers):
- Volume threshold: 2.0x (2x average)
- Breakout period: 20 bars
- Extension filter: 5 ATR
- Stop loss: 5%
- Take profit: 10%
- Max hold: 10 bars
- EMA periods: 20, 50
TRAIN PERFORMANCE (2024-01-01 to 2025-06-30):
- 2024: +23.9% (17 trades)
- 2025 H1: +36.5% (10 trades)
- Total: +60.4% | 27 trades | MaxDD=6.6% | Sharpe=1.66
"""
import sys
sys.path.insert(0, '/root/trade_rules')
def init_strategy():
"""Initialize strategy configuration."""
return {
'name': 'vol_spike_breakout_sol',
'role': 'momentum',
'warmup': 200,
'subscriptions': [
{'symbol': 'SOLUSDT', 'exchange': 'binance', 'timeframe': '4h'},
],
'parameters': {
# Entry parameters - all round numbers
'vol_threshold': 2.0, # Volume must be 2x above 20-bar average
'breakout_period': 20, # Breakout above 20-bar high
'max_extension_atr': 5, # Not more than 5 ATR above EMA50
# Exit parameters
'stop_loss_pct': 5,
'take_profit_pct': 10,
'max_hold_bars': 10,
# EMA periods
'ema_fast': 20,
'ema_slow': 50,
}
}
def process_time_step(ctx):
"""Process each time step and return actions."""
key = ('SOLUSDT', 'binance')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
params = ctx['parameters']
# Need enough bars for indicators - now handled by framework via 'warmup' field
# min_bars = max(params['ema_slow'], params['breakout_period']) + 1
# if i < min_bars:
# return []
actions = []
# Extract price data up to current bar
closes = [b.close for b in bars[:i+1]]
volumes = [b.volume for b in bars[:i+1]]
highs = [b.high for b in bars[:i+1]]
lows = [b.low for b in bars[:i+1]]
opens = [b.open for b in bars[:i+1]]
# Calculate EMA50 (slow trend filter)
ema_slow_period = params['ema_slow']
k_slow = 2 / (ema_slow_period + 1)
ema_slow = sum(closes[:ema_slow_period]) / ema_slow_period
for j in range(ema_slow_period, len(closes)):
ema_slow = closes[j] * k_slow + ema_slow * (1 - k_slow)
# Calculate EMA20 (fast - for trailing exit)
ema_fast_period = params['ema_fast']
k_fast = 2 / (ema_fast_period + 1)
ema_fast = sum(closes[:ema_fast_period]) / ema_fast_period
for j in range(ema_fast_period, len(closes)):
ema_fast = closes[j] * k_fast + ema_fast * (1 - k_fast)
# Calculate 20-bar Volume SMA
vol_period = 20
vol_sma = sum(volumes[max(0, i-vol_period+1):i+1]) / min(vol_period, i+1)
# Calculate 14-bar ATR
atr_period = 14
if i < atr_period:
return []
tr_list = []
for j in range(i - atr_period + 1, i + 1):
if j == 0:
tr = highs[j] - lows[j]
else:
tr = max(
highs[j] - lows[j],
abs(highs[j] - closes[j-1]),
abs(lows[j] - closes[j-1])
)
tr_list.append(tr)
atr = sum(tr_list) / len(tr_list)
# Avoid division by zero
if vol_sma <= 0 or atr <= 0:
return []
if key not in positions:
# ============================================
# ENTRY LOGIC
# ============================================
# 1. Volume spike: current volume > 2x average
vol_ratio = volumes[i] / vol_sma
if vol_ratio < params['vol_threshold']:
return []
# 2. Bullish bar: close > open
if closes[i] <= opens[i]:
return []
# 3. Uptrend filter: price above EMA50
if closes[i] < ema_slow:
return []
# 4. Breakout: price above 20-bar high
breakout_period = params['breakout_period']
if i < breakout_period:
return []
prev_high = max(highs[i-breakout_period:i])
if closes[i] <= prev_high:
return []
# 5. Not overextended: within 5 ATR of EMA50
extension = (closes[i] - ema_slow) / atr
if extension > params['max_extension_atr']:
return []
# All conditions met - enter long
actions.append({
'action': 'open_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
'size': 1.0,
'stop_loss_pct': params['stop_loss_pct'],
'take_profit_pct': params['take_profit_pct'],
})
else:
# ============================================
# EXIT LOGIC
# ============================================
pos = positions[key]
bars_held = i - pos.entry_bar
# Time-based exit: max hold period
if bars_held >= params['max_hold_bars']:
actions.append({
'action': 'close_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
})
# Trailing exit: close below EMA20 (after min hold of 2 bars)
elif bars_held >= 2 and closes[i] < ema_fast:
actions.append({
'action': 'close_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
})
# Note: Stop loss and take profit are handled by the framework
return actions
# For testing
if __name__ == '__main__':
from strategy import backtest_strategy
results, profitable, _ = backtest_strategy(init_strategy, process_time_step)
print(f"\nProfitable periods: {profitable}/2")