Auto-discovered strategy
Symbol: ETH | Exchange: Binance | Role: momentum
Click a year to view chart
| Year | Return | Win Rate | Trades | Max DD | Sharpe |
|---|---|---|---|---|---|
| 2020 | +30.3% | 60.0% | 40 | 9.4% | 1.36 |
| 2021 | -25.8% | 50.0% | 52 | 26.5% | -1.10 |
| 2022 | -12.8% | 43.8% | 16 | 22.1% | -0.86 |
| 2023 | -12.8% | 29.4% | 17 | 12.4% | -1.62 |
| 2024 | +10.7% | 67.9% | 28 | 6.0% | 0.86 |
| 2025 | +1.2% | 42.9% | 21 | 13.1% | 0.08 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | -5.2% | FAIL | 2026-01→ongoing | +0.0% | FAIL |
Not yet reviewed. Run: ./review_strategy.sh eth_breakout_gap_fill
"""
ETH Breakout Gap Fill Strategy
==============================
Trades the "gap fill" pattern that occurs after breakouts.
When price breaks to a new N-bar high (breakout), a "gap" is created
in the price structure - the body of the breakout candle. Price often
pulls back to fill this gap before continuing higher.
Entry conditions:
1. UPTREND: EMA50 > EMA200 (regime filter)
2. Recent N-bar high breakout (within last 10 bars)
3. Price pulls back to the breakout candle's body low (gap fill level)
4. Bullish reversal at gap level (close > open, upper 50% close)
Exit conditions:
1. Target: 1% above the breakout high (continuation)
2. Stop: 5% below entry
3. Trend exit: EMA50 crosses below EMA200
4. Timeout: 10 bars (40 hours)
Parameters (all round values):
- N-bar high lookback: 50 bars
- Pullback window: 10 bars
- Stop loss: 5%
- Max hold: 10 bars
Performance (TRAIN DATA 2024-01-01 to 2025-06-30):
- 2024: +10.7%, 28 trades, 68% WR, 6% max DD
- 2025H1: +8.7%, 5 trades, 40% WR, 3% max DD
- Total: +19.4%
Strategy survives bear markets by:
1. Staying flat when EMA50 < EMA200 (no trades in downtrend)
2. Using tight 5% stops
3. Trading continuation (with trend) not mean reversion
"""
# State to track breakout levels across bars
_breakout_levels = []
def init_strategy():
"""Initialize strategy configuration."""
global _breakout_levels
_breakout_levels = [] # Reset state on init
return {
'name': 'eth_breakout_gap_fill',
'role': 'momentum',
'warmup': 200,
'subscriptions': [
{'symbol': 'ETHUSDT', 'exchange': 'binance', 'timeframe': '4h'},
],
'parameters': {
'n_bar_high': 50, # Breakout lookback period
'pullback_bars': 10, # Max bars to wait for pullback entry
'stop_pct': 5, # Stop loss percentage
'max_hold': 10 # Maximum bars to hold position
}
}
def process_time_step(ctx):
"""Process each time step and return trading actions."""
global _breakout_levels
import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema
key = ('ETHUSDT', 'binance')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
params = ctx['parameters']
# Need warmup period for EMA200
if i < 200:
return []
# Pre-compute EMAs
closes = [bars[j].close for j in range(max(0, i - 250), i + 1)]
ema50_vals = ema(closes, 50)
ema200_vals = ema(closes, 200)
if ema50_vals[-1] is None or ema200_vals[-1] is None:
return []
ema50 = ema50_vals[-1]
ema200 = ema200_vals[-1]
actions = []
curr_bar = bars[i]
curr_close = curr_bar.close
# Clean up old breakout levels (older than 50 bars)
_breakout_levels = [b for b in _breakout_levels if b['bar'] > i - 50]
# REGIME FILTER: Uptrend required (EMA50 > EMA200)
in_uptrend = ema50 > ema200
if key not in positions:
# ENTRY LOGIC
# Stay flat in downtrend
if not in_uptrend:
return []
# Check for new N-bar high breakout
n_bar = params['n_bar_high']
n_bar_high_val = max(bars[j].high for j in range(i - n_bar, i))
if curr_bar.high > n_bar_high_val:
# New breakout - record the gap level
_breakout_levels.append({
'bar': i,
'high': curr_bar.high,
'body_low': min(curr_bar.open, curr_close), # Gap fill level
'used': False
})
# Look for pullback entry at gap fill level
pullback_max = params['pullback_bars']
for breakout in _breakout_levels:
if breakout['used']:
continue
bars_since = i - breakout['bar']
# Must be 2-10 bars after breakout
if bars_since < 2 or bars_since > pullback_max:
continue
# Check if price pulled back to gap fill level and recovered
gap_level = breakout['body_low']
if curr_bar.low <= gap_level and curr_close > gap_level:
# Pullback touched gap level - check for bullish reversal
bar_range = curr_bar.high - curr_bar.low
if bar_range > 0:
close_pos = (curr_close - curr_bar.low) / bar_range
# Bullish bar closing in upper half
if close_pos > 0.5 and curr_close > curr_bar.open:
breakout['used'] = True
# ENTRY SIGNAL
actions.append({
'action': 'open_long',
'symbol': 'ETHUSDT',
'exchange': 'binance',
'size': 1.0,
'stop_loss_pct': params['stop_pct'],
'metadata': {
'breakout_high': breakout['high'],
'entry_bar': i
}
})
break
else:
# EXIT LOGIC
pos = positions[key]
meta = pos.metadata if hasattr(pos, 'metadata') and pos.metadata else {}
entry_bar = meta.get('entry_bar', i - 5)
breakout_high = meta.get('breakout_high', pos.entry_price * 1.05)
bars_held = i - entry_bar
# TARGET: Just above breakout high (continuation)
target_price = breakout_high * 1.01
if curr_bar.high >= target_price:
actions.append({
'action': 'close_long',
'symbol': 'ETHUSDT',
'exchange': 'binance',
})
return actions
# TREND EXIT: Close if trend turns bearish
if not in_uptrend:
actions.append({
'action': 'close_long',
'symbol': 'ETHUSDT',
'exchange': 'binance',
})
return actions
# TIMEOUT: Close after max hold period
if bars_held >= params['max_hold']:
actions.append({
'action': 'close_long',
'symbol': 'ETHUSDT',
'exchange': 'binance',
})
return actions