None
Symbol: SOL | Exchange: Binance | Role: defensive
Click a period to view chart
| Period | Return | Win Rate | Trades | Max DD | Sharpe |
|---|
| 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 |
Not yet reviewed. Run: ./review_strategy.sh defensive_ema_pullback_sol
#!/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'}")