← Back to list

sol_low_vol_higher_lows_carry VALIDATED PASS

Auto-discovered strategy

Symbol: SOL | Exchange: Binance | Role: carry

3/6
Profitable Years
+35.9%
Total Return
44.3%
Avg Win Rate
0.20
Avg Sharpe

Year-by-Year Results

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

Performance Chart

Loading chart...

Walk-Forward Validation PASS

1/1 Windows Profitable
+6.8% 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 +6.8% OK 2026-01→ongoing +0.0% PASS

AI Review

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

Source Code

"""
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)