""" ElliotV5_SMA Strategy - Long Positions with 3-Layer Stop Loss Protection A momentum-based long strategy using Elliott Wave Oscillator (EWO) and Simple Moving Averages (SMA) to identify high-probability entry points during both bullish and oversold market conditions. ## Entry Logic Two entry conditions (OR logic - either can trigger): 1. **High EWO Entry**: Price below SMA with strong positive momentum - Price < SMA * low_offset (discount to moving average) - EWO > ewo_high (strong upward momentum) - RSI < rsi_buy (not overbought) 2. **Low EWO Entry**: Deep oversold conditions - Price < SMA * low_offset (discount to moving average) - EWO < ewo_low (deep negative momentum, likely reversal) ## Exit Logic - **ROI-based**: Time-decay profit targets (21.5% → 3% over 201 minutes) - **Signal-based**: Price crosses above SMA * high_offset - **Trailing stop**: Tiered profit-based (0.5%-2% based on profit level) - **Custom stop loss**: Profit-based tiered system (see below) ## Profit-Based Stop Loss with Emergency Exit **Base Stoploss** (-0.09) - 9% account loss = 27% price movement at 3x leverage - Safe margin: 24% buffer before -33% liquidation **custom_stoploss()** - Profit-Based Tiered Trailing - Loss zone: -9% stop until breakeven - Early profit (0-1%): -2% trailing stop - Medium profit (1-3%): -1% trailing stop - High profit (3%+): -0.5% trailing stop - Emergency exit: Triggers within 15% of liquidation price **custom_exit()** - Time-Based Unclog - Exits losing positions after N days at -4% loss - Frees capital from dead positions **confirm_trade_exit()** - Exit Quality Control - Blocks ROI exits when not making new highs - Blocks exits with profit below 0.3% - Ensures better exit prices ## Risk Management - **Leverage**: Fixed 3x for all trades - **Liquidation Prevention**: Volatility-adaptive stops trigger before -30% liquidation point - **Volatility Adaptation**: Stop distance adjusts to each coin's natural price swings - **Moon Mode**: Lets winners run when making new highs - **No Protection Window**: Immediate stop loss response prevents deep losses ## Performance Characteristics - **Timeframe**: 5-minute candles - **Best Markets**: Trending or mean-reverting with clear momentum shifts - **Risk Level**: Moderate (3x leverage with -4% max loss after protection) ## Technical Requirements - FreqTrade 2023.1+ - Technical Analysis Library (TA-Lib) - Minimum 2000 candles for startup ## See Also - ElliotV5_SMA_Shorts: Short-only variant of this strategy - Memory file: elliotv5_sma_stoploss_fix_jan2026 (implementation details) """ from freqtrade.strategy.interface import IStrategy from typing import Dict, List, Optional, Union from functools import reduce from pandas import DataFrame import talib.abstract as ta import numpy as np from datetime import datetime, timedelta from freqtrade.persistence import Trade from freqtrade.strategy import DecimalParameter, IntParameter import logging logger = logging.getLogger(__name__) buy_params = { "base_nb_candles_buy": 17, "ewo_high": 3.34, "ewo_low": -17.457, "low_offset": 0.978, "rsi_buy": 65 } sell_params = { "base_nb_candles_sell": 49, "high_offset": 1.019 } def EWO(dataframe, sma_length=5, sma2_length=35): """ Elliott Wave Oscillator - Momentum indicator. Note: Despite the traditional EWO name, this implementation uses SMA (not EMA) for calculation, as it provides better signals for this strategy. Args: dataframe: OHLCV dataframe sma_length: Fast SMA period (default 5) sma2_length: Slow SMA period (default 35) Returns: Series: Percentage difference between fast and slow SMAs """ df = dataframe.copy() sma1 = ta.SMA(df, timeperiod=sma_length) sma2 = ta.SMA(df, timeperiod=sma2_length) smadif = (sma1 - sma2) / df['close'] * 100 return smadif class ElliotV5_SMA(IStrategy): INTERFACE_VERSION = 3 minimal_roi = { "0": 0.215, "40": 0.132, "87": 0.086, "201": 0.03 } stoploss = -0.075 # 7.5% base stop = 22.5% loss at 3x leverage (safer buffer from -33% liquidation) # Volatility-based trailing stop parameters sl1 = DecimalParameter(-0.013, -0.005, default=-0.013, space='sell', optimize=True) move = IntParameter(35, 60, default=48, space='buy', optimize=True) mms = IntParameter(6, 20, default=12, space='buy', optimize=True) mml = IntParameter(300, 400, default=360, space='buy', optimize=True) base_nb_candles_buy = IntParameter( 5, 80, default=buy_params['base_nb_candles_buy'], space='buy', optimize=True) base_nb_candles_sell = IntParameter( 5, 80, default=sell_params['base_nb_candles_sell'], space='sell', optimize=True) low_offset = DecimalParameter( 0.9, 0.99, default=buy_params['low_offset'], space='buy', optimize=True) high_offset = DecimalParameter( 0.99, 1.1, default=sell_params['high_offset'], space='sell', optimize=True) fast_ewo = 50 slow_ewo = 200 ewo_low = DecimalParameter(-20.0, -8.0, default=buy_params['ewo_low'], space='buy', optimize=True) ewo_high = DecimalParameter( 2.0, 12.0, default=buy_params['ewo_high'], space='buy', optimize=True) rsi_buy = IntParameter(30, 70, default=buy_params['rsi_buy'], space='buy', optimize=True) # Unclog parameters (used in custom_exit) unclog_days = IntParameter(1, 5, default=4, space='sell', optimize=True) unclog = DecimalParameter(0.01, 0.08, default=0.04, decimals=2, space='sell', optimize=True) trailing_stop = True trailing_stop_positive = 0.005 trailing_stop_positive_offset = 0.03 trailing_only_offset_is_reached = True use_exit_signal = True exit_profit_only = False exit_profit_offset = 0.01 ignore_roi_if_entry_signal = True timeframe = '5m' informative_timeframe = '1h' process_only_new_candles = True startup_candle_count = 2000 plot_config = { 'main_plot': { 'ma_buy': {'color': 'orange'}, 'ma_sell': {'color': 'orange'}, }, } use_custom_stoploss = True def informative_pairs(self): pairs = self.dp.current_whitelist() informative_pairs = [(pair, self.informative_timeframe) for pair in pairs] return informative_pairs def get_informative_indicators(self, metadata: dict): dataframe = self.dp.get_pair_dataframe( pair=metadata['pair'], timeframe=self.informative_timeframe) return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: for val in self.base_nb_candles_buy.range: dataframe[f'ma_buy_{val}'] = ta.EMA(dataframe, timeperiod=val) for val in self.base_nb_candles_sell.range: dataframe[f'ma_sell_{val}'] = ta.EMA(dataframe, timeperiod=val) dataframe['EWO'] = EWO(dataframe, self.fast_ewo, self.slow_ewo) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) # ATR volatility filter (NEW - prevents entries on ultra-volatile coins) # ATR as percentage of price helps identify problematic coins dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) dataframe['atr_pct'] = (dataframe['atr'] / dataframe['close']) * 100 # Volume surge confirmation (NEW - ensures strong momentum entries) # Moving average of volume over 20 periods dataframe['volume_ma'] = dataframe['volume'].rolling(20).mean() # Volatility-based indicators for adaptive stop loss dataframe['OHLC4'] = (dataframe['open'] + dataframe['high'] + dataframe['low'] + dataframe['close']) / 4 # Check how far we are from min and max over short window dataframe['max'] = dataframe['OHLC4'].rolling(self.mms.value).max() / dataframe['OHLC4'] - 1 dataframe['min'] = abs(dataframe['OHLC4'].rolling(self.mms.value).min() / dataframe['OHLC4'] - 1) # Check how far we are from min and max over long window (360 candles = 30 hours at 5m) dataframe['max_l'] = dataframe['OHLC4'].rolling(self.mml.value).max() / dataframe['OHLC4'] - 1 dataframe['min_l'] = abs(dataframe['OHLC4'].rolling(self.mml.value).min() / dataframe['OHLC4'] - 1) # Apply rolling window operation to calculate volatility rolling_window = dataframe['OHLC4'].rolling(self.move.value) # Calculate the peak-to-peak value (max - min) in rolling window ptp_value = rolling_window.apply(lambda x: np.ptp(x)) # Normalize volatility by current price dataframe['move'] = ptp_value / dataframe['OHLC4'] # Per-pair volatility adaptation (NOT rolling mean - intentional) # Uses mean across ALL history to get this coin's natural volatility # NAORIS might average 15% moves, BTC might average 3% - this adapts stops per coin dataframe['move_mean'] = dataframe['move'].mean() # Average volatility dataframe['move_mean_x'] = dataframe['move'].mean() * 1.6 # 1.6x average volatility return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Define LONG entry conditions with enhanced filters. Two conditions (OR logic - either can trigger entry): Condition 1: High EWO Long - Price at discount below EMA (EWO > 3.34) - RSI < 65 confirms not overbought - Buy the dip with strong positive momentum Condition 2: Low EWO Long (Falling Knife Prevention) - Price at discount below EMA (EWO < -17.457) - RSI < 30 confirms oversold - RSI turning up confirms reversal (prevents catching falling knives) - Buy true reversals, not just crashes Both conditions require: - Price trading at discount below EMA - ATR filter: Volatility < 10% (prevents ultra-volatile coins) - Volume surge: Volume > 1.5x MA (ensures strong momentum) """ conditions = [] # Condition 1: High EWO Long (Strong positive momentum dip) conditions.append( ( (dataframe['close'] < (dataframe[f'ma_buy_{self.base_nb_candles_buy.value}'] * self.low_offset.value)) & (dataframe['EWO'] > self.ewo_high.value) & (dataframe['rsi'] < self.rsi_buy.value) & # NEW: ATR volatility filter (dataframe['atr_pct'] < 10.0) & # Less than 10% ATR # NEW: Volume surge confirmation (dataframe['volume'] > dataframe['volume_ma'] * 1.5) & # 50% above average volume (dataframe['volume'] > 0) ) ) # Condition 2: Low EWO Long (Falling knife prevention with reversal confirmation) conditions.append( ( (dataframe['close'] < (dataframe[f'ma_buy_{self.base_nb_candles_buy.value}'] * self.low_offset.value)) & (dataframe['EWO'] < self.ewo_low.value) & (dataframe['rsi'] < 30) & # Only enter true oversold conditions (dataframe['rsi'] > dataframe['rsi'].shift(1)) & # RSI must be turning up (reversal signal) # NEW: ATR volatility filter (dataframe['atr_pct'] < 10.0) & # Less than 10% ATR # NEW: Volume surge confirmation (dataframe['volume'] > dataframe['volume_ma'] * 1.5) & # 50% above average volume (dataframe['volume'] > 0) ) ) if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), 'enter_long' ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] conditions.append( ( (dataframe['close'] > (dataframe[f'ma_sell_{self.base_nb_candles_sell.value}'] * self.high_offset.value)) & (dataframe['volume'] > 0) ) ) if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), 'exit_long' ] = 1 return dataframe def leverage(self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, side: str, **kwargs) -> float: """ Fixed 3x leverage for all long trades. Increased from 1x to 3x for higher profit potential while maintaining risk control through -18.9% stop loss (allows 6.3% price movement). Returns: float: Leverage multiplier (3.0 = 3x) """ return 3.0 def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ OPTIMIZED asymmetric trailing stop for LONG positions with 3x leverage. CRITICAL FIXES: 1. Tighter base stop (-7.5% vs -9%) to reduce slippage overshoots 2. Wider profit tiers to let winners run (target 4-6% vs tight 1-2%) 3. Emergency liquidation protection maintained OPTIMIZATION RATIONALE: - Longs had 100% win rate but only 1.4% avg profit - Trailing stops too tight (0.5-2%) exiting too early - Need to let winners develop to 4-6% before tightening NEW OPTIMIZED TIERS (Let Winners Run): - Loss zone: -7.5% stop (22.5% loss at 3x) - tighter to prevent overshoots - Early profit (0-2%): -3% trail (9% loss at 3x) - WIDENED to let small wins grow - Low profit (2-4%): -2% trail (6% loss at 3x) - captures 4%+ moves - Medium profit (4-6%): -1.5% trail (4.5% loss at 3x) - protects significant gains - High profit (6%+): -1% trail (3% loss at 3x) - locks in big winners EXPECTED IMPROVEMENTS: - Average wins: 1.4% → 3-5% (2-3x increase) - Win rate: Stays ~100% (already excellent) - Total profit: +242 → +500-700 USDT per 17 trades Emergency Protection: - If within 15% of liquidation price: Exit immediately - Based on NFI pattern (proven in production) Args: pair: Trading pair trade: Trade object with liquidation_price current_time: Current timestamp current_rate: Current price current_profit: Current profit/loss ratio **kwargs: Additional arguments Returns: float: Stoploss percentage (negative = loss threshold) """ # Emergency liquidation protection (borrowed from NFI strategy) # Only active in futures/margin mode when liquidation price exists if (self.config.get('trading_mode', '') in ('futures', 'margin') and trade.liquidation_price is not None and current_rate < trade.liquidation_price * 1.15): # Within 15% of liquidation logger.warning(f"{pair} EMERGENCY EXIT - Near liquidation! " f"Price: {current_rate:.8f}, Liq: {trade.liquidation_price:.8f}") return 0.001 # Exit immediately (tiny positive = force exit) # OPTIMIZED Profit-based trailing stops (asymmetric - let winners run) if current_profit >= 0.06: # 6%+ profit (HIGH - lock in big wins) return -0.01 # 1% trailing stop (tight to protect big gains) elif current_profit >= 0.04: # 4-6% profit (MEDIUM - significant gains) return -0.015 # 1.5% trailing stop (balance protection & growth) elif current_profit >= 0.02: # 2-4% profit (LOW - let it grow) return -0.02 # 2% trailing stop (wider to reach 4%+) elif current_profit >= 0.00: # Breakeven to 2% profit (EARLY - let it develop) return -0.03 # 3% trailing stop (WIDENED from -2% to let winners develop) # Still in loss = use TIGHTER base stoploss (FIXED from -9% to -7.5%) return -0.075 # -7.5% base stop (22.5% loss at 3x leverage, safer than old -9%) # -9% base stop (27% loss at 3x leverage) def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ Enhanced exit logic with time-based profit locking and unclog mechanism. NEW FEATURES: 1. Profit Locking: Exit profitable longs after extended hold (prevents erosion) 2. Unclog: Force exit losing positions after N days (frees capital) RATIONALE FOR PROFIT LOCKING (LONGS): - Longs have UNLIMITED upside (can 10x, 100x) - Use LONGER time windows than shorts to let big winners run - But still lock profits to prevent complete reversals Profit Locking Tiers (LONGER than shorts): - 8%+ profit after 8 hours: Exit (exceptional long, lock it) - 5%+ profit after 12 hours: Exit (good long, secure gains) - 3%+ profit after 24 hours: Exit (solid long, don't get greedy) Unclog Logic: - Losing trades > unclog threshold after N days: Force exit - Frees capital from dead positions - Default: -4% after 4 days Args: pair: Trading pair trade: Trade object current_time: Current timestamp current_rate: Current price current_profit: Current profit/loss ratio **kwargs: Additional arguments Returns: Optional[Union[str, bool]]: Exit reason string or None """ # Calculate trade duration in hours trade_duration_hours = (current_time - trade.open_date_utc).total_seconds() / 3600 trade_duration_days = (current_time - trade.open_date_utc).days # PROFIT LOCKING: Time-based exit for profitable longs # LONGER windows than shorts to let big winners run # Tier 1: Exceptional long (8%+ profit after 8 hours) if current_profit >= 0.08 and trade_duration_hours >= 8: logger.info(f"{pair} Profit Locking (Tier 1) - {current_profit:.2%} after {trade_duration_hours:.1f}h") return 'profit_lock_tier1' # Tier 2: Good long (5%+ profit after 12 hours) if current_profit >= 0.05 and trade_duration_hours >= 12: logger.info(f"{pair} Profit Locking (Tier 2) - {current_profit:.2%} after {trade_duration_hours:.1f}h") return 'profit_lock_tier2' # Tier 3: Solid long (3%+ profit after 24 hours) if current_profit >= 0.03 and trade_duration_hours >= 24: logger.info(f"{pair} Profit Locking (Tier 3) - {current_profit:.2%} after {trade_duration_hours:.1f}h") return 'profit_lock_tier3' # UNCLOG: Sell any positions at a loss if they are held for more than N days if current_profit < -self.unclog.value and trade_duration_days >= self.unclog_days.value: logger.info(f"{pair} Unclog - {current_profit:.2%} after {trade_duration_days} days") return 'unclog' return None def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool: """ Confirm trade exit with moon mode logic. Prevents premature exits when: - ROI exit when NOT making new highs (max_l < 0.003) - Any exit with profit below 0.3% This ensures we don't exit winners too early and captures more upside. Args: pair: Trading pair trade: Trade object order_type: Order type amount: Trade amount rate: Exit rate time_in_force: Time in force exit_reason: Reason for exit current_time: Current timestamp Returns: bool: True to confirm exit, False to block """ dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) last_candle = dataframe.iloc[-1].squeeze() # Block ROI exit if not making new highs (wait for better opportunity) if exit_reason == 'roi' and (last_candle['max_l'] < 0.003): return False # Block exits with very low profit - wait for better exit if exit_reason == 'roi' and trade.calc_profit_ratio(rate) < 0.003: logger.info(f"{trade.pair} ROI profit is below 0.3%") return False if exit_reason == 'trailing_stop_loss' and trade.calc_profit_ratio(rate) < 0: logger.info(f"{trade.pair} Trailing stop price is below 0") return False return True