← Back to list

rally_exhaustion_pullback_4bar

Trend exhaustion strategy that enters after strong 4-bar rallies show first signs of pullback. Entry Logic:

Symbol: BTC | Exchange: Bitfinex

5/6
Profitable Years
+79.6%
Total Return
54.1%
Avg Win Rate
0.60
Avg Sharpe

Year-by-Year Results

Year Return Win Rate Trades Max DD Sharpe
2020 +23.6% 45.0% 20 6.2% 1.26
2021 +40.3% 65.5% 29 11.8% 1.32
2022 -26.5% 21.4% 14 24.1% -2.12
2023 +27.3% 73.3% 15 4.1% 1.93
2024 +14.4% 63.6% 11 7.9% 1.11
2025 +0.6% 55.6% 9 6.4% 0.10

Entry Logic

See strategy file

Exit Logic

See strategy file

Source Code

"""
Strategy: rally_exhaustion_pullback_4bar
========================================
Trend exhaustion strategy that enters after strong 4-bar rallies show
first signs of pullback.

Entry Logic:
- 4 consecutive green bars (uptrend momentum)
- Rally of 4%+ during those 4 bars (meaningful move)
- Current bar closes red (first exhaustion signal/pullback)
- Price remains above EMA20 (trend still intact)

Exit Logic:
- Hold for 12 bars (2 days on 4h timeframe)

Performance: 5/6 years profitable | Total: +79.6%
2020: +23.6% | 45% WR | 20 trades
2021: +40.3% | 66% WR | 29 trades
2022: -26.5% | 21% WR | 14 trades
2023: +27.3% | 73% WR | 15 trades
2024: +14.4% | 64% WR | 11 trades
2025: +0.6% | 56% WR | 9 trades
"""
import sys
sys.path.insert(0, "/root/trade_rules")
from lib import ema


def init_strategy():
    return {
        'name': 'rally_exhaustion_pullback_4bar',
        'subscriptions': [
            {'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {'hold_bars': 12}
    }


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

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

    closes = [b.close for b in bars]
    opens = [b.open for b in bars]
    ema20 = ema(closes, 20)

    if ema20[i] is None:
        return []

    actions = []
    has_position = key in positions

    if not has_position:
        # Entry: Rally exhaustion pattern
        # Check for 4 consecutive green bars before current
        consecutive_green = all(closes[j] > opens[j] for j in range(i-4, i))
        if not consecutive_green:
            return []

        # Current bar must be red (first pullback)
        if closes[i] >= opens[i]:
            return []

        # Must be above EMA20 (uptrend context)
        if closes[i] < ema20[i]:
            return []

        # Rally must be at least 4% (from 5 bars ago to 1 bar ago)
        rally_pct = (closes[i-1] - closes[i-5]) / closes[i-5] * 100
        if rally_pct < 4:
            return []

        actions.append({
            'action': 'open_long',
            'symbol': 'tBTCUSD',
            'exchange': 'bitfinex',
            'size': 1.0,
        })
    else:
        # Exit: after 12 bars hold period
        pos = positions[key]
        bars_held = i - pos.entry_bar
        if bars_held >= 12:
            actions.append({
                'action': 'close_long',
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
            })

    return actions