← Back to list

defensive_ema_pullback_sol VALIDATED FAIL

None

Symbol: SOL | Exchange: Binance | Role: defensive

7/6
Profitable Years
+40.0%
Total Return
0.0%
Avg Win Rate
0.00
Avg Sharpe

Monthly Results

Click a period to view chart

Period Return Win Rate Trades Max DD Sharpe

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

0/1 Windows Profitable
-8.1% OOS Return
0.00 Median Sharpe
0.000 Score
Window Train Period Val Period Val Return Val Test Period Test Return Status
WF-1 2025-01→2025-09 2025-10→2025-12 -8.1% FAIL 2026-01→ongoing +0.0% FAIL

AI Review

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

Source Code

#!/usr/bin/env python3
"""
Defensive Strategy: EMA Pullback in Strong Uptrend - SOLUSDT

Role: defensive
Goal: Capital preservation - DO NOT LOSE MONEY

Strategy Logic:
1. Only trade in CALM volatility (ATR z-score < 1.0)
2. Only trade in STRONG uptrend (EMA10 > EMA20 > EMA50)
3. Enter on pullback: when low touches EMA(20) area
4. Quick exit: at EMA(10) or after 8 bars

Key Features:
- Volatility regime filter prevents trading in chaos
- Trend alignment reduces adverse selection
- Quick exits lock in gains and limit losses
- Simple logic with few parameters (no curve fitting)

TRAIN PERIOD RESULTS (2025-01 to 2025-09):
- Total Return: +40.0%
- Max Drawdown: 10.3% (< 12% requirement)
- Profitable Months: 7/9
- Win Rate: ~56%

DEFENSIVE ROLE VALIDATION:
- Max DD < 12%: PASS (10.3%)
- Return >= 0%: PASS (+40.0%)
- Min Trades >= 5: PASS

References:
- ATR regime filtering: https://www.mindmathmoney.com/articles/atr-indicator-trading-strategy
- EMA pullback: https://capital.com/en-int/analysis/day-traders-toolbox-part-3-average-true-range-atr
"""

import sys
sys.path.insert(0, '/root/trade_15m')

from lib import ema, atr
from math import sqrt


def init_strategy():
    """Initialize the defensive EMA pullback strategy."""
    return {
        'name': 'defensive_ema_pullback_sol',
        'role': 'defensive',
        'warmup': 100,
        'subscriptions': [
            {'symbol': 'SOLUSDT', 'exchange': 'binance', 'timeframe': '15m'},
        ],
        'parameters': {
            'ema_fast': 10,
            'ema_mid': 20,
            'ema_slow': 50,
            'atr_period': 20,
            'atr_z_lookback': 60,
            'max_atr_z': 1.0,
            'max_hold_bars': 8,
            'stop_loss_pct': 1.5,
            'take_profit_pct': 2.0,
        }
    }


def process_time_step(ctx):
    """
    Process each 15-minute bar.
    
    Entry: Long only on EMA(20) pullback in uptrend with calm volatility.
    Exit: At EMA(10), max hold time, or chaos regime.
    """
    key = ('SOLUSDT', 'binance')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']

    actions = []

    # Need warmup for indicators
    if i < 80:
        return actions

    # Extract price data
    closes = [b.close for b in bars]
    highs = [b.high for b in bars]
    lows = [b.low for b in bars]

    current_close = closes[i]
    current_low = lows[i]

    # Calculate EMAs
    ema_fast = ema(closes, params['ema_fast'])
    ema_mid = ema(closes, params['ema_mid'])
    ema_slow = ema(closes, params['ema_slow'])

    e10 = ema_fast[i] if ema_fast[i] else current_close
    e20 = ema_mid[i] if ema_mid[i] else current_close
    e50 = ema_slow[i] if ema_slow[i] else current_close

    # Calculate ATR
    atr_values = atr(highs, lows, closes, params['atr_period'])
    current_atr = atr_values[i] if atr_values[i] else 0

    if current_atr == 0:
        return actions

    # Calculate ATR z-score for volatility regime detection
    atr_z_lookback = params['atr_z_lookback']
    atr_window = [atr_values[j] for j in range(max(0, i - atr_z_lookback), i + 1)
                  if atr_values[j] is not None]

    if len(atr_window) < 30:
        return actions

    atr_mean = sum(atr_window) / len(atr_window)
    atr_var = sum((a - atr_mean) ** 2 for a in atr_window) / len(atr_window)
    atr_std = sqrt(atr_var) if atr_var > 0 else 0.001
    atr_z = (current_atr - atr_mean) / atr_std

    # =========================================================================
    # POSITION MANAGEMENT
    # =========================================================================

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

        # DEFENSIVE EXIT 1: Chaos regime - exit immediately
        if atr_z > 1.5:
            actions.append({
                'action': 'close_long',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
            })
            return actions

        # DEFENSIVE EXIT 2: Max hold time exceeded
        if bars_held >= params['max_hold_bars']:
            actions.append({
                'action': 'close_long',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
            })
            return actions

        # PROFIT EXIT: Price reached EMA(10) or higher
        if current_close >= e10:
            actions.append({
                'action': 'close_long',
                'symbol': 'SOLUSDT',
                'exchange': 'binance',
            })

        return actions

    # =========================================================================
    # ENTRY LOGIC - VERY STRICT FILTERS
    # =========================================================================

    # FILTER 1: Calm volatility regime only (ATR z-score < max_atr_z)
    if atr_z > params['max_atr_z']:
        return actions

    # FILTER 2: Strong uptrend - EMA alignment
    # EMA(10) > EMA(20) > EMA(50)
    if not (e10 > e20 > e50):
        return actions

    # FILTER 3: Pullback condition - low touched EMA(20) area
    # Allow 0.5% tolerance on either side
    ema20_touch = current_low <= e20 * 1.005 and current_low >= e20 * 0.995

    # FILTER 4: Close still above EMA(20) - didn't break down
    close_above_ema20 = current_close > e20

    # All conditions met - enter long
    if ema20_touch and close_above_ema20:
        actions.append({
            'action': 'open_long',
            'symbol': 'SOLUSDT',
            'exchange': 'binance',
            'size': 0.5,  # Half size for defensive
            'stop_loss_pct': params['stop_loss_pct'],
            'take_profit_pct': params['take_profit_pct'],
        })

    return actions


if __name__ == '__main__':
    """Run backtest when executed directly."""
    import sys
    sys.path.insert(0, '/root/trade_15m')
    from strategy import backtest_strategy

    print("\n" + "=" * 60)
    print("DEFENSIVE: EMA Pullback in Uptrend - SOLUSDT")
    print("=" * 60)
    print("\nLong only in uptrend with calm volatility filter.")
    print()

    results, profitable, _ = backtest_strategy(init_strategy, process_time_step)

    total_trades = sum(r.get('trades', 0) for r in results.values())
    total_return = sum(r.get('return', 0) for r in results.values())
    max_dd = max(r.get('max_dd', 0) for r in results.values()) if results else 0

    print(f"\n  Aggregate Stats:")
    print(f"  Total Trades: {total_trades}")
    print(f"  Total Return: {total_return:+.2f}%")
    print(f"  Max Drawdown: {max_dd:.2f}%")
    print(f"  Profitable Months: {profitable}/9")

    print("\n  Defensive Role Validation:")
    print(f"  Max DD < 12%: {'PASS' if max_dd < 12 else 'FAIL'} ({max_dd:.1f}%)")
    print(f"  Return >= 0%: {'PASS' if total_return >= 0 else 'FAIL'} ({total_return:+.1f}%)")
    print(f"  Trades >= 5:  {'PASS' if total_trades >= 5 else 'FAIL'} ({total_trades})")

    passes = max_dd < 12 and total_return >= 0 and total_trades >= 5
    print(f"\n  {'=' * 50}")
    print(f"  TRAIN RESULT: {'PASS - Ready for validation' if passes else 'FAIL - Needs tuning'}")