← Back to list

fvg_support_bounce VALIDATED PASS

Auto-discovered strategy

Symbol: BTC | Exchange: Bitfinex | Role: momentum

5/6
Profitable Years
+70.9%
Total Return
44.3%
Avg Win Rate
0.62
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +21.3% 42.9% 35 14.7% 0.78
2021 +4.2% 38.2% 34 27.7% 0.15
2022 -18.7% 21.4% 14 18.0% -1.35
2023 +10.0% 47.8% 23 9.2% 0.67
2024 +32.1% 54.2% 24 7.6% 1.60
2025 +22.0% 61.5% 13 4.3% 1.86

Performance Chart

Loading chart...

Walk-Forward Validation PASS

1/1 Windows Profitable
+0.9% OOS Return
0.00 Median Sharpe
0.000 Score
Window Train Period Val Period Val Return Val Test Period Test Return Status
WF-1 2024-01→2025-06 2025-07→2025-12 +0.9% OK 2026-01→ongoing +0.0% PASS

AI Review

Not yet reviewed. Run: ./review_strategy.sh fvg_support_bounce

Source Code

"""
Strategy: FVG Support Bounce
============================

Trades bullish Fair Value Gap (FVG) retests in uptrends.

A Fair Value Gap forms when price moves rapidly, leaving a gap between
bar[i-2].high and bar[i].low. This creates an "inefficiency zone" that
price tends to revisit and use as support. When price retests this zone
and bounces, it confirms buyer strength at that level.

Entry Conditions:
- EMA50 > EMA200 (uptrend regime filter)
- Bullish FVG exists in recent bars (3-15 bars ago)
- FVG gap size is 1-5% (significant but not extreme)
- Current bar's low touches the FVG zone (retest)
- Current bar is bullish (close > open)
- Current bar closes above the FVG (reclaim)
- Strong close in upper 50% of bar range

Exit Conditions:
- Take profit at 5%+ gain
- Trail exit when close < EMA50
- Time exit after 30 bars (~5 days on 4h)
- Regime exit when EMA50 < EMA200
- 5% stop loss

Role: momentum (trend-following with support bounce entries)

Train Performance (2024-01-01 to 2025-06-30):
  2024: +32.1% | 24 trades | 54% WR | Sharpe 1.60 | MaxDD 7.6%
  2025: +19.8% | 7 trades | 71% WR | Sharpe 2.17 | MaxDD 2.6%
  Total: +51.9%
"""

import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema


def init_strategy():
    return {
        'name': 'fvg_support_bounce',
        'role': 'momentum',
        'warmup': 200,
        'subscriptions': [
            {'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {}
    }


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

    actions = []

    # Calculate EMAs for regime filter
    closes = [bars[j].close for j in range(i+1)]
    ema50_vals = ema(closes, 50)
    ema200_vals = ema(closes, 200)

    if ema50_vals[i] is None or ema200_vals[i] is None:
        return []

    # REGIME FILTER: Only trade when EMA50 > EMA200 (uptrend)
    in_uptrend = ema50_vals[i] > ema200_vals[i]

    curr_bar = bars[i]

    if key not in positions:
        if not in_uptrend:
            return []

        # ENTRY: Find bullish FVG that's being retested and reclaimed
        for lookback in range(3, 15):
            if i - lookback < 2:
                continue

            fvg_bar1 = bars[i - lookback - 2]
            fvg_bar3 = bars[i - lookback]

            # Bullish FVG: bar1.high < bar3.low (price gapped up)
            if fvg_bar1.high >= fvg_bar3.low:
                continue

            gap_bottom = fvg_bar1.high
            gap_top = fvg_bar3.low
            gap_pct = (gap_top - gap_bottom) / gap_bottom * 100

            # Gap must be 1-5% (significant but not extreme)
            if not (1 <= gap_pct <= 5):
                continue

            # Current bar's low must touch the FVG zone (retest)
            if not (gap_bottom <= curr_bar.low <= gap_top * 1.02):
                continue

            # Current bar must be bullish
            if curr_bar.close <= curr_bar.open:
                continue

            # Must close above the FVG (reclaim)
            if curr_bar.close < gap_top:
                continue

            # Strong close: in upper 50% of range
            bar_range = curr_bar.high - curr_bar.low
            if bar_range > 0:
                close_pos = (curr_bar.close - curr_bar.low) / bar_range
                if close_pos < 0.5:
                    continue

            state['entry_bar'] = i
            state['entry_price'] = curr_bar.close
            state['fvg_bottom'] = gap_bottom

            actions.append({
                'action': 'open_long',
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
                'size': 1.0,
                'stop_loss_pct': 5,  # 5% hard stop
            })
            break
    else:
        # Exit logic
        entry_bar = state.get('entry_bar', 0)
        entry_price = state.get('entry_price', curr_bar.close)
        bars_held = i - entry_bar

        should_exit = False

        # Take profit at 5%+ gain
        if curr_bar.close >= entry_price * 1.05:
            should_exit = True

        # Trail exit: close below EMA50
        if curr_bar.close < ema50_vals[i]:
            should_exit = True

        # Time exit: max 30 bars (~5 days)
        if bars_held >= 30:
            should_exit = True

        # Regime exit: trend reversal
        if ema50_vals[i] < ema200_vals[i]:
            should_exit = True

        if should_exit:
            actions.append({
                'action': 'close_long',
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
            })

    return actions