Auto-discovered strategy
Symbol: ETH | Exchange: Bitfinex | Role: momentum
Click a year to view chart
| Year | Return | Win Rate | Trades | Max DD | Sharpe |
|---|---|---|---|---|---|
| 2020 | +98.5% | 100.0% | 3 | 0.0% | 8.53 |
| 2021 | -11.9% | 20.0% | 5 | 28.4% | -0.34 |
| 2022 | -34.8% | 0.0% | 3 | 31.0% | -9.75 |
| 2023 | -1.5% | 33.3% | 3 | 19.3% | -0.06 |
| 2024 | +43.0% | 66.7% | 3 | 9.6% | 1.42 |
| 2025 | -2.2% | 20.0% | 5 | 32.9% | -0.05 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | -14.1% | FAIL | 2026-01→ongoing | +0.0% | FAIL |
Not yet reviewed. Run: ./review_strategy.sh eth_hhll_breakout
"""
ETH HHLL Breakout Strategy
===========================
A momentum strategy that enters on N-bar high breakouts when the market
exhibits bullish price structure (Higher Highs and Higher Lows).
Core Logic:
- Entry: 50-bar high breakout with confirmed HH+HL structure in bull regime
- Exit: Structure break (close below swing low) or stop/target hit
- Risk: Dynamic stop below swing low, 3:1 reward/risk target
Key Principles:
- Uses RELATIVE indicators only (EMAs, bar counts, % moves)
- ROUND parameters (15, 20, 50, 100)
- Regime filter prevents trading against macro trend
- Structure-based entries avoid chasing
Train Period Results (2024-01 to 2025-06):
- 2024: +43.0% | 3 trades | 67% WR | 9.6% DD
- 2025: +20.3% | 3 trades | 33% WR | 14.7% DD
- Total: +63.3% | 6 trades | Max DD 14.7%
"""
import sys
sys.path.insert(0, '/root/trade_rules')
def init_strategy():
"""Initialize the HHLL Breakout strategy"""
return {
'name': 'eth_hhll_breakout',
'role': 'momentum', # Trend-following, allowed to lose bounded in bear markets
'warmup': 200, # Need EMA100 and swing lookback to be valid
'subscriptions': [
{'symbol': 'tETHUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
],
'parameters': {
'breakout_period': 50, # N-bar high breakout
'swing_period': 15, # Swing detection period
'ema_fast': 20, # Fast EMA for regime
'ema_slow': 100, # Slow EMA for regime
'min_swing_pct': 3, # Min % for valid swing point
}
}
def ema_calc(prices, period):
"""Calculate Exponential Moving Average"""
if len(prices) < period:
return [None] * len(prices)
result = [None] * (period - 1)
k = 2 / (period + 1)
sma_val = sum(prices[:period]) / period
result.append(sma_val)
for j in range(period, len(prices)):
result.append(prices[j] * k + result[-1] * (1 - k))
return result
def get_swing_points(bars, idx, period, min_pct):
"""
Find swing highs and lows with minimum percentage requirement.
A swing high is a local maximum over 2*period bars.
A swing low is a local minimum over 2*period bars.
Returns: (swing_highs, swing_lows) as lists of (index, price) tuples
"""
highs = [b.high for b in bars]
lows = [b.low for b in bars]
swing_highs = []
swing_lows = []
# Find swing highs - scan backward from current bar
for i in range(idx - period, max(period, idx - 200), -1):
if i < period or i >= len(bars) - period:
continue
# Check if this is a local maximum
window_highs = highs[i-period:i+period+1]
if highs[i] == max(window_highs):
# Verify swing has minimum size
nearby_low = min(lows[max(0, i-period):i+period+1])
swing_size = (highs[i] - nearby_low) / nearby_low * 100
if swing_size >= min_pct:
# Don't cluster swings too close together
if not swing_highs or abs(i - swing_highs[-1][0]) >= period:
swing_highs.append((i, highs[i]))
if len(swing_highs) >= 2:
break
# Find swing lows - scan backward from current bar
for i in range(idx - period, max(period, idx - 200), -1):
if i < period or i >= len(bars) - period:
continue
# Check if this is a local minimum
window_lows = lows[i-period:i+period+1]
if lows[i] == min(window_lows):
# Verify swing has minimum size
nearby_high = max(highs[max(0, i-period):i+period+1])
swing_size = (nearby_high - lows[i]) / lows[i] * 100
if swing_size >= min_pct:
if not swing_lows or abs(i - swing_lows[-1][0]) >= period:
swing_lows.append((i, lows[i]))
if len(swing_lows) >= 2:
break
return swing_highs, swing_lows
def process_time_step(ctx):
"""
Process each bar and generate trading signals.
Entry Conditions (ALL must be true):
1. Bull regime: EMA20 > EMA100 AND price > EMA20
2. Bullish structure: Higher High + Higher Low confirmed
3. 50-bar high breakout
Exit Conditions (ANY triggers exit):
1. Structure break: Close below swing low (with 2% buffer)
2. Stop loss hit (dynamic, based on swing low)
3. Take profit hit (3x stop distance)
"""
key = ('tETHUSD', 'bitfinex')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
state = ctx['state']
params = ctx['parameters']
closes = [b.close for b in bars]
highs = [b.high for b in bars]
# Calculate EMAs for regime filter
ema_fast = ema_calc(closes, params['ema_fast'])
ema_slow = ema_calc(closes, params['ema_slow'])
if ema_fast[i] is None or ema_slow[i] is None:
return []
# REGIME FILTER: Only trade in bull regime
# This prevents entering during downtrends
bull_regime = ema_fast[i] > ema_slow[i] and closes[i] > ema_fast[i]
current_price = closes[i]
current_high = highs[i]
breakout_period = params['breakout_period']
if i < breakout_period:
return []
# N-bar high breakout detection
prev_high = max(highs[i-breakout_period:i])
breakout = current_high > prev_high
# Get swing structure for HH/HL detection
swing_highs, swing_lows = get_swing_points(
bars, i, params['swing_period'], params['min_swing_pct']
)
# Check for Higher High and Higher Low (bullish price structure)
# swing_highs[0] is most recent, swing_highs[1] is previous
has_hh = len(swing_highs) >= 2 and swing_highs[0][1] > swing_highs[1][1]
has_hl = len(swing_lows) >= 2 and swing_lows[0][1] > swing_lows[1][1]
bullish_structure = has_hh and has_hl
# Initialize state for tracking
if 'entry_swing_low' not in state:
state['entry_swing_low'] = None
actions = []
if key not in positions:
# ENTRY LOGIC
if bull_regime and bullish_structure and breakout:
# Set stop below most recent swing low
entry_swing_low = swing_lows[0][1] if swing_lows else current_price * 0.95
stop_dist = (current_price - entry_swing_low) / current_price * 100
stop_pct = max(5, stop_dist) # At least 5% stop
# Only enter if stop is reasonable
if stop_pct < 15:
actions.append({
'action': 'open_long',
'symbol': 'tETHUSD',
'exchange': 'bitfinex',
'size': 1.0,
'stop_loss_pct': stop_pct,
'take_profit_pct': stop_pct * 3, # 3:1 R/R
})
state['entry_swing_low'] = entry_swing_low
else:
# EXIT LOGIC: Structure break (close below swing low with 2% buffer)
entry_swing_low = state.get('entry_swing_low')
if entry_swing_low and closes[i] < entry_swing_low * 0.98:
actions.append({
'action': 'close_long',
'symbol': 'tETHUSD',
'exchange': 'bitfinex',
})
state['entry_swing_low'] = None
return actions
# For standalone testing
if __name__ == '__main__':
from strategy import backtest_strategy
results, profitable, _ = backtest_strategy(init_strategy, process_time_step)
print(f"\nProfitable years: {profitable}/2")