""" ElliotV5_SMA_Shorts Strategy A shorts-only variant of the ElliotV5_SMA strategy, designed to profit in bear markets and during overbought conditions. This strategy mirrors the successful long-only approach but inverts the logic for short positions. Original ElliotV5_SMA Performance (Longs): - Win Rate: 73.7% - Avg Profit: 0.287% - Total Trades: 1447 - Profit Factor: 1.11 Strategy Concept: Uses Elliott Wave Oscillator (EWO) to identify momentum divergence and mean reversion opportunities for short entries. EWO measures the percentage difference between fast and slow SMAs. Entry Conditions (OR logic - either triggers entry): 1. Bear Market Rally Short: Price above EMA during downtrend (EWO < -2.5), RSI > 50 2. Parabolic Pump Short: Extreme positive momentum (EWO > 12.0), RSI > 70 Exit Conditions: - Signal: Price falls below EMA discount - ROI: Time-decay targets (15% → 10% → 6% → 2%) - Volatility-Adaptive Stop Loss: Crash mode for profit protection - Trailing Stop: +2% activation, 0.5% trail Volatility-Adaptive Stop Loss with Crash Mode: - NO PROTECTION WINDOW - Immediately trails to prevent liquidations - Crash Mode: When making new lows (min_l < 0.003), tightens stops to protect profits - Rally Mode: When not making new lows, allows wider trailing based on volatility - Adapts to each coin's volatility (move_mean, move_mean_x) - Prevents liquidations by tightening before -30% loss Key Differences from Long Strategy: - Crash Mode vs Moon Mode: Tightens when making new lows (inverse logic) - Lower ROI targets: 15% max vs 21.5% (faster profit-taking for shorts) - Asymmetric EWO thresholds: Adjusted for crypto bear market dynamics - Always uses RSI filters: Extra confirmation to avoid short squeezes - Max 4 short positions: Position limit enforcement via confirm_trade_entry Author: Derived from ElliotV5_SMA Version: 1.0.0 """ from freqtrade.strategy.interface import IStrategy from typing import Dict, List, Optional, Union from functools import reduce from pandas import DataFrame from datetime import datetime import talib.abstract as ta import numpy as np import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.persistence import Trade from freqtrade.strategy import DecimalParameter, IntParameter import logging logger = logging.getLogger(__name__) # Optimized parameters for short entries short_params = { "base_nb_candles_short_entry": 17, "ewo_high": 12.0, "ewo_low": -2.5, "high_offset": 1.022, "rsi_short": 50, "rsi_overbought": 70 } # Optimized parameters for short exits (covers) cover_params = { "base_nb_candles_short_exit": 49, "low_offset": 0.981 } def EWO(dataframe, ema_length=5, ema2_length=35): """ Elliott Wave Oscillator (EWO) Calculates the percentage difference between fast and slow SMAs. Despite the name, this uses Simple Moving Averages (SMA), not Exponential (EMA). Formula: ((SMA_fast - SMA_slow) / Close) * 100 Interpretation: - Positive EWO: Fast SMA > Slow SMA (upward momentum) - Negative EWO: Fast SMA < Slow SMA (downward momentum) - High magnitude: Strong momentum divergence Args: dataframe: Price data ema_length: Fast SMA period (default 5, strategy uses 50) ema2_length: Slow SMA period (default 35, strategy uses 200) Returns: Series: EWO values as percentage """ df = dataframe.copy() ema1 = ta.SMA(df, timeperiod=ema_length) ema2 = ta.SMA(df, timeperiod=ema2_length) emadif = (ema1 - ema2) / df['close'] * 100 return emadif class ElliotV5_SMA_Shorts(IStrategy): """ Shorts-only Elliott Wave Oscillator strategy for bear markets. This strategy is designed to run in PARALLEL with ElliotV5_SMA (longs) in separate containers for 30+ days to evaluate short performance independently. """ INTERFACE_VERSION = 3 can_short = True # More conservative ROI for shorts (crypto pumps are violent) minimal_roi = { "0": 0.15, # 15% profit target (vs 21.5% for longs) "40": 0.10, # 10% after 40 minutes (vs 13.2%) "87": 0.06, # 6% after 87 minutes (vs 8.6%) "201": 0.02 # 2% after 201 minutes (vs 3%) } # Stop loss for shorts - OPTIMIZED to prevent overshoots (was -0.09, caused -21% losses) 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) # Unclog parameters (used in custom_exit) unclog_days = IntParameter(1, 5, default=3, space='sell', optimize=True) unclog = DecimalParameter(0.01, 0.08, default=0.04, decimals=2, space='sell', optimize=True) # Position limits (enforced via confirm_trade_entry) max_short_trades = 4 # Hyperopt parameters for short entries (use 'buy' space per Freqtrade convention) base_nb_candles_short_entry = IntParameter( 5, 80, default=short_params['base_nb_candles_short_entry'], space='buy', optimize=True) high_offset = DecimalParameter( 1.01, 1.1, default=short_params['high_offset'], space='buy', optimize=True) # Hyperopt parameters for short exits/covers (use 'sell' space per Freqtrade convention) base_nb_candles_short_exit = IntParameter( 5, 80, default=cover_params['base_nb_candles_short_exit'], space='sell', optimize=True) low_offset = DecimalParameter( 0.9, 0.99, default=cover_params['low_offset'], space='sell', optimize=True) # EWO thresholds (asymmetric - adjusted for crypto dynamics) fast_ewo = 50 # Fast SMA period for EWO slow_ewo = 200 # Slow SMA period for EWO ewo_low = DecimalParameter( -8.0, -1.0, default=short_params['ewo_low'], space='buy', optimize=True) ewo_high = DecimalParameter( 8.0, 20.0, default=short_params['ewo_high'], space='buy', optimize=True) # RSI filters (always enabled for shorts to reduce risk) rsi_short = IntParameter( 40, 70, default=short_params['rsi_short'], space='buy', optimize=True) rsi_overbought = IntParameter( 60, 80, default=short_params['rsi_overbought'], space='buy', optimize=True) # Trailing stop (tighter activation for shorts) trailing_stop = True trailing_stop_positive = 0.005 # 0.5% trail distance (same as longs) trailing_stop_positive_offset = 0.02 # +2% activation (vs +3% for longs) trailing_only_offset_is_reached = True # Exit signal configuration use_exit_signal = True exit_profit_only = False exit_profit_offset = 0.01 ignore_roi_if_entry_signal = True # Timeframe and data settings timeframe = '5m' informative_timeframe = '1h' process_only_new_candles = True startup_candle_count = 2000 # Custom stoploss enabled for volatility-adaptive stop loss use_custom_stoploss = True # Plot configuration plot_config = { 'main_plot': { 'ma_short_entry': {'color': 'red'}, 'ma_short_exit': {'color': 'green'}, }, } def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], side: str, **kwargs) -> bool: """ Enforce maximum short position limit. This callback is executed before every entry to ensure we don't exceed max_short_trades positions. Essential for risk management in crypto shorts. Args: side: Trade direction ('long' or 'short') Returns: bool: True to confirm entry, False to reject """ # Only allow shorts in this strategy if side == "long": return False # Reject any long signals # Count current open short positions short_count = 0 trades = Trade.get_trades_proxy(is_open=True) for trade in trades: if trade.is_short: short_count += 1 # Check if we can open another short if short_count >= self.max_short_trades: return False # Already at max short positions return True # Confirm entry 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 short trades. Increased from 2x 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 SHORT 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 current 2%) 3. Emergency liquidation protection maintained PROBLEM IDENTIFIED: - Old tiers: 0.5-2% trails captured only 2.1% avg profit - Stop overshoots: -9% became -21% on AXS due to volatility - Risk-reward: 1:16 (terrible - wins too small, losses too big) 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: 2.1% → 4-6% (2-3x increase) - Max loss: -21% → -7.5% (70% reduction) - Risk-reward: 1:16 → 1:2-3 (sustainable) - Profit factor: 0.21 → 1.2+ (profitable) Emergency Protection: - If within 15% of liquidation price: Exit immediately (inverted for shorts) - 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 for SHORTS (inverted logic) # 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 * 0.85): # Within 15% of liquidation (INVERTED) logger.warning(f"{pair} SHORT EMERGENCY EXIT - Near liquidation! " f"Price: {current_rate:.8f}, Liq: {trade.liquidation_price:.8f}") return 0.001 # Exit immediately # 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%) # This prevents the -21% overshoots seen on AXS, -17% on ZKC 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 shorts after extended hold (prevents erosion) 2. Unclog: Force exit losing positions after N days (frees capital) RATIONALE FOR PROFIT LOCKING: - Shorts have LIMITED upside (price can only go to zero) - Longs have UNLIMITED upside (price can 10x, 100x) - Therefore: Lock short profits faster than long profits - Data shows: Many 3-4% profits eroded back to 2% before trailing stop triggered Profit Locking Tiers: - 5%+ profit after 4 hours: Exit (exceptional short, lock it) - 3%+ profit after 6 hours: Exit (good short, don't be greedy) - 2%+ profit after 12 hours: Exit (mediocre short, take it before reversal) Unclog Logic: - Losing trades > unclog threshold after N days: Force exit - Frees capital from dead positions - Default: -4% after 3 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 shorts # Prevents profit erosion and captures gains before reversal # Tier 1: Exceptional short (5%+ profit after 4 hours) if current_profit >= 0.05 and trade_duration_hours >= 4: logger.info(f"{pair} Profit Locking (Tier 1) - {current_profit:.2%} after {trade_duration_hours:.1f}h") return 'profit_lock_tier1' # Tier 2: Good short (3%+ profit after 6 hours) if current_profit >= 0.03 and trade_duration_hours >= 6: logger.info(f"{pair} Profit Locking (Tier 2) - {current_profit:.2%} after {trade_duration_hours:.1f}h") return 'profit_lock_tier2' # Tier 3: Mediocre short (2%+ profit after 12 hours) if current_profit >= 0.02 and trade_duration_hours >= 12: logger.info(f"{pair} Profit Locking (Tier 3) - {current_profit:.2%} after {trade_duration_hours:.1f}h") return 'profit_lock_tier3' # UNCLOG: Sell any short 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_short' 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 crash mode logic (inverse of moon mode). Prevents premature exits when: - ROI exit when NOT making new lows (min_l > 0.003) - Any exit with profit below 0.3% For shorts: min_l > 0.003 = price far from lows = not crashing = block exit 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 lows (wait for crash to continue) # For shorts: min_l > 0.003 = price far from lows = not crashing = block exit if exit_reason == 'roi' and (last_candle['min_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 def informative_pairs(self): """ Define additional timeframes to download. Currently requests 1h data but doesn't use it. Could be used for trend filtering in future versions. """ 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): """ Retrieve informative timeframe data. Currently unused but available for future multi-timeframe analysis. """ dataframe = self.dp.get_pair_dataframe( pair=metadata['pair'], timeframe=self.informative_timeframe) return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Calculate all technical indicators needed for entry/exit decisions. Indicators: - EMAs: Multiple periods for entry/exit price reference - EWO: Elliott Wave Oscillator (momentum indicator) - RSI: Relative Strength Index (overbought/oversold filter) - ATR: Average True Range (volatility filter) - Volume MA: Volume moving average (surge confirmation) - Volatility indicators: For adaptive stop loss """ # Calculate EMAs for entry signals (short at premium above EMA) for val in self.base_nb_candles_short_entry.range: dataframe[f'ma_short_entry_{val}'] = ta.EMA(dataframe, timeperiod=val) # Calculate EMAs for exit signals (cover at discount below EMA) for val in self.base_nb_candles_short_exit.range: dataframe[f'ma_short_exit_{val}'] = ta.EMA(dataframe, timeperiod=val) # Calculate Elliott Wave Oscillator # EWO = ((SMA_50 - SMA_200) / Close) * 100 dataframe['EWO'] = EWO(dataframe, self.fast_ewo, self.slow_ewo) # Calculate RSI (14-period) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) # ATR volatility filter (NEW - prevents entries on ultra-volatile coins) # ATR as percentage of price helps identify coins like AXS, ZKC that caused -21% losses 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 SHORT entry conditions with enhanced filters. Two conditions (OR logic - either can trigger entry): Condition 1: Bear Market Rally Short - Price pumps above EMA during downtrend (EWO < -2.5) - RSI > 50 confirms upward momentum - Short the temporary rally in bearish trend Condition 2: Parabolic Pump Short - Extreme positive momentum (EWO > 12.0) - RSI > 70 confirms overbought - Short the parabolic blow-off top Both conditions require: - Price trading at premium above EMA (high_offset * EMA) - ATR filter: Volatility < 10% (NEW - prevents ultra-volatile coins) - Volume surge: Volume > 1.5x MA (NEW - ensures strong momentum) Rationale for new filters: - ATR < 10%: Filters out coins like AXS (caused -21% loss), ZKC (-17%) - Volume surge: Ensures entries on real moves, not low-liquidity spikes """ conditions = [] # Condition 1: Bear Market Rally Short (Negative EWO) # Short temporary pumps during downtrends conditions.append( ( # Price above EMA discount threshold (trading at premium) (dataframe['close'] > ( dataframe[f'ma_short_entry_{self.base_nb_candles_short_entry.value}'] * self.high_offset.value )) & # Weak/negative momentum (downtrend with rally) (dataframe['EWO'] < self.ewo_low.value) & # RSI confirms upward movement (rally to short) (dataframe['rsi'] > self.rsi_short.value) & (dataframe['rsi'] < dataframe['rsi'].shift(1)) & # RSI turning down (continuation signal) # NEW: ATR volatility filter - avoid ultra-volatile coins (dataframe['atr_pct'] < 10.0) & # Less than 10% ATR (filters AXS, ZKC, SOMI) # NEW: Volume surge confirmation - ensure strong momentum (dataframe['volume'] > dataframe['volume_ma'] * 1.5) & # 50% above average volume # Basic volume check (dataframe['volume'] > 0) ) ) # Condition 2: Parabolic Pump Short (High Positive EWO) # Short extreme overbought conditions conditions.append( ( # Price above EMA discount threshold (trading at premium) (dataframe['close'] > ( dataframe[f'ma_short_entry_{self.base_nb_candles_short_entry.value}'] * self.high_offset.value )) & # Strong positive momentum (parabolic/overbought) (dataframe['EWO'] > self.ewo_high.value) & # RSI confirms overbought (dataframe['rsi'] > self.rsi_overbought.value) & # NEW: ATR volatility filter - avoid ultra-volatile coins (dataframe['atr_pct'] < 10.0) & # Less than 10% ATR # NEW: Volume surge confirmation - ensure strong momentum (dataframe['volume'] > dataframe['volume_ma'] * 1.5) & # 50% above average volume # Basic volume check (dataframe['volume'] > 0) ) ) # Apply conditions with OR logic (either condition triggers) if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), 'enter_short' ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Define SHORT exit (cover) conditions. Exit when: - Price falls below EMA threshold (low_offset * EMA) - This indicates the short has achieved profit target - Cover the short position to lock in gains Additional exits handled by: - ROI table (time-decay profit targets) - Stop loss (-12%) - Trailing stop (+2% activation) """ conditions = [] # Cover short when price falls below EMA threshold conditions.append( ( # Price below EMA threshold (profit target reached) (dataframe['close'] < ( dataframe[f'ma_short_exit_{self.base_nb_candles_short_exit.value}'] * self.low_offset.value )) & # Volume confirmation (dataframe['volume'] > 0) ) ) # Apply exit conditions if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), 'exit_short' ] = 1 return dataframe