Auto-discovered strategy
Symbol: BTC | Exchange: Binance | Role: momentum
Click a year to view chart
| Year | Return | Win Rate | Trades | Max DD | Sharpe |
|---|---|---|---|---|---|
| 2020 | +22.9% | 50.0% | 12 | 9.1% | 1.14 |
| 2021 | +15.2% | 50.0% | 12 | 11.0% | 0.71 |
| 2022 | -5.0% | 0.0% | 1 | 5.0% | 0.00 |
| 2023 | +15.2% | 50.0% | 14 | 10.9% | 0.77 |
| 2024 | +25.4% | 63.6% | 11 | 4.7% | 1.77 |
| 2025 | +12.6% | 71.4% | 7 | 2.2% | 1.86 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | +5.1% | OK | 2026-01→ongoing | +0.0% | PASS |
Not yet reviewed. Run: ./review_strategy.sh hhhl_structure_momentum
"""
Higher High / Higher Low Structure Momentum Strategy
=====================================================
Trades breakouts in confirmed uptrends using market structure analysis.
Identifies genuine swing points and only enters when HH/HL pattern is present.
CONCEPT:
Market structure analysis is a core price action technique. In an uptrend:
- Price makes Higher Highs (HH): Each swing high exceeds the previous
- Price makes Higher Lows (HL): Each swing low exceeds the previous
This strategy enters on breakouts above swing highs when this pattern exists.
ENTRY CONDITIONS (5 total):
1. Bull regime: EMA50 > EMA200
2. Price above EMA50: Confirmed uptrend
3. Higher High: Latest swing high > Previous swing high
4. Higher Low: Latest swing low > Previous swing low
5. Breakout: Close breaks above latest swing high
6. Volume: Above 1.1x 20-bar average
7. Momentum: 2 consecutive higher closes
EXIT CONDITIONS:
1. Structure break: Close < recent swing low
2. Close below EMA50: Trend weakening
3. Regime change: EMA50 < EMA200
4. Time exit: 20 bars maximum hold
5. Stop loss: 5%
6. Take profit: 10%
SWING POINT DETECTION:
A swing high/low requires 3 bars on each side with lower/higher values.
This ensures we're trading genuine local extrema, not noise.
TRAIN RESULTS (2024-01 to 2025-06):
2024: +25.4% | 11 trades | 64% WR | 4.7% DD | Sharpe 1.77
2025H1: +5.3% | 3 trades | 67% WR | 2.2% DD | Sharpe 1.09
Total: +30.7%
ROBUSTNESS FEATURES:
- Uses only relative indicators (EMAs, swing points, volume ratios)
- Round parameters: 50, 200, 20, 3, 5%, 10%
- Bull-market filter prevents trades in downtrends
- Structure-based exit preserves profits on reversal
- Consecutive higher closes reduces false breakouts
Symbol: BTCUSDT
Exchange: binance
Timeframe: 4h
Role: momentum
"""
import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema
def init_strategy():
return {
'name': 'hhhl_structure_momentum',
'role': 'momentum',
'warmup': 200, # EMA200 + buffer
'subscriptions': [
{'symbol': 'BTCUSDT', 'exchange': 'binance', 'timeframe': '4h'},
],
'parameters': {}
}
def find_swing_points(bars, i, lookback=50, confirmation=3):
"""
Find confirmed swing highs and lows.
A swing point requires 'confirmation' bars on each side with lower highs
(for swing high) or higher lows (for swing low).
Args:
bars: List of Bar objects
i: Current bar index
lookback: How many bars to look back
confirmation: Bars required on each side for confirmation
Returns:
(swing_highs, swing_lows): Lists of (bar_index, price) tuples
"""
swing_highs = []
swing_lows = []
start = max(confirmation, i - lookback)
end = i - confirmation
for j in range(start, end):
# Check if j is a swing high
is_high = all(
bars[j].high >= bars[k].high
for k in range(j - confirmation, j + confirmation + 1)
if k != j and 0 <= k < len(bars)
)
if is_high:
swing_highs.append((j, bars[j].high))
# Check if j is a swing low
is_low = all(
bars[j].low <= bars[k].low
for k in range(j - confirmation, j + confirmation + 1)
if k != j and 0 <= k < len(bars)
)
if is_low:
swing_lows.append((j, bars[j].low))
return swing_highs, swing_lows
def process_time_step(ctx):
"""
Process each time step and return trading actions.
ENTRY LOGIC:
1. Bull regime: EMA50 > EMA200
2. Price > EMA50 (confirmed uptrend)
3. Find last 2 swing highs and 2 swing lows in 50-bar window
4. Higher High: latest swing high > previous swing high
5. Higher Low: latest swing low > previous swing low
6. Breakout: close > latest swing high
7. Volume: above 1.1x 20-bar average
8. Momentum: 2 consecutive higher closes
EXIT LOGIC:
1. Structure break: close < recent swing low
2. Close below EMA50
3. Regime change: EMA50 < EMA200
4. Time exit: 20 bars
5. Stop loss: 5%
6. Take profit: 10%
"""
key = ('BTCUSDT', 'binance')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
closes = [b.close for b in bars]
volumes = [b.volume for b in bars]
# Calculate EMAs
ema50_vals = ema(closes, 50)
ema200_vals = ema(closes, 200)
ema50 = ema50_vals[i]
ema200 = ema200_vals[i]
if ema50 is None or ema200 is None:
return []
# Regime filter: bull market
bull_regime = ema50 > ema200
price_above_ema50 = bars[i].close > ema50
# Find swing points
swing_highs, swing_lows = find_swing_points(bars, i, lookback=50, confirmation=3)
actions = []
if key not in positions:
# STAY FLAT in bear markets or when price is below EMA50
if not (bull_regime and price_above_ema50):
return []
# Need at least 2 swing highs and 2 swing lows for HH/HL pattern
if len(swing_highs) < 2 or len(swing_lows) < 2:
return []
# Sort by time
swing_highs.sort(key=lambda x: x[0])
swing_lows.sort(key=lambda x: x[0])
# Get most recent 2 swings
recent_highs = swing_highs[-2:]
recent_lows = swing_lows[-2:]
# HH: latest swing high > previous swing high
higher_high = recent_highs[1][1] > recent_highs[0][1]
# HL: latest swing low > previous swing low
higher_low = recent_lows[1][1] > recent_lows[0][1]
# Breakout above latest swing high
latest_swing_high = recent_highs[-1][1]
breakout = bars[i].close > latest_swing_high
# Volume confirmation: above 1.1x average
avg_vol = sum(volumes[i-20:i]) / 20 if i >= 20 else volumes[i]
vol_confirm = volumes[i] > avg_vol * 1.1
# Momentum: 2 consecutive higher closes
momentum = closes[i] > closes[i-1] > closes[i-2]
# ENTRY: All conditions must be true
if higher_high and higher_low and breakout and vol_confirm and momentum:
actions.append({
'action': 'open_long',
'symbol': 'BTCUSDT',
'exchange': 'binance',
'size': 1.0,
'stop_loss_pct': 5.0,
'take_profit_pct': 10.0,
'metadata': {'entry_swing_low': recent_lows[-1][1]}
})
else:
# === EXIT LOGIC ===
pos = positions[key]
bars_held = i - pos.entry_bar
# Get current swing lows for exit reference
_, current_swing_lows = find_swing_points(bars, i, lookback=20, confirmation=3)
# Use entry swing low as reference, update if newer swing forms
ref_swing_low = pos.metadata.get('entry_swing_low', 0)
if current_swing_lows:
current_swing_lows.sort(key=lambda x: x[0])
ref_swing_low = current_swing_lows[-1][1]
# Exit conditions
structure_break = bars[i].close < ref_swing_low
below_ema50 = bars[i].close < ema50
regime_exit = ema50 < ema200
time_exit = bars_held >= 20
if structure_break or below_ema50 or regime_exit or time_exit:
actions.append({
'action': 'close_long',
'symbol': 'BTCUSDT',
'exchange': 'binance',
})
return actions
# Allow direct execution for testing
if __name__ == "__main__":
from strategy import backtest_strategy
results, profitable, _ = backtest_strategy(init_strategy, process_time_step, verbose=True)