Auto-discovered strategy
Symbol: SOL | Exchange: Binance | Role: momentum
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 |
| 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 |
Not yet reviewed. Run: ./review_strategy.sh sol_uptrend_dip_fill
"""
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)