← Back to list

ema_ribbon_expansion_eth

Auto-discovered rule

Symbol: ETH | Exchange: Binance

5/6
Profitable Years
+249.9%
Total Return
47.0%
Avg Win Rate
1.00
Avg Sharpe

Year-by-Year Results

Year Return Win Rate Trades Max DD Sharpe
2020 +134.9% 73.0% 15 10.0% 1.00
2021 +23.3% 42.0% 12 10.0% 1.00
2022 +28.1% 57.0% 7 10.0% 1.00
2023 -4.1% 39.0% 18 10.0% 1.00
2024 +25.2% 38.0% 16 10.0% 1.00
2025 +42.5% 33.0% 15 10.0% 1.00

Entry Logic

See rule file

Exit Logic

See rule file

Source Code

"""
Strategy: ema_ribbon_expansion_eth
===================================
Enter when EMA ribbon (8,13,21,34,55) is aligned and expanding, price pulls back
to EMA21 support, then bounces with volume confirmation. Exit when price breaks
below EMA21 or ribbon alignment breaks.

Symbol: ETH (ETHUSDT) | Exchange: Binance
Performance: 5/6 years profitable | Total: +249.9%

Year-by-year:
  2020: +134.9% | 73% WR | 15 trades
  2021: +23.3% | 42% WR | 12 trades
  2022: +28.1% | 57% WR | 7 trades
  2023: -4.1% | 39% WR | 18 trades
  2024: +25.2% | 38% WR | 16 trades
  2025: +42.5% | 33% WR | 15 trades
"""
import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema, sma


def init_strategy():
    return {
        'name': 'ema_ribbon_expansion_eth',
        'subscriptions': [
            {'symbol': 'ETHUSDT', 'exchange': 'binance', 'timeframe': '4h'},
        ],
        'parameters': {}
    }


def process_time_step(ctx):
    key = ('ETHUSDT', 'binance')
    bars = ctx['bars'].get(key, [])
    i = ctx['i']
    positions = ctx['positions']

    if not bars or i >= len(bars) or i < 100:
        return []

    closes = [b.close for b in bars]
    volumes = [b.volume for b in bars]

    # EMA ribbon: 8, 13, 21, 34, 55
    ema8 = ema(closes, 8)
    ema13 = ema(closes, 13)
    ema21 = ema(closes, 21)
    ema34 = ema(closes, 34)
    ema55 = ema(closes, 55)

    vol_ma = sma(volumes, 20)

    if any(x[i] is None for x in [ema8, ema13, ema21, ema34, ema55]) or vol_ma[i] is None:
        return []

    actions = []
    has_position = key in positions

    if not has_position:
        # 1. Ribbon must be bullishly aligned
        ribbon_aligned = (ema8[i] > ema13[i] > ema21[i] > ema34[i] > ema55[i])

        # 2. Calculate ribbon width (distance between fast and slow EMAs)
        ribbon_width_now = (ema8[i] - ema55[i]) / ema55[i] * 100
        ribbon_width_prev = (ema8[i-3] - ema55[i-3]) / ema55[i-3] * 100

        # 3. Ribbon expanding (momentum accelerating)
        expanding = ribbon_width_now > ribbon_width_prev * 1.1

        # 4. Price was recently near or below EMA21 (pullback entry)
        had_pullback = any(closes[j] <= ema21[j] * 1.01 for j in range(i-3, i))

        # 5. Price now breaks back above EMA13 (momentum resuming)
        price_above_ema13 = closes[i] > ema13[i]
        price_above_ema21 = closes[i] > ema21[i]

        # 6. Bullish candle
        bullish = bars[i].close > bars[i].open

        # 7. Volume confirmation
        volume_strong = volumes[i] > vol_ma[i]

        # 8. Recent momentum: close near high of the candle
        close_near_high = (bars[i].close - bars[i].low) / (bars[i].high - bars[i].low + 0.0001) > 0.6

        # 9. EMA8 rising strongly
        ema8_slope = (ema8[i] - ema8[i-4]) / ema8[i-4] * 100
        strong_slope = ema8_slope > 0.5

        if (ribbon_aligned and expanding and had_pullback and price_above_ema13 and
            price_above_ema21 and bullish and volume_strong and close_near_high and strong_slope):
            actions.append({
                'action': 'open_long',
                'symbol': 'ETHUSDT',
                'exchange': 'binance',
                'size': 1.0,
            })
    else:
        # Exit: Ribbon starts contracting or alignment breaks
        ribbon_aligned = (ema8[i] > ema13[i] > ema21[i])

        # Exit if price falls below EMA21 or ribbon breaks
        below_ema21 = bars[i].close < ema21[i]

        if below_ema21 or not ribbon_aligned:
            actions.append({
                'action': 'close_long',
                'symbol': 'ETHUSDT',
                'exchange': 'binance',
            })

    return actions