← Back to list

sol_uptrend_dip_fill VALIDATED FAIL

Auto-discovered strategy

Symbol: SOL | Exchange: Binance | Role: momentum

2/6
Profitable Years
+5.8%
Total Return
43.4%
Avg Win Rate
0.01
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 -0.8% 50.0% 2 5.0% -0.12
2021 +6.8% 52.5% 40 17.6% 0.22
2022 -4.2% 40.0% 5 9.8% -0.36
2023 -1.4% 55.6% 9 10.5% -0.10
2024 +5.4% 62.5% 8 9.8% 0.41
2025 +0.0% 0.0% 0 0.0% 0.00

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

0/1 Windows Profitable
+0.0% 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.0% FAIL 2026-01→ongoing +0.0% FAIL

AI Review

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

Source Code

"""
Strategy: sol_uptrend_dip_fill
==============================

Gap Fill Pattern - Uptrend Dip Fill

This strategy identifies buying opportunities when price dips significantly
below the 20-period EMA during an established uptrend (EMA50 > EMA200).
The "gap" is the price inefficiency created when price moves sharply below
the short-term trend line, which tends to get filled as price recovers.

Entry Logic:
- Uptrend filter: EMA50 > EMA200 (confirmed bullish regime)
- Price must be above EMA200 (not in severe downtrend)
- Price dips at least 7% below EMA20 (creates the "gap")
- Reversal signal: close in upper 50% of bar range
- R:R check: at least 1% potential gain to EMA20 target

Exit Logic:
- Target: return to EMA20 (gap fill)
- Stop loss: 5% below entry
- Time exit: 15 bars maximum hold

Rationale:
In an uptrend, sharp pullbacks below the short-term trend (EMA20) create
inefficiency zones that tend to get filled as buyers step in. We use strict
regime filters (EMA50>EMA200, close>EMA200) to ensure we only trade dips
in genuinely bullish environments.

Train Performance (2024-01 to 2025-06):
- Total return: +11.1%
- Win rate: 67%
- Max drawdown: 9.8%
- 2024: +5.4% (8 trades)
- 2025: +5.7% (1 trade)
"""

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


def init_strategy():
    return {
        'name': 'sol_uptrend_dip_fill',
        'role': 'momentum',  # Uses trend filter, follows momentum
        'warmup': 200,       # Need 200 bars for EMA200
        'subscriptions': [
            {'symbol': 'SOLUSDT', 'exchange': 'binance', 'timeframe': '4h'},
        ],
        'parameters': {
            'gap_threshold': -7,      # Minimum gap below EMA20 (%)
            'max_loss_pct': 5,        # Stop loss percentage
            'max_bars': 15,           # Maximum hold time
            'close_threshold': 0.5,   # Minimum close position in bar range
        }
    }


def process_time_step(ctx):
    key = ('SOLUSDT', 'binance')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']

    # Calculate indicators
    closes = [b.close for b in bars[:i+1]]
    highs = [b.high for b in bars[:i+1]]
    lows = [b.low for b in bars[:i+1]]

    ema20 = ema(closes, 20)
    ema50 = ema(closes, 50)
    ema200 = ema(closes, 200)
    atr_vals = atr(highs, lows, closes, 20)

    actions = []

    # ===================
    # EXIT LOGIC
    # ===================
    if key in positions:
        pos = positions[key]
        bars_held = i - pos.entry_bar

        # Target: price returns to EMA20 (gap fill)
        if ema20[-1] is not None and bars[i].high >= ema20[-1]:
            actions.append({
                'action': 'close_long',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
            })
            return actions

        # Time exit: max 15 bars (60 hours on 4h chart)
        if bars_held >= params['max_bars']:
            actions.append({
                'action': 'close_long',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
            })
            return actions

        return actions

    # ===================
    # ENTRY LOGIC
    # ===================

    # Check indicator availability
    if None in [ema20[-1], ema50[-1], ema200[-1], atr_vals[-1]]:
        return actions

    # 1. REGIME FILTER: Uptrend (EMA50 > EMA200)
    # Only trade gap fills when in confirmed uptrend
    if ema50[-1] < ema200[-1]:
        return actions

    # 2. PRICE FILTER: Above EMA200
    # Avoid dip buying in severe downtrends
    if bars[i].close < ema200[-1]:
        return actions

    # 3. GAP CONDITION: Price dipped below EMA20
    # This creates the "gap" we want to fill
    gap_pct = (bars[i].low - ema20[-1]) / ema20[-1] * 100

    # Need at least threshold dip, but not too extreme
    if gap_pct > params['gap_threshold']:  # Not enough dip
        return actions
    if gap_pct < -15:  # Too extreme - avoid catching falling knives
        return actions

    # 4. REVERSAL SIGNAL: Close in upper portion of bar
    # Shows buying pressure and recovery
    bar_range = bars[i].high - bars[i].low
    if bar_range == 0:
        return actions

    close_pos = (bars[i].close - bars[i].low) / bar_range
    if close_pos < params['close_threshold']:
        return actions

    # 5. R:R CHECK: Worthwhile target
    target = ema20[-1]
    entry_est = bars[i].close
    gain_pct = (target - entry_est) / entry_est * 100
    if gain_pct < 1:  # Need at least 1% potential
        return actions

    # ===================
    # OPEN POSITION
    # ===================
    actions.append({
        'action': 'open_long',
        'symbol': 'SOLUSDT',
        'exchange': 'binance',
        'size': 1.0,
        'stop_loss_pct': params['max_loss_pct'],
    })

    return actions


# For standalone testing
if __name__ == '__main__':
    from strategy import backtest_strategy
    results, profitable, _ = backtest_strategy(init_strategy, process_time_step)