Auto-discovered strategy
Symbol: SOL | Exchange: Binance | Role: carry
Click a year to view chart
| Year | Return | Win Rate | Trades | Max DD | Sharpe |
|---|---|---|---|---|---|
| 2020 | -3.0% | 33.3% | 3 | 5.9% | -0.61 |
| 2021 | +16.4% | 60.0% | 25 | 15.8% | 1.18 |
| 2022 | -12.5% | 14.3% | 7 | 12.0% | -2.30 |
| 2023 | -0.8% | 41.9% | 31 | 12.1% | -0.06 |
| 2024 | +15.0% | 53.8% | 26 | 8.9% | 1.08 |
| 2025 | +20.9% | 62.5% | 24 | 5.7% | 1.92 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | +6.8% | OK | 2026-01→ongoing | +0.0% | PASS |
Not yet reviewed. Run: ./review_strategy.sh sol_low_vol_higher_lows_carry
"""
Strategy: sol_low_vol_higher_lows_carry
=======================================
Carry-style strategy for SOLUSDT that captures low-volatility upward drift.
Core Concept:
During established uptrends, SOL often consolidates with consecutive higher lows
before continuing higher. This pattern indicates orderly accumulation with bounded
risk - ideal for carry-style trading that prioritizes capital preservation.
Entry Conditions (all must be true):
1. Strong uptrend: EMA20 > EMA50 > EMA200 (full cascade)
2. Price in consolidation zone: between EMA20 and EMA50, or just above EMA20
3. 2 consecutive higher lows (orderly accumulation)
4. Low volatility: average range of last 2 bars < 80% of ATR(14)
Exit Conditions (any triggers exit):
1. Stop loss: 3% (tight for carry role)
2. Take profit: 3% (quick profits)
3. Close below EMA20 (trend weakening)
4. Max hold: 10 bars (bounded exposure)
Why This is a Carry Strategy:
- Bounded risk: 3% stop, 8.9% max DD in training
- Short duration: avg ~4 bars held
- Targets low-volatility regimes (calm market = steady gains)
- Prioritizes capital preservation over maximum gains
- Works by "harvesting" the natural upward drift in quiet uptrends
Role: carry
Validation gates: max_loss=-5%, max_dd=15%, min_sharpe=-0.2, min_trades=3
Train results (2024-01-01 to 2025-06-30):
- 2024: +15.0%, 26 trades, 54% WR, 8.9% DD
- 2025H1: +2.8%, 8 trades, 50% WR
- Total: +17.8%, Max DD 8.9%
"""
import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema, atr
def init_strategy():
return {
'name': 'sol_low_vol_higher_lows_carry',
'role': 'carry',
'warmup': 200,
'subscriptions': [
{'symbol': 'SOLUSDT', 'exchange': 'binance', 'timeframe': '4h'},
],
'parameters': {
'consec_hl': 2, # 2 consecutive higher lows
'atr_mult': 0.8, # Low volatility filter (range < 80% of ATR)
'max_hold': 10, # Max 10 bars
'sl_pct': 3.0, # 3% stop loss
'tp_pct': 3.0, # 3% take profit
}
}
def process_time_step(ctx):
key = ('SOLUSDT', 'binance')
bars = ctx['bars'].get(key, [])
i = ctx['i']
positions = ctx['positions']
params = ctx['parameters']
if i >= len(bars):
return []
bar = bars[i]
# Pre-compute indicators
closes = [b.close for b in bars[:i+1]]
highs = [b.high for b in bars[:i+1]]
lows_list = [b.low for b in bars[:i+1]]
ema20_vals = ema(closes, 20)
ema50_vals = ema(closes, 50)
ema200_vals = ema(closes, 200)
atr_vals = atr(highs, lows_list, closes, 14)
ema20 = ema20_vals[i] if ema20_vals[i] else 0
ema50 = ema50_vals[i] if ema50_vals[i] else 0
ema200 = ema200_vals[i] if ema200_vals[i] else 0
current_atr = atr_vals[i] if atr_vals[i] else 0
actions = []
has_position = key in positions
if not has_position:
# Entry conditions
# 1. Strong uptrend: EMA cascade (EMA20 > EMA50 > EMA200)
if not (ema20 > ema50 > ema200 > 0):
return []
# 2. Price in consolidation zone (between EMA20 and EMA50, or just above EMA20)
price_in_zone = (ema20 > bar.close > ema50) or (bar.close > ema20 and bar.close < ema20 * 1.02)
if not price_in_zone:
return []
# 3. Consecutive higher lows
consec_hl = int(params['consec_hl'])
all_hl = True
for j in range(1, consec_hl + 1):
if i - j < 0:
all_hl = False
break
if bars[i - j + 1].low <= bars[i - j].low:
all_hl = False
break
if not all_hl:
return []
# 4. Low volatility: average range < ATR * threshold
if current_atr == 0:
return []
avg_range = sum(bars[i-k].high - bars[i-k].low for k in range(consec_hl)) / consec_hl
if avg_range > current_atr * params['atr_mult']:
return []
# All conditions met - enter long
actions.append({
'action': 'open_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
'size': 1.0,
'stop_loss_pct': params['sl_pct'],
'take_profit_pct': params['tp_pct'],
})
else:
# Exit conditions
pos = positions[key]
bars_held = i - pos.entry_bar
should_exit = False
# Exit 1: Close below EMA20 (trend weakening)
if ema20 > 0 and bar.close < ema20 and bars_held >= 2:
should_exit = True
# Exit 2: Max hold period
elif bars_held >= params['max_hold']:
should_exit = True
if should_exit:
actions.append({
'action': 'close_long',
'symbol': 'SOLUSDT',
'exchange': 'binance',
})
return actions
if __name__ == '__main__':
from strategy import backtest_strategy, validate_new_strategy
print("Backtesting sol_low_vol_higher_lows_carry strategy...")
results, profitable, _ = backtest_strategy(init_strategy, process_time_step)
print(f"\nSummary:")
print(f" Profitable years: {profitable}/2")
total_ret = sum(r['return'] for r in results.values())
max_dd = max(r['max_dd'] for r in results.values())
print(f" Total return: {total_ret:.1f}%")
print(f" Max drawdown: {max_dd:.1f}%")
print("\n" + "="*60)
print("Running validation on unseen data...")
validate_new_strategy(init_strategy, process_time_step)