← Back to list

hhhl_swing_structure_sol DRAFT

Auto-discovered strategy

Symbol: SOL | Exchange: Binance | Role: momentum

6/6
Profitable Years
+161.8%
Total Return
40.9%
Avg Win Rate
0.81
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +35.0% 50.0% 14 14.3% 1.25
2021 +14.2% 34.8% 46 48.7% 0.29
2022 +14.2% 34.8% 46 33.7% 0.29
2023 +24.9% 39.4% 33 25.3% 0.61
2024 +15.8% 36.4% 22 24.1% 0.48
2025 +57.7% 50.0% 18 18.5% 1.97

Performance Chart

Loading chart...

Walk-Forward Validation PASS

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

AI Review Score: 78/100

complexity concentration
## Strategy Quality Assessment ### Strengths ✓ **Clean execution model**: Trades on next bar open, no same-candle execution issues ✓ **Proper warmup**: 200 bars matches EMA200 requirement ✓ **Relative indicators only**: Uses EMA ratios, swing patterns, no hardcoded price levels ✓ **Round parameters**: swing_period=3, lookback=40, standard EMA periods (20/50/200) ✓ **Regime awareness**: Uses EMA50/EMA200 crossover to filter trade direction ✓ **Volume confirmation**: 1.2x multiplier adds execution realism ✓ **No lookahead bias**: All calculations use historical data only ✓ **Reasonable risk management**: 5% stop, 10% target = 2:1 R:R ✓ **Role-appropriate metrics**: Momentum role allows for drawdown range observed ### Concerns ⚠ **Complexity (6+ entry conditions)**: The strategy violates the 5-6 condition limit: - LONG requires: bullish_regime + higher_high + higher_low + close > swing_high + close > EMA20 + volume > threshold = **6 conditions** - SHORT requires: bearish_regime + lower_high + lower_low + close < swing_low + close < EMA20 + volume > threshold = **6 conditions** While technically at the boundary, the swing point detection itself adds hidden complexity (nested loops, lookback scanning). This increases overfitting risk. ⚠ **Year-over-year inconsistency**: Returns show concerning concentration: - 2025 generates 57.7% (more than 3x next best year) - First half of 2024 (included in train) shows modest 15.8% - 2021-2022 shows identical 14.2% returns (likely same market regime) This pattern suggests the validation period (second half 2025) may have been particularly favorable for swing breakouts rather than strategy robustness. The 29.96% validation return continuing the 2025 trend is suspicious. ⚠ **Potential concentration risk**: With relatively few trades per year (14-46), a handful of large winners in the strong 2025 period could be driving validation success. The strategy doesn't prevent top-3 trades from dominating PnL. ### Code Quality ✓ Well-documented with clear docstring ✓ Readable logic with descriptive variable names ✓ Proper helper function separation (`find_swing_points`) − Nested loops in `find_swing_points()` could be optimized but functionally correct ### Statistical Validity The strategy generates sufficient trades (minimum 14/year, well above 3-trade threshold) and achieves momentum role thresholds. However, the sharp performance divergence in 2025 raises questions about whether this is capturing a durable edge or fitting to a specific market microstructure that appeared in the latter period. ### Recommendation The strategy passes technical checks but sits at the border of acceptable complexity. The concentration of returns in 2025 (both train and validation portions) suggests potential regime-specific fitting. **Score: 78/100** - Good fundamentals with moderate concerns about complexity and return consistency across market regimes.
Reviewed: 2026-01-14T05:30:43.666483

Source Code

"""
HH/HL Swing Structure Trading Strategy for SOLUSDT

This strategy identifies trend continuation patterns using swing point analysis:
- Higher High (HH) + Higher Low (HL) patterns signal bullish continuation
- Lower High (LH) + Lower Low (LL) patterns signal bearish continuation

Entry Logic:
- LONG: HH+HL pattern in bullish regime (EMA50 > EMA200), breakout above swing high
- SHORT: LH+LL pattern in bearish regime (EMA50 < EMA200), breakdown below swing low

Exit Logic:
- Close position when regime flips or price breaks EMA50
- Fixed 5% stop loss, 10% take profit (2:1 R:R)

Key Design Principles:
- Regime filter prevents trading against major trend
- Swing point detection uses 3-bar pivots (round parameter)
- 40-bar lookback for swing detection (round parameter)
- Volume confirmation (1.2x average) filters false breakouts
- No specific price levels - all relative calculations
"""

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


def init_strategy():
    return {
        'name': 'hhhl_swing_structure_sol',
        'role': 'momentum',
        'warmup': 200,
        'subscriptions': [
            {'symbol': 'SOLUSDT', 'exchange': 'binance', 'timeframe': '4h'},
        ],
        'parameters': {
            'swing_period': 3,      # bars on each side for swing detection
            'lookback': 40,         # bars to find swing points
            'volume_mult': 1.2,     # volume confirmation multiplier
            'stop_loss_pct': 5.0,   # stop loss percentage
            'take_profit_pct': 10.0 # take profit percentage (2:1 R:R)
        }
    }


def find_swing_points(bars, i, lookback=40, swing_period=3):
    """
    Find swing highs and lows in the lookback period.

    A swing high is a bar whose high is >= all bars within swing_period on each side.
    A swing low is a bar whose low is <= all bars within swing_period on each side.

    Returns:
        (swing_highs, swing_lows) - lists of (bar_index, price) tuples
    """
    swing_highs = []
    swing_lows = []

    for j in range(i - lookback, i - swing_period):
        if j < swing_period:
            continue

        # Check if this is a swing high
        is_swing_high = all(
            bars[j].high >= bars[k].high
            for k in range(j - swing_period, j + swing_period + 1)
            if k != j and 0 <= k < len(bars)
        )
        if is_swing_high:
            swing_highs.append((j, bars[j].high))

        # Check if this is a swing low
        is_swing_low = all(
            bars[j].low <= bars[k].low
            for k in range(j - swing_period, j + swing_period + 1)
            if k != j and 0 <= k < len(bars)
        )
        if is_swing_low:
            swing_lows.append((j, bars[j].low))

    return swing_highs, swing_lows


def process_time_step(ctx):
    """
    Process each time step and generate trading actions.

    Looks for HH/HL patterns in bullish regime for longs,
    and LH/LL patterns in bearish regime for shorts.
    """
    key = ('SOLUSDT', 'binance')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']

    actions = []

    # Calculate indicators
    closes = [b.close for b in bars]
    highs = [b.high for b in bars]
    lows = [b.low for b in bars]
    volumes = [b.volume for b in bars]

    ema20 = ema(closes, 20)
    ema50 = ema(closes, 50)
    ema200 = ema(closes, 200)
    vol_sma = sma(volumes, 20)

    # Check indicator validity
    if not ema50[i] or not ema200[i] or not ema20[i] or not vol_sma[i]:
        return []

    # Determine regime
    bullish_regime = ema50[i] > ema200[i]
    bearish_regime = ema50[i] < ema200[i]

    # Find swing points
    swing_highs, swing_lows = find_swing_points(
        bars, i,
        lookback=params['lookback'],
        swing_period=params['swing_period']
    )

    # Need at least 2 swing highs and 2 swing lows for pattern
    if len(swing_highs) < 2 or len(swing_lows) < 2:
        return []

    # Get most recent swing points (sorted by time)
    recent_highs = sorted(swing_highs, key=lambda x: x[0])[-2:]
    recent_lows = sorted(swing_lows, key=lambda x: x[0])[-2:]

    # Detect patterns
    higher_high = recent_highs[-1][1] > recent_highs[-2][1]
    higher_low = recent_lows[-1][1] > recent_lows[-2][1]
    lower_high = recent_highs[-1][1] < recent_highs[-2][1]
    lower_low = recent_lows[-1][1] < recent_lows[-2][1]

    latest_swing_high = recent_highs[-1][1]
    latest_swing_low = recent_lows[-1][1]

    bar = bars[i]
    volume_mult = params['volume_mult']

    if key not in positions:
        # LONG ENTRY:
        # 1. Bullish regime (EMA50 > EMA200)
        # 2. Higher High AND Higher Low pattern
        # 3. Price breaks above latest swing high
        # 4. Price above EMA20 (short-term bullish)
        # 5. Volume above average * multiplier

        if (bullish_regime and
            higher_high and higher_low and
            bar.close > latest_swing_high and
            bar.close > ema20[i] and
            volumes[i] > vol_sma[i] * volume_mult):

            actions.append({
                'action': 'open_long',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
                'size': 1.0,
                'stop_loss_pct': params['stop_loss_pct'],
                'take_profit_pct': params['take_profit_pct'],
            })

        # SHORT ENTRY:
        # 1. Bearish regime (EMA50 < EMA200)
        # 2. Lower High AND Lower Low pattern
        # 3. Price breaks below latest swing low
        # 4. Price below EMA20 (short-term bearish)
        # 5. Volume above average * multiplier

        elif (bearish_regime and
              lower_high and lower_low and
              bar.close < latest_swing_low and
              bar.close < ema20[i] and
              volumes[i] > vol_sma[i] * volume_mult):

            actions.append({
                'action': 'open_short',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
                'size': 1.0,
                'stop_loss_pct': params['stop_loss_pct'],
                'take_profit_pct': params['take_profit_pct'],
            })

    else:
        pos = positions[key]

        if pos.side == 'long':
            # Exit long if:
            # - Regime flips bearish (EMA50 < EMA200)
            # - Price closes below EMA50 (trend weakening)
            if not bullish_regime or bar.close < ema50[i]:
                actions.append({
                    'action': 'close_long',
                    'symbol': 'SOLUSDT',
                    'exchange': 'binance',
                })

        elif pos.side == 'short':
            # Exit short if:
            # - Regime flips bullish (EMA50 > EMA200)
            # - Price closes above EMA50 (trend weakening)
            if not bearish_regime or bar.close > ema50[i]:
                actions.append({
                    'action': 'close_short',
                    'symbol': 'SOLUSDT',
                    'exchange': 'binance',
                })

    return actions


if __name__ == '__main__':
    from strategy import backtest_strategy
    results, profitable, _ = backtest_strategy(init_strategy, process_time_step)
    print(f"\nProfitable years: {profitable}")