import logging import numpy as np import pandas as pd import warnings from datetime import datetime, timedelta from typing import Optional, Dict, List, Tuple import talib.abstract as ta import pandas_ta as pta # pandas_ta is imported but not explicitly used in the provided code. # If it's for future use or part of an older version, that's okay. # Otherwise, it can be removed if not needed. from scipy.signal import argrelextrema import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter, BooleanParameter from freqtrade.persistence import Trade warnings.simplefilter(action="ignore", category=pd.errors.PerformanceWarning) logger = logging.getLogger(__name__) # Define Murrey Math level names for consistency MML_LEVEL_NAMES = [ "[-3/8]P", "[-2/8]P", "[-1/8]P", "[0/8]P", "[1/8]P", "[2/8]P", "[3/8]P", "[4/8]P", "[5/8]P", "[6/8]P", "[7/8]P", "[8/8]P", "[+1/8]P", "[+2/8]P", "[+3/8]P" ] def calculate_minima_maxima(df, window): if df is None or df.empty: return np.zeros(0), np.zeros(0) minima = np.zeros(len(df)) maxima = np.zeros(len(df)) for i in range(window, len(df)): window_data = df['ha_close'].iloc[i - window:i + 1] if df['ha_close'].iloc[i] == window_data.min() and (window_data == df['ha_close'].iloc[i]).sum() == 1: minima[i] = -window if df['ha_close'].iloc[i] == window_data.max() and (window_data == df['ha_close'].iloc[i]).sum() == 1: maxima[i] = window return minima, maxima class AlexBattleTankKillerV4(IStrategy): """ Enhanced strategy on the 15-minute timeframe with Market Correlation Filters. Key improvements: - Adjustet Entrys - Added Option to use Stoploss on Exchange for Trades - Fixed Stop Loss and Added Long and Short Exits - Dynamic stoploss based on ATR. - Dynamic leverage calculation. - Murrey Math level calculation (rolling window for performance). - Enhanced DCA (Average Price) logic. - Translated to English and code structured for clarity. - Parameterization of internal constants for optimization. - Changed Exit Signals for Opposite. - Change SL to -0.07 - Changed stake amout for renentry - FIXED: Prevents opening opposite position when trade is active - FIXED: Trailing stop properly disabled - NEW: Market correlation filters for better entry timing """ # General strategy parameters timeframe = "1h" startup_candle_count: int = 200 stoploss = -0.15 # Fixed 8% stop loss use_custom_stoploss = False # Disable custom logic stoploss_on_exchange = True # Let exchange handle it stoploss_on_exchange_interval = 60 # Check every 60 seconds trailing_stop = True trailing_stop_positive = 0.015 # Start trailing at 1.5% trailing_stop_positive_offset = 0.03 # Begin after 3% profit trailing_only_offset_is_reached = True position_adjustment_enable = True can_short = True use_exit_signal = True ignore_roi_if_entry_signal = False max_stake_per_trade = 10.0 max_portfolio_percentage_per_trade = 0.05 max_entry_position_adjustment = 1 process_only_new_candles = True max_dca_orders = 1 max_total_stake_per_pair = 10 max_single_dca_amount = 4 use_custom_exits_advanced = True use_emergency_exits = True enable_position_flip = True disable_volatility_filter = BooleanParameter(default=True, space="buy", optimize=False) # 🎯 REENTRY PARAMETERS enable_reentry = BooleanParameter(default=False, space='buy') reentry_cooldown_minutes = IntParameter(15, 120, default=45, space='buy') # 15min-2h (1-8 candles) max_reentries_per_direction = IntParameter(1, 3, default=2, space='buy') # Reduced from 7→2 enable_quick_reentry_on_roi = BooleanParameter(default=True, space='buy') quick_reentry_cooldown = IntParameter(15, 45, default=30, space='buy') # 15-45min (1-3 candles) reentry_signal_strength_threshold = DecimalParameter(0.3, 0.8, default=0.4, space='buy') signal_quality_enabled = BooleanParameter(default=True, space='buy', optimize=True) min_signal_strength = DecimalParameter(0.3, 0.8, default=0.5, space='buy', optimize=True) # 🎯 NEW SIGNAL FLIP PARAMETERS signal_flip_enabled = BooleanParameter(default=True, space="sell", optimize=True, load=True) # NEW signal_flip_min_profit = DecimalParameter(-0.05, 0.02, default=-0.02, decimals=3, space="sell", optimize=True, load=True) # NEW signal_flip_max_loss = DecimalParameter(-0.10, -0.03, default=-0.06, decimals=2, space="sell", optimize=True, load=True) # NEW # Volume confirmation (referenced but not defined!) volume_confirmation_enabled = BooleanParameter(default=True, space='buy', optimize=True) require_volume_confirmation = BooleanParameter(default=False, space='buy', optimize=True) # Signal confirmation parameters signal_confirmation_candles = IntParameter(1, 5, default=2, space="buy", optimize=True, load=True) signal_cooldown_period = IntParameter(3, 15, default=5, space="buy", optimize=True, load=True) min_signal_strength = DecimalParameter(0.7, 0.9, default=0.8, decimals=2, space="buy", optimize=True, load=True) # Trend stability parameters trend_stability_period = IntParameter(5, 20, default=10, space="buy", optimize=True, load=True) min_trend_strength = DecimalParameter(0.01, 0.05, default=0.02, decimals=3, space="buy", optimize=True, load=True) # Signal filtering parameters signal_quality_enabled = BooleanParameter(default=True, space='buy') min_signal_strength = DecimalParameter(0.3, 0.8, default=0.5, space='buy') flip_profit_threshold = DecimalParameter(low=0.01, high=0.10, default=0.03, decimals=3, space="buy", optimize=True, load=True) # 🚀 FOLLOW THE PRICE PARAMETERS (NEU) follow_price_enabled = BooleanParameter(default=False, space="sell", optimize=True, load=True) follow_price_activation_profit = DecimalParameter(0.01, 0.05, default=0.02, decimals=3, space="sell", optimize=True, load=True) # CHANGED: Was 0.01, now 0.02 follow_price_pullback_pct = DecimalParameter(0.005, 0.03, default=0.015, decimals=3, space="sell", optimize=True, load=True) # CHANGED: Range tightened follow_price_min_profit = DecimalParameter(0.005, 0.04, default=0.01, decimals=3, space="sell", optimize=True, load=True) # 🎯 MML-BASIERTE FOLLOW PRICE (NEU) follow_price_use_mml = BooleanParameter(default=True, space="sell", optimize=True, load=True) follow_price_mml_buffer = DecimalParameter(0.001, 0.01, default=0.003, decimals=4, space="sell", optimize=True, load=True) # 📊 MARKET CONDITION ADJUSTMENTS (NEU) follow_price_market_adjustment = BooleanParameter(default=True, space="sell", optimize=True, load=True) follow_price_volatile_multiplier = DecimalParameter(0.5, 1.0, default=0.7, decimals=2, space="sell", optimize=True, load=True) follow_price_bearish_multiplier = DecimalParameter(0.6, 1.0, default=0.8, decimals=2, space="sell", optimize=True, load=True) # 🔧 ATR STOPLOSS PARAMETERS (Anpassbar machen) atr_stoploss_multiplier = DecimalParameter(2.5, 3.5, default=2.8, space="sell") atr_stoploss_minimum = DecimalParameter(-0.30, -0.15, default=-0.25, space="sell") atr_stoploss_maximum = DecimalParameter(-0.12, -0.06, default=-0.08, space="sell") early_trade_multiplier = DecimalParameter(1.2, 1.6, default=1.4, space="sell") early_trade_hours = DecimalParameter(1.0, 3.0, default=2.0, space="sell") # DCA parameters initial_safety_order_trigger = DecimalParameter( low=-0.02, high=-0.01, default=-0.018, decimals=3, space="buy", optimize=True, load=True ) max_safety_orders = IntParameter(1, 3, default=2, space="buy", optimize=True, load=True) safety_order_step_scale = DecimalParameter( low=1.05, high=1.5, default=1.25, decimals=2, space="buy", optimize=True, load=True ) safety_order_volume_scale = DecimalParameter( low=1.1, high=2.0, default=1.4, decimals=1, space="buy", optimize=True, load=True ) h2 = IntParameter(20, 60, default=40, space="buy", optimize=True, load=True) h1 = IntParameter(10, 40, default=20, space="buy", optimize=True, load=True) h0 = IntParameter(5, 20, default=10, space="buy", optimize=True, load=True) cp = IntParameter(5, 20, default=10, space="buy", optimize=True, load=True) # Entry parameters increment_for_unique_price = DecimalParameter( low=1.0005, high=1.002, default=1.001, decimals=4, space="buy", optimize=True, load=True ) last_entry_price: Optional[float] = None # Protection parameters cooldown_lookback = IntParameter(2, 48, default=1, space="protection", optimize=True) stop_duration = IntParameter(12, 200, default=4, space="protection", optimize=True) use_stop_protection = BooleanParameter(default=True, space="protection", optimize=True) # Murrey Math level parameters mml_const1 = DecimalParameter(1.0, 1.1, default=1.0699, decimals=4, space="buy", optimize=True, load=True) mml_const2 = DecimalParameter(0.99, 1.0, default=0.99875, decimals=5, space="buy", optimize=True, load=True) indicator_mml_window = IntParameter(32, 128, default=64, space="buy", optimize=True, load=True) # Dynamic Stoploss parameters use_dynamic_stoploss = BooleanParameter(default=True, space='sell', optimize=True) atr_multiplier = DecimalParameter(1.5, 3.0, default=2.0, space='sell', optimize=True) max_stoploss = DecimalParameter(-0.25, -0.10, default=-0.15, space='sell', optimize=True) min_stoploss = DecimalParameter(-0.10, -0.05, default=-0.08, space='sell', optimize=True) stoploss_atr_multiplier = DecimalParameter(1.0, 3.0, default=1.5, decimals=1, space="sell", optimize=True, load=True) stoploss_max_reasonable = DecimalParameter(-0.30, -0.10, default=-0.20, decimals=2, space="sell", optimize=True, load=True) # === Hyperopt Parameters === dominance_threshold = IntParameter(1, 10, default=3, space="buy", optimize=True) tightness_factor = DecimalParameter(0.5, 2.0, default=1.0, space="buy", optimize=True) long_rsi_threshold = IntParameter(50, 65, default=55, space="buy", optimize=True) short_rsi_threshold = IntParameter(30, 45, default=40, space="sell", optimize=True) # Dynamic Leverage parameters leverage_window_size = IntParameter(20, 100, default=50, space="buy", optimize=True, load=True) leverage_base = DecimalParameter(5.0, 20.0, default=7.0, decimals=1, space="buy", optimize=True, load=True) leverage_rsi_low = DecimalParameter(20.0, 40.0, default=30.0, decimals=1, space="buy", optimize=True, load=True) leverage_rsi_high = DecimalParameter(60.0, 80.0, default=70.0, decimals=1, space="buy", optimize=True, load=True) leverage_long_increase_factor = DecimalParameter(1.1, 2.0, default=1.5, decimals=1, space="buy", optimize=True, load=True) leverage_long_decrease_factor = DecimalParameter(0.3, 0.9, default=0.5, decimals=1, space="buy", optimize=True, load=True) leverage_volatility_decrease_factor = DecimalParameter(0.5, 0.95, default=0.8, decimals=2, space="buy", optimize=True, load=True) leverage_atr_threshold_pct = DecimalParameter(0.01, 0.05, default=0.03, decimals=3, space="buy", optimize=True, load=True) # Indicator parameters indicator_extrema_order = IntParameter(3, 10, default=5, space="buy", optimize=True, load=True) indicator_rolling_window_threshold = IntParameter(5, 20, default=10, space="buy", optimize=True, load=True) indicator_rolling_check_window = IntParameter(2, 10, default=4, space="buy", optimize=True, load=True) # === Market Correlation Parameters === # Bitcoin correlation parameters enable_btc_correlation = BooleanParameter(default=True, space="buy", optimize=False) btc_correlation_enabled = BooleanParameter(default=False, space="buy", optimize=False) btc_correlation_threshold = DecimalParameter(0.3, 0.8, default=0.5, decimals=2, space="buy", optimize=True) btc_trend_filter_enabled = BooleanParameter(default=False, space="buy", optimize=True) # Market Breadth # Add these to your strategy class: market_breadth_enabled = BooleanParameter(default=True, space="buy", optimize=True) market_breadth_threshold = DecimalParameter(0.3, 0.6, default=0.45, space="buy", optimize=True) market_breadth_bull_threshold = DecimalParameter(0.35, 0.65, default=0.50, space="buy", optimize=True, load=True) market_breadth_bear_threshold = DecimalParameter(0.35, 0.65, default=0.50, space="buy", optimize=True, load=True) market_breadth_sensitivity = DecimalParameter(0.05, 0.20, default=0.10, space="buy", optimize=True, load=True) breadth_weighting_enabled = BooleanParameter(default=True, space="buy", optimize=True, load=True) volume_confirmation_enabled = BooleanParameter(default=True, space="buy", optimize=True, load=True) # Total market cap parameters total_mcap_filter_enabled = BooleanParameter(default=True, space="buy", optimize=True) total_mcap_ma_period = IntParameter(20, 100, default=50, space="buy", optimize=True) # Market regime parameters regime_filter_enabled = BooleanParameter(default=True, space="buy", optimize=True) regime_lookback_period = IntParameter(24, 168, default=72, space="buy", optimize=True) # hours # Fear & Greed parameters fear_greed_enabled = BooleanParameter(default=False, space="buy", optimize=True) # Optional fear_greed_extreme_threshold = IntParameter(20, 30, default=25, space="buy", optimize=True) fear_greed_greed_threshold = IntParameter(70, 80, default=75, space="buy", optimize=True) # Momentum avoid_strong_trends = BooleanParameter(default=True, space="buy", optimize=True) trend_strength_threshold = DecimalParameter(0.01, 0.05, default=0.02, space="buy", optimize=True) momentum_confirmation_candles = IntParameter(1, 5, default=2, space="buy", optimize=True) # ROI table (minutes to decimal) minimal_roi = { "0": 0.17, # 4.5% immediate (vs your 7%) "64": 0.113, # 4% after 30 min "170": 0.051, # 3.5% after 1h "304": 0 # 3% after 2h } plot_config = { "main_plot": { # Price action and key levels "actual_stoploss": {"color": "white", "type": "line"}, "follow_price_level": {"color": "green", "type": "line"}, # Key MML levels on main chart for better visibility "[2/8]P": {"color": "#ff6b6b", "type": "line"}, # 25% - Support/Resistance "[4/8]P": {"color": "#4ecdc4", "type": "line"}, # 50% - Key level "[6/8]P": {"color": "#45b7d1", "type": "line"}, # 75% - Support/Resistance # Entry signals on main chart "enter_long": {"color": "lime", "type": "scatter"}, "enter_short": {"color": "red", "type": "scatter"}, }, "subplots": { # 📊 EXIT SIGNALS ANALYSIS (NEW - Most Important) "exit_signals": { "exit_long": {"color": "#ff4757", "type": "scatter"}, # Red exit longs "exit_short": {"color": "#2ed573", "type": "scatter"}, # Green exit shorts "Signal_Flip_Short": {"color": "#ff9ff3", "type": "scatter"}, # Pink signal flips "Signal_Flip_Long": {"color": "#54a0ff", "type": "scatter"}, # Blue signal flips "Momentum_Loss": {"color": "#ff6b35", "type": "scatter"}, # Orange momentum loss "Support_Break": {"color": "#ff3838", "type": "scatter"}, # Dark red breaks "Resistance_Break": {"color": "#2ecc71", "type": "scatter"}, # Green breaks }, # 🎯 TRADE PERFORMANCE INDICATORS (NEW) "trade_metrics": { "price_change_5": {"color": "#74b9ff", "type": "line"}, # 5-period price change "rsi_change": {"color": "#fd79a8", "type": "line"}, # RSI momentum "volume_surge": {"color": "#fdcb6e", "type": "line"}, # Volume spikes }, # 📈 RSI WITH EXIT ZONES (ENHANCED) "rsi_analysis": { "rsi": {"color": "#6c5ce7", "type": "line"}, "rsi_overbought_70": {"color": "#fd79a8", "type": "line"}, # 70 level "rsi_oversold_30": {"color": "#00b894", "type": "line"}, # 30 level }, # 🏔️ EXTREMA ANALYSIS (KEEP - Important for entries) "extrema_analysis": { "s_extrema": {"color": "#f53580", "type": "line"}, "minima_sort_threshold": {"color": "#4ae747", "type": "line"}, "maxima_sort_threshold": {"color": "#5b5e4b", "type": "line"}, }, # 📍 MIN/MAX VISUALIZATION (KEEP - Entry confirmation) "min_max_viz": { "maxima": {"color": "#a29db9", "type": "scatter"}, # Changed to scatter "minima": {"color": "#aac7fc", "type": "scatter"}, # Changed to scatter "maxima_check": {"color": "#e17055", "type": "line"}, "minima_check": {"color": "#74b9ff", "type": "line"}, }, # 🎯 ADDITIONAL MML LEVELS (ENHANCED) "murrey_math_levels": { "[1/8]P": {"color": "#d63031", "type": "line"}, # 12.5% - Extreme oversold "[3/8]P": {"color": "#fd79a8", "type": "line"}, # 37.5% - Support "[4/8]P": {"color": "#0984e3", "type": "line"}, # 50% - Key level (duplicate for subplot) "[5/8]P": {"color": "#00b894", "type": "line"}, # 62.5% - Resistance "[7/8]P": {"color": "#e84393", "type": "line"}, # 87.5% - Extreme overbought "[8/8]P": {"color": "#d63031", "type": "line"}, # 100% - Top "[0/8]P": {"color": "#d63031", "type": "line"}, # 0% - Bottom }, # 🌐 MARKET CORRELATION (KEEP - Market context) "market_correlation": { "btc_correlation": {"color": "#0984e3", "type": "line"}, "market_breadth": {"color": "#00b894", "type": "line"}, "market_score": {"color": "#6c5ce7", "type": "line"}, "market_direction": {"color": "#fd79a8", "type": "line"}, # NEW }, # 📊 MARKET REGIME (ENHANCED) "market_regime": { "market_volatility": {"color": "#e17055", "type": "line"}, "mcap_trend": {"color": "#fdcb6e", "type": "line"}, "market_adx": {"color": "#74b9ff", "type": "line"}, # NEW - Trend strength }, # 📉 VOLUME ANALYSIS (NEW - Important for exit confirmation) "volume_analysis": { "volume": {"color": "#636e72", "type": "bar"}, "volume_ratio": {"color": "#00cec9", "type": "line"}, # Volume vs average "avg_volume": {"color": "#fd79a8", "type": "line"}, # 20-period average }, # 🎯 TREND ANALYSIS (NEW - For exit timing) "trend_analysis": { "trend_consistency": {"color": "#a29bfe", "type": "line"}, # Multi-timeframe trend "trend_strength": {"color": "#fd79a8", "type": "line"}, # Trend momentum "strong_uptrend": {"color": "#00b894", "type": "line"}, # Boolean uptrend "strong_downtrend": {"color": "#d63031", "type": "line"}, # Boolean downtrend }, # 🔥 SIGNAL QUALITY (NEW - Signal strength analysis) "signal_quality": { "combined_signal_strength": {"color": "#fd79a8", "type": "line"}, # Overall signal strength "mml_signal_strength": {"color": "#74b9ff", "type": "line"}, # MML-based strength "rsi_signal_strength": {"color": "#00b894", "type": "line"}, # RSI-based strength "volume_strength": {"color": "#fdcb6e", "type": "line"}, # Volume confirmation } }, } def get_or_init_reentry_data(self, pair: str): """ 📝 Initialize re-entry data for pair if not exists (Use this approach if __init__ causes issues) """ # Initialize recent_exits if not exists if not hasattr(self, 'recent_exits'): self.recent_exits = {} if pair not in self.recent_exits: self.recent_exits[pair] = [] # Initialize reentry_count if not exists if not hasattr(self, 'reentry_count'): self.reentry_count = {} if pair not in self.reentry_count: self.reentry_count[pair] = {'long': 0, 'short': 0} # Initialize signal_strength_cache if not exists if not hasattr(self, 'signal_strength_cache'): self.signal_strength_cache = {} def calculate_signal_strength(self, dataframe: pd.DataFrame) -> pd.DataFrame: """ 🔧 FIXED: Enhanced signal strength calculation that actually works """ # Initialize all strength columns dataframe['rsi_signal_strength'] = 0.0 dataframe['mml_signal_strength'] = 0.0 dataframe['mml_bounce_strength'] = 0.0 dataframe['volume_strength'] = 0.0 dataframe['extrema_strength'] = 0.0 # RSI signal strength rsi = dataframe['rsi'].fillna(50) # Long RSI strength dataframe.loc[rsi < 25, 'rsi_signal_strength'] = 1.0 # Very oversold dataframe.loc[rsi.between(25, 35), 'rsi_signal_strength'] = 0.8 # Oversold dataframe.loc[rsi.between(35, 40), 'rsi_signal_strength'] = 0.6 # Mildly oversold # Short RSI strength dataframe.loc[rsi > 75, 'rsi_signal_strength'] = 1.0 # Very overbought dataframe.loc[rsi.between(65, 75), 'rsi_signal_strength'] = 0.8 # Overbought dataframe.loc[rsi.between(60, 65), 'rsi_signal_strength'] = 0.6 # Mildly overbought # MML signal strength (safe column access) try: if '[1/8]P' in dataframe.columns and '[7/8]P' in dataframe.columns: # Extreme MML signals dataframe.loc[dataframe['close'] < dataframe['[1/8]P'], 'mml_signal_strength'] = 1.0 dataframe.loc[dataframe['close'] > dataframe['[7/8]P'], 'mml_signal_strength'] = 1.0 if '[2/8]P' in dataframe.columns and '[6/8]P' in dataframe.columns: # Medium MML signals dataframe.loc[dataframe['close'] < dataframe['[2/8]P'], 'mml_signal_strength'] = 0.8 dataframe.loc[dataframe['close'] > dataframe['[6/8]P'], 'mml_signal_strength'] = 0.8 # MML bounces bounce_conditions = ( ((dataframe['low'] <= dataframe['[2/8]P']) & (dataframe['close'] > dataframe['[2/8]P'])) | ((dataframe['high'] >= dataframe['[6/8]P']) & (dataframe['close'] < dataframe['[6/8]P'])) ) dataframe.loc[bounce_conditions, 'mml_bounce_strength'] = 0.9 except Exception as e: logger.warning(f"MML signal strength calculation error: {e}") # Volume strength (safe calculation) try: avg_volume = dataframe['volume'].rolling(20).mean() dataframe.loc[dataframe['volume'] > avg_volume * 1.5, 'volume_strength'] = 1.0 dataframe.loc[dataframe['volume'] > avg_volume * 1.2, 'volume_strength'] = 0.8 dataframe.loc[dataframe['volume'] > avg_volume, 'volume_strength'] = 0.6 dataframe.loc[dataframe['volume'] <= avg_volume, 'volume_strength'] = 0.3 except Exception as e: logger.warning(f"Volume strength calculation error: {e}") dataframe['volume_strength'] = 0.5 # Extrema strength (safe access) try: if 'minima' in dataframe.columns: dataframe.loc[dataframe['minima'] == 1, 'extrema_strength'] = 0.8 if 'maxima' in dataframe.columns: dataframe.loc[dataframe['maxima'] == 1, 'extrema_strength'] = 0.8 except Exception as e: logger.warning(f"Extrema strength calculation error: {e}") # Combined signal strength (weighted average) dataframe['combined_signal_strength'] = ( dataframe['rsi_signal_strength'] * 0.3 + dataframe['mml_signal_strength'] * 0.25 + dataframe['mml_bounce_strength'] * 0.2 + dataframe['volume_strength'] * 0.15 + dataframe['extrema_strength'] * 0.1 ).clip(0, 1) return dataframe def check_signal_confirmation(self, dataframe: pd.DataFrame, signal_type: str) -> pd.Series: """ Check if signal persists for required confirmation period """ confirmation_period = self.signal_confirmation_candles.value if signal_type == 'long': # Check if bullish conditions persist bullish_conditions = ( (dataframe['rsi'] < 45) | # Oversold or neutral (dataframe['close'] > dataframe['[4/8]P']) | # Above 50% MML (dataframe['minima'] == 1) | # Local bottom (dataframe['close'] > dataframe['close'].shift(1)) # Price rising ) # Signal confirmed if conditions met for X candles confirmed = bullish_conditions.rolling(confirmation_period).sum() >= (confirmation_period * 0.7) elif signal_type == 'short': # Check if bearish conditions persist bearish_conditions = ( (dataframe['rsi'] > 55) | # Overbought or neutral (dataframe['close'] < dataframe['[4/8]P']) | # Below 50% MML (dataframe['maxima'] == 1) | # Local top (dataframe['close'] < dataframe['close'].shift(1)) # Price falling ) # Signal confirmed if conditions met for X candles confirmed = bearish_conditions.rolling(confirmation_period).sum() >= (confirmation_period * 0.7) else: confirmed = pd.Series([False] * len(dataframe)) return confirmed def check_signal_cooldown(self, dataframe: pd.DataFrame) -> pd.Series: """ Prevent signals too close together """ cooldown_period = self.signal_cooldown_period.value # Track recent signals dataframe['recent_long_signal'] = dataframe['enter_long'].rolling(cooldown_period).sum() dataframe['recent_short_signal'] = dataframe['enter_short'].rolling(cooldown_period).sum() # Only allow new signals if no recent signals can_signal = ( (dataframe['recent_long_signal'] == 0) & (dataframe['recent_short_signal'] == 0) ) return can_signal def detect_market_state(self, df: pd.DataFrame) -> pd.DataFrame: """ Detect current market state with pair-specific calculations """ df = df.copy() df['price_volatility'] = df['atr'] / df['close'] df['volatility_percentile'] = df['price_volatility'].rolling(self.regime_lookback_period.value).rank(pct=True) df['volume_ratio'] = df['volume'] / df['volume'].rolling(20).mean() df['volume_increasing'] = df['volume'] > df['volume'].shift(1) df['momentum_3'] = df['close'].pct_change(3) df['momentum_5'] = df['close'].pct_change(5) df['momentum_10'] = df['close'].pct_change(10) df['consistent_uptrend'] = ( (df['momentum_3'] > 0) & (df['momentum_5'] > 0) & (df['momentum_10'] > 0) ) df['consistent_downtrend'] = ( (df['momentum_3'] < 0) & (df['momentum_5'] < 0) & (df['momentum_10'] < 0) ) df['market_state'] = 'neutral' df.loc[ (df['consistent_uptrend'] & (df['adx'] > 25) & (df['volume_ratio'] > 1.2)), 'market_state' ] = 'bull_run' df.loc[ (df['consistent_downtrend'] & (df['adx'] > 25) & (df['volume_ratio'] > 1.2)), 'market_state' ] = 'bear_market' df.loc[ (df['adx'] < 20) & (df['volatility_percentile'] < 0.5), 'market_state' ] = 'ranging' df.loc[ (df['volatility_percentile'] > 0.8), 'market_state' ] = 'high_volatility' df['potential_reversal'] = ( ((df['high'] >= df['high'].rolling(10).max()) & (df['momentum_3'] < df['momentum_3'].shift(3))) | ((df['low'] <= df['low'].rolling(10).min()) & (df['momentum_3'] > df['momentum_3'].shift(3))) ) df['potential_breakout'] = ( (df['close'] > df['high'].rolling(20).max().shift(1)) | (df['close'] < df['low'].rolling(20).min().shift(1)) ) & (df['volume_ratio'] > 1.5) df['market_regime'] = df['market_state'] logger.info(f"🏛️ Market regime: {df['market_regime'].iloc[-1]}, " f"Volatility: {df['price_volatility'].iloc[-1]:.4f}, " f"ADX: {df['adx'].iloc[-1]:.1f}, " f"Trend Strength: {df['momentum_10'].iloc[-1]:.2%}") return df def get_timing_filters(self, dataframe: pd.DataFrame, direction: str) -> pd.Series: """ Get timing-based filters to avoid bad entry timing """ if direction == 'long': good_timing = ( # Avoid entering during strong downtrends (dataframe['market_state'] != 'strong_downtrend') & # Prefer breakouts or oversold conditions ( dataframe['potential_breakout'] | (dataframe['rsi'] < 40) | (dataframe['market_state'] == 'strong_uptrend') ) & # Avoid high volatility unless extremely oversold ( (dataframe['market_state'] != 'high_volatility') | (dataframe['rsi'] < 25) ) & # Volume confirmation if required ( (~self.require_volume_confirmation.value) | (dataframe['volume_ratio'] > 1.1) ) ) else: # short good_timing = ( # Avoid entering during strong uptrends (dataframe['market_state'] != 'strong_uptrend') & # Prefer breakdowns or overbought conditions ( dataframe['potential_breakout'] | (dataframe['rsi'] > 60) | (dataframe['market_state'] == 'strong_downtrend') ) & # Avoid high volatility unless extremely overbought ( (dataframe['market_state'] != 'high_volatility') | (dataframe['rsi'] > 75) ) & # Volume confirmation if required ( (~self.require_volume_confirmation.value) | (dataframe['volume_ratio'] > 1.1) ) ) return good_timing # Helper method to check if we have an active position in the opposite direction def has_active_trade(self, pair: str, side: str) -> bool: """ Check if there's an active trade in the specified direction """ try: trades = Trade.get_open_trades() for trade in trades: if trade.pair == pair: if side == "long" and not trade.is_short: return True elif side == "short" and trade.is_short: return True except Exception as e: logger.warning(f"Error checking active trades for {pair}: {e}") return False @staticmethod def _calculate_mml_core(mn: float, finalH: float, mx: float, finalL: float, mml_c1: float, mml_c2: float) -> Dict[str, float]: dmml_calc = ((finalH - finalL) / 8.0) * mml_c1 if dmml_calc == 0 or np.isinf(dmml_calc) or np.isnan(dmml_calc) or finalH == finalL: return {key: finalL for key in MML_LEVEL_NAMES} mml_val = (mx * mml_c2) + (dmml_calc * 3) if np.isinf(mml_val) or np.isnan(mml_val): return {key: finalL for key in MML_LEVEL_NAMES} ml = [mml_val - (dmml_calc * i) for i in range(16)] return { "[-3/8]P": ml[14], "[-2/8]P": ml[13], "[-1/8]P": ml[12], "[0/8]P": ml[11], "[1/8]P": ml[10], "[2/8]P": ml[9], "[3/8]P": ml[8], "[4/8]P": ml[7], "[5/8]P": ml[6], "[6/8]P": ml[5], "[7/8]P": ml[4], "[8/8]P": ml[3], "[+1/8]P": ml[2], "[+2/8]P": ml[1], "[+3/8]P": ml[0], } def calculate_rolling_murrey_math_levels_optimized(self, df: pd.DataFrame, window_size: int) -> Dict[str, pd.Series]: """ OPTIMIZED: Calculate Murrey Math Levels using a rolling window with caching and interpolation. Only recalculates every 5 candles for performance, interpolating intermediate values. Compatible with Pandas 2.0+ and handles edge cases robustly. """ # Initialize cache if not present if not hasattr(self, 'mml_cache'): self.mml_cache = {} pair = getattr(self, 'current_pair', 'default_pair') # Fallback for pair context if pair not in self.mml_cache or len(self.mml_cache[pair]) != len(df): murrey_levels_data = {key: [np.nan] * len(df) for key in MML_LEVEL_NAMES} # Use rolling max/min with minimum periods to avoid NaN issues rolling_high = df["high"].rolling(window=window_size, min_periods=1).max() rolling_low = df["low"].rolling(window=window_size, min_periods=1).min() mml_c1 = self.mml_const1.value mml_c2 = self.mml_const2.value # Optimize by calculating only every 5th candle calculation_step = 5 for i in range(0, len(df), calculation_step): if i < window_size - 1: continue mn_period = rolling_low.iloc[i] mx_period = rolling_high.iloc[i] current_close = df["close"].iloc[i] # Handle edge cases where data might be invalid if pd.isna(mn_period) or pd.isna(mx_period) or mn_period == mx_period or mn_period == 0: for key in MML_LEVEL_NAMES: murrey_levels_data[key][i] = current_close continue dmml_calc = ((mx_period - mn_period) / 8.0) * mml_c1 if dmml_calc == 0 or np.isinf(dmml_calc) or np.isnan(dmml_calc): for key in MML_LEVEL_NAMES: murrey_levels_data[key][i] = current_close continue mml_val = (mx_period * mml_c2) + (dmml_calc * 3) if np.isinf(mml_val) or np.isnan(mml_val): for key in MML_LEVEL_NAMES: murrey_levels_data[key][i] = current_close continue ml = [mml_val - (dmml_calc * i) for i in range(16)] levels = { "[-3/8]P": ml[14], "[-2/8]P": ml[13], "[-1/8]P": ml[12], "[0/8]P": ml[11], "[1/8]P": ml[10], "[2/8]P": ml[9], "[3/8]P": ml[8], "[4/8]P": ml[7], "[5/8]P": ml[6], "[6/8]P": ml[5], "[7/8]P": ml[4], "[8/8]P": ml[3], "[+1/8]P": ml[2], "[+2/8]P": ml[1], "[+3/8]P": ml[0], } for key in MML_LEVEL_NAMES: murrey_levels_data[key][i] = levels.get(key, current_close) # Interpolate and fill missing values efficiently for key in MML_LEVEL_NAMES: series = pd.Series(murrey_levels_data[key], index=df.index) series = series.interpolate(method='linear', limit_direction='both').ffill().bfill() murrey_levels_data[key] = series.tolist() self.mml_cache[pair] = {key: pd.Series(data, index=df.index) for key, data in murrey_levels_data.items()} return self.mml_cache[pair] def calculate_market_correlation_simple(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ Simplified correlation calculation that avoids index issues """ pair = metadata['pair'] base_currency = pair.split('/')[0] # Skip if this IS BTC if base_currency == 'BTC': dataframe['btc_correlation'] = 1.0 dataframe['btc_trend'] = 1 dataframe['btc_close'] = dataframe['close'] dataframe['btc_sma20'] = dataframe['close'].rolling(20).mean() dataframe['btc_sma50'] = dataframe['close'].rolling(50).mean() dataframe['returns'] = dataframe['close'].pct_change() return dataframe # Try to get BTC data btc_pairs = ["BTC/USDT:USDT", "BTC/USDT", "BTC/USD"] btc_dataframe = None for btc_pair in btc_pairs: try: btc_dataframe, _ = self.dp.get_analyzed_dataframe(btc_pair, self.timeframe) if btc_dataframe is not None and not btc_dataframe.empty and len(btc_dataframe) >= 50: logger.info(f"Using {btc_pair} for simplified BTC correlation with {pair}") break except: continue # If no BTC data, use neutral values if btc_dataframe is None or btc_dataframe.empty: dataframe['btc_correlation'] = 0.5 dataframe['btc_trend'] = 0 dataframe['btc_close'] = dataframe['close'] dataframe['btc_sma20'] = dataframe['close'] dataframe['btc_sma50'] = dataframe['close'] dataframe['returns'] = dataframe['close'].pct_change() logger.warning(f"No BTC data for {pair}, using neutral correlation") return dataframe # Simple correlation using only latest values try: # Use shorter period to avoid length issues correlation_period = min(20, len(dataframe) // 5, len(btc_dataframe) // 5) correlation_period = max(5, correlation_period) # Calculate returns pair_returns = dataframe['close'].pct_change().dropna() btc_returns = btc_dataframe['close'].pct_change().dropna() # Use only last N periods for correlation if len(pair_returns) >= correlation_period and len(btc_returns) >= correlation_period: pair_recent = pair_returns.tail(correlation_period) btc_recent = btc_returns.tail(correlation_period) # Align lengths min_len = min(len(pair_recent), len(btc_recent)) correlation = pair_recent.tail(min_len).corr(btc_recent.tail(min_len)) if pd.isna(correlation): correlation = 0.5 else: correlation = 0.5 # BTC trend (simple) btc_close = btc_dataframe['close'].iloc[-1] btc_sma20 = btc_dataframe['close'].rolling(20).mean().iloc[-1] btc_sma50 = btc_dataframe['close'].rolling(50).mean().iloc[-1] if pd.isna(btc_sma20) or pd.isna(btc_sma50): btc_trend = 0 elif btc_close > btc_sma20 > btc_sma50: btc_trend = 1 elif btc_close < btc_sma20 < btc_sma50: btc_trend = -1 else: btc_trend = 0 # Set constant values for entire dataframe dataframe['btc_correlation'] = correlation dataframe['btc_trend'] = btc_trend dataframe['btc_close'] = btc_close dataframe['btc_sma20'] = btc_sma20 dataframe['btc_sma50'] = btc_sma50 logger.debug(f"{pair} Simple correlation: {correlation:.3f}, BTC trend: {btc_trend}") except Exception as e: logger.warning(f"Simple correlation failed for {pair}: {e}") dataframe['btc_correlation'] = 0.5 dataframe['btc_trend'] = 0 dataframe['btc_close'] = dataframe['close'] dataframe['btc_sma20'] = dataframe['close'] dataframe['btc_sma50'] = dataframe['close'] # Always calculate returns dataframe['returns'] = dataframe['close'].pct_change() return dataframe def informative_pairs(self): """ Define additional, informative pair/interval combinations to download """ pairs = self.dp.current_whitelist() informative_pairs = [] # Add major crypto pairs for market breadth calculation # These are the pairs used in calculate_market_breadth_weighted major_pairs = [ 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'SOL/USDT:USDT', 'XRP/USDT:USDT', 'ADA/USDT:USDT', 'AVAX/USDT:USDT' ] # Add major pairs for multiple timeframes (needed for market breadth) for pair in major_pairs: for tf in ['15m', '1h', '4h']: informative_pairs.append((pair, tf)) # Add informative timeframes for current pairs for tf in ['15m', '1h', '4h']: informative_pairs.extend([(pair, tf) for pair in pairs]) # Remove duplicates while preserving order seen = set() unique_pairs = [] for pair in informative_pairs: if pair not in seen: seen.add(pair) unique_pairs.append(pair) return unique_pairs def _extract_dataframe_safely(self, result, pair_name): """ Safely extract DataFrame from get_analyzed_dataframe result """ try: # Handle tuple return (dataframe, metadata) if isinstance(result, tuple) and len(result) > 0: potential_df = result[0] if isinstance(potential_df, pd.DataFrame): return potential_df elif isinstance(potential_df, list) and len(potential_df) > 0: # If first element of tuple is a list, try to get DataFrame from it if isinstance(potential_df[0], pd.DataFrame): return potential_df[0] # Handle direct DataFrame return elif isinstance(result, pd.DataFrame): return result # Handle list return elif isinstance(result, list) and len(result) > 0: if isinstance(result[0], pd.DataFrame): return result[0] elif isinstance(result[0], tuple) and len(result[0]) > 0: # List of tuples case if isinstance(result[0][0], pd.DataFrame): return result[0][0] logger.debug(f" ❌ {pair_name}: Cannot extract DataFrame from result (type: {type(result)})") return None except Exception as e: logger.debug(f" ❌ {pair_name}: Error extracting DataFrame: {str(e)}") return None def calculate_market_breadth_weighted(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ 🔧 FIXED: Proper logging levels for market breadth calculation """ current_pair = metadata['pair'] is_futures = ':' in current_pair if is_futures: settlement = current_pair.split(':')[1] primary_pairs = [ (f"BTC/USDT:{settlement}", 0.40), (f"ETH/USDT:{settlement}", 0.25), (f"BNB/USDT:{settlement}", 0.10), (f"SOL/USDT:{settlement}", 0.08), (f"XRP/USDT:{settlement}", 0.07), (f"ADA/USDT:{settlement}", 0.05), (f"AVAX/USDT:{settlement}", 0.02), ] fallback_pairs = [ ("BTC/USDT", 0.40), ("ETH/USDT", 0.25), ("BNB/USDT", 0.10), ("SOL/USDT", 0.08), ("XRP/USDT", 0.07), ("ADA/USDT", 0.05), ("AVAX/USDT", 0.02), ] else: primary_pairs = [ ("BTC/USDT", 0.40), ("ETH/USDT", 0.25), ("BNB/USDT", 0.10), ("SOL/USDT", 0.08), ("XRP/USDT", 0.07), ("ADA/USDT", 0.05), ("AVAX/USDT", 0.02), ] fallback_pairs = [] bullish_weight = 0.0 bearish_weight = 0.0 neutral_weight = 0.0 total_weight = 0.0 pairs_checked = 0 logger.debug(f"🔍 DEBUG BREADTH for {metadata['pair']}:") # Changed from ERROR to DEBUG # Try primary pairs with proper logging levels for check_pair, weight in primary_pairs: try: pair_data = self.dp.get_pair_dataframe(check_pair, self.timeframe) if pair_data is None or pair_data.empty: logger.debug(f" ❌ {check_pair}: No data") # Changed from ERROR to DEBUG continue if len(pair_data) < 20: logger.debug(f" ❌ {check_pair}: Only {len(pair_data)} candles") # Changed from ERROR to DEBUG continue current_close = pair_data['close'].iloc[-1] sma20 = pair_data['close'].rolling(20).mean().iloc[-1] if pd.isna(current_close) or pd.isna(sma20) or sma20 <= 0: logger.debug(f" ❌ {check_pair}: Invalid data (close: {current_close}, sma: {sma20})") # Changed from ERROR to DEBUG continue # Enhanced threshold logic threshold = 1.002 # 0.2% buffer price_vs_sma = current_close / sma20 logger.debug(f" 📊 {check_pair}: Close=${current_close:.3f}, SMA20=${sma20:.3f}, Ratio={price_vs_sma:.6f}") # Changed from ERROR to DEBUG if current_close > sma20 * threshold: bullish_weight += weight trend_status = "BULLISH" elif current_close < sma20 / threshold: bearish_weight += weight trend_status = "BEARISH" else: neutral_weight += weight trend_status = "NEUTRAL" total_weight += weight pairs_checked += 1 logger.debug(f" ✅ {check_pair}: {trend_status} (weight: {weight:.2f})") # Changed from ERROR to DEBUG except Exception as e: logger.warning(f" 💥 {check_pair} ERROR: {str(e)}") # Changed from ERROR to WARNING continue # Enhanced calculation logic with proper logging logger.debug(f"📊 BREADTH CALCULATION:") # Changed from ERROR to DEBUG logger.debug(f" Bullish weight: {bullish_weight:.3f}") # Changed from ERROR to DEBUG logger.debug(f" Bearish weight: {bearish_weight:.3f}") # Changed from ERROR to DEBUG logger.debug(f" Neutral weight: {neutral_weight:.3f}") # Changed from ERROR to DEBUG logger.debug(f" Total weight: {total_weight:.3f}") # Changed from ERROR to DEBUG logger.debug(f" Pairs checked: {pairs_checked}") # Changed from ERROR to DEBUG if pairs_checked > 0 and total_weight > 0: # Include neutral in calculation directional_weight = bullish_weight + bearish_weight if directional_weight > 0: market_breadth = bullish_weight / directional_weight else: # All neutral = 50% market_breadth = 0.5 logger.debug(f" 🔧 All pairs neutral, setting breadth to 50%") # Changed from ERROR to DEBUG # Market direction if bullish_weight > bearish_weight: market_direction = 1 elif bearish_weight > bullish_weight: market_direction = -1 else: market_direction = 0 logger.info(f"📊 {metadata['pair']} Market Breadth: {market_breadth:.1%} ({pairs_checked} pairs)") # Changed from ERROR to INFO else: logger.info(f"🔄 {metadata['pair']} No breadth data - using fallback calculation") # Changed from ERROR to INFO # Fallback: Use current pair trend try: current_close = dataframe['close'].iloc[-1] sma20 = dataframe['close'].rolling(20).mean().iloc[-1] if not pd.isna(sma20) and sma20 > 0: pair_ratio = current_close / sma20 logger.debug(f" 📈 Current pair trend: {pair_ratio:.6f}") # Changed from ERROR to DEBUG if pair_ratio > 1.01: # 1% above SMA market_breadth = 0.65 market_direction = 1 logger.debug(f" 📈 Pair bullish -> 65% breadth") # Changed from ERROR to DEBUG elif pair_ratio < 0.99: # 1% below SMA market_breadth = 0.35 market_direction = -1 logger.debug(f" 📉 Pair bearish -> 35% breadth") # Changed from ERROR to DEBUG else: market_breadth = 0.5 market_direction = 0 logger.debug(f" 📊 Pair neutral -> 50% breadth") # Changed from ERROR to DEBUG else: market_breadth = 0.5 market_direction = 0 logger.debug(f" 🔧 Invalid pair data -> 50% breadth") # Changed from ERROR to DEBUG pairs_checked = 1 except Exception as e: logger.warning(f" 💥 Fallback failed: {e}") # Changed from ERROR to WARNING market_breadth = 0.5 market_direction = 0 pairs_checked = 0 # Prevent 0% breadth with proper logging if market_breadth == 0.0: logger.info(f"🔧 {metadata['pair']} Adjusting 0% breadth to 35% (extreme bearish)") # Changed from ERROR to INFO market_breadth = 0.35 # Assume bearish but not extreme market_direction = -1 logger.info(f"🎯 {metadata['pair']} Final Breadth: {market_breadth:.1%} (Direction: {market_direction})") # Changed from ERROR to INFO, improved formatting # Set dataframe values dataframe['market_breadth'] = market_breadth dataframe['market_direction'] = market_direction dataframe['bullish_weight'] = bullish_weight dataframe['bearish_weight'] = bearish_weight dataframe['total_weight'] = total_weight dataframe['breadth_pairs_checked'] = pairs_checked return dataframe def calculate_market_regime_aligned(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ 🔍 Determine the current market regime based on volatility, trend strength, and ADX. Uses BTC data as a proxy and aligns with the strategy's timeframe for consistency. Handles NumPy array issues by ensuring Pandas Series compatibility. """ lookback = self.regime_lookback_period.value current_pair = metadata['pair'] # Detect mode and set BTC pairs to try if ':' in current_pair: settlement = current_pair.split(':')[1] btc_pairs_to_try = [f'BTC/USDT:{settlement}', 'BTC/USDT'] else: btc_pairs_to_try = ['BTC/USDT', 'BTC/USD'] btc_data = None btc_pair_used = None # Attempt to fetch BTC data with appropriate logging for btc_pair in btc_pairs_to_try: try: logger.debug(f"🔍 {current_pair} Trying BTC pair: {btc_pair}") btc_data = self.dp.get_pair_dataframe(btc_pair, self.timeframe) if btc_data is not None and not btc_data.empty and len(btc_data) >= lookback: btc_pair_used = btc_pair logger.debug(f"✅ {current_pair} Successfully fetched {len(btc_data)} candles from {btc_pair}") break else: available_candles = len(btc_data) if btc_data is not None and not btc_data.empty else 0 logger.debug(f"⚠️ {current_pair} {btc_pair} insufficient: {available_candles}/{lookback} candles") except Exception as e: logger.debug(f"💥 {current_pair} Error accessing {btc_pair}: {str(e)}") continue # Fallback to neutral regime if no BTC data if btc_data is None or btc_data.empty or len(btc_data) < lookback: logger.info(f"🔄 {current_pair} No BTC data available, using neutral regime") dataframe['market_regime'] = 'neutral' dataframe['market_volatility'] = 0.02 dataframe['market_adx'] = 25.0 dataframe['btc_trend_strength'] = 0.0 return dataframe try: # Calculate market indicators with Pandas Series compatibility logger.debug(f"📊 {current_pair} Calculating regime using {btc_pair_used} ({len(btc_data)} candles)") # Daily volatility based on timeframe (assuming 30m) timeframe_minutes = 30 btc_returns = btc_data['close'].pct_change().to_frame('returns') market_volatility = btc_returns['returns'].rolling(window=lookback).std() * np.sqrt(1440 / timeframe_minutes) # Trend strength using ADX and price movement adx_values = ta.ADX(btc_data['high'], btc_data['low'], btc_data['close'], timeperiod=14) market_adx = pd.Series(adx_values, index=btc_data.index) if isinstance(adx_values, np.ndarray) else adx_values btc_mean = btc_data['close'].rolling(window=lookback).mean() btc_std = btc_data['close'].rolling(window=lookback).std() btc_trend_strength = ((btc_data['close'].iloc[-1] - btc_mean.iloc[-1]) / btc_std.iloc[-1] if btc_std.iloc[-1] != 0 else 0.0) # Ensure we have the latest values last_volatility = market_volatility.iloc[-1] if not market_volatility.empty else 0.02 last_adx = market_adx.iloc[-1] if not market_adx.empty else 25.0 last_trend_strength = btc_trend_strength if not pd.isna(btc_trend_strength) else 0.0 if last_volatility > 0.05 and last_trend_strength > 1.5 and last_adx > 35: regime = 'bull_run' elif last_volatility > 0.05 and last_trend_strength < -1.5 and last_adx > 35: regime = 'bear_market' elif last_volatility < 0.008 and last_adx < 15: regime = 'ranging' elif last_volatility > 0.08: # Only extreme volatility regime = 'high_volatility' else: regime = 'neutral' # Default to neutral instead of high_volatility # Apply to dataframe dataframe['market_regime'] = regime dataframe['market_volatility'] = last_volatility dataframe['market_adx'] = last_adx dataframe['btc_trend_strength'] = last_trend_strength logger.info(f"🏛️ {current_pair} Market regime: {regime}, Volatility: {last_volatility:.4f}, " f"ADX: {last_adx:.1f}, Trend Strength: {last_trend_strength:.2f}") return dataframe except Exception as e: logger.warning(f"💥 {current_pair} Regime calculation failed: {str(e)}, using neutral regime") dataframe['market_regime'] = 'neutral' dataframe['market_volatility'] = 0.02 dataframe['market_adx'] = 25.0 dataframe['btc_trend_strength'] = 0.0 return dataframe def apply_market_regime_filters(self, df: pd.DataFrame, any_long_signal: pd.Series, any_short_signal: pd.Series, metadata: dict) -> tuple: """ 🧠 INTELLIGENT: Adjust signal sensitivity based on market conditions This method modifies your entry signals based on the current market regime to improve timing and reduce stop losses. Returns: (enhanced_long_signal, enhanced_short_signal) """ try: # Get current market regime (use last available value) current_regime = df['market_regime'].iloc[-1] if 'market_regime' in df.columns else 'neutral' btc_strength = df['btc_strength'].iloc[-1] if 'btc_strength' in df.columns else 0 fear_greed = df['market_fear_greed'].iloc[-1] if 'market_fear_greed' in df.columns else 50 # =========================================== # REGIME-SPECIFIC SIGNAL ADJUSTMENTS # =========================================== if current_regime == 'bull_run': # 🚀 BULL RUN MODE: Favor longs, restrict shorts long_boost = df['rsi'] < 60 # More aggressive long entries short_restrict = df['rsi'] > 80 # Only extreme short entries enhanced_long_signal = any_long_signal & long_boost enhanced_short_signal = any_short_signal & short_restrict logger.info(f"{metadata['pair']} 🚀 BULL RUN MODE: Boosting longs, restricting shorts") elif current_regime == 'bear_market': # 🐻 BEAR MARKET MODE: Favor shorts, restrict longs short_boost = df['rsi'] > 40 # More aggressive short entries long_restrict = df['rsi'] < 20 # Only extreme long entries enhanced_long_signal = any_long_signal & long_restrict enhanced_short_signal = any_short_signal & short_boost logger.info(f"{metadata['pair']} 🐻 BEAR MARKET MODE: Boosting shorts, restricting longs") elif current_regime == 'high_volatility': # ⚡ HIGH VOLATILITY MODE: Only trade extremes extreme_oversold = df['rsi'] < 25 extreme_overbought = df['rsi'] > 75 enhanced_long_signal = any_long_signal & extreme_oversold enhanced_short_signal = any_short_signal & extreme_overbought logger.info(f"{metadata['pair']} ⚡ HIGH VOLATILITY MODE: Only extreme entries") elif current_regime == 'sideways': # 📊 SIDEWAYS MODE: Range trading, both directions OK range_long = (df['rsi'] < 35) & (df['close'] <= df.get('[3/8]P', df['close'])) range_short = (df['rsi'] > 65) & (df['close'] >= df.get('[5/8]P', df['close'])) enhanced_long_signal = any_long_signal & range_long enhanced_short_signal = any_short_signal & range_short logger.info(f"{metadata['pair']} 📊 SIDEWAYS MODE: Range trading activated") elif current_regime == 'transitional': # 🔄 TRANSITIONAL MODE: Be more selective quality_long = (df['rsi'] < 40) & (df['volume'] > df['volume'].rolling(20).mean() * 1.2) quality_short = (df['rsi'] > 60) & (df['volume'] > df['volume'].rolling(20).mean() * 1.2) enhanced_long_signal = any_long_signal & quality_long enhanced_short_signal = any_short_signal & quality_short logger.info(f"{metadata['pair']} 🔄 TRANSITIONAL MODE: Quality-focused entries") else: # neutral # 😐 NEUTRAL MODE: Standard filtering enhanced_long_signal = any_long_signal enhanced_short_signal = any_short_signal # =========================================== # FEAR/GREED ADJUSTMENTS # =========================================== if fear_greed > 75: # Extreme greed # Be more careful with longs, favor shorts enhanced_long_signal = enhanced_long_signal & (df['rsi'] < 30) logger.info(f"{metadata['pair']} 🤑 EXTREME GREED: Restricting longs") elif fear_greed < 25: # Extreme fear # Be more careful with shorts, favor longs enhanced_short_signal = enhanced_short_signal & (df['rsi'] > 70) logger.info(f"{metadata['pair']} 😨 EXTREME FEAR: Restricting shorts") return enhanced_long_signal, enhanced_short_signal except Exception as e: logger.error(f"{metadata['pair']} ❌ Market regime filter error: {e}") # Return original signals on error return any_long_signal, any_short_signal def calculate_total_market_cap_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ Calculate total crypto market cap trend FIXED: Handles futures format """ # Detect futures mode is_futures = ':' in metadata['pair'] if is_futures: settlement = metadata['pair'].split(':')[1] # Futures weighted pairs major_coins = { f"BTC/USDT:{settlement}": 0.4, f"ETH/USDT:{settlement}": 0.2, f"BNB/USDT:{settlement}": 0.1, f"SOL/USDT:{settlement}": 0.05, f"ADA/USDT:{settlement}": 0.05, } else: # Spot pairs major_coins = { "BTC/USDT": 0.4, "ETH/USDT": 0.2, "BNB/USDT": 0.1, "SOL/USDT": 0.05, "ADA/USDT": 0.05, } weighted_trend = 0 total_weight = 0 for coin_pair, weight in major_coins.items(): try: coin_data, _ = self.dp.get_analyzed_dataframe(coin_pair, self.timeframe) if coin_data.empty or len(coin_data) < self.total_mcap_ma_period.value: # Try spot equivalent if futures not found if is_futures and ':' in coin_pair: spot_pair = coin_pair.split(':')[0] coin_data, _ = self.dp.get_analyzed_dataframe(spot_pair, self.timeframe) if coin_data.empty: continue else: continue # Calculate trend using MA ma_period = self.total_mcap_ma_period.value current_price = coin_data['close'].iloc[-1] ma_value = coin_data['close'].rolling(ma_period).mean().iloc[-1] # Trend strength trend_strength = (current_price - ma_value) / ma_value weighted_trend += trend_strength * weight total_weight += weight except Exception as e: logger.debug(f"Could not process {coin_pair} for mcap trend: {e}") continue if total_weight > 0: mcap_trend = weighted_trend / total_weight else: mcap_trend = 0 # Classify trend if mcap_trend > 0.05: mcap_status = 'bullish' elif mcap_trend < -0.05: mcap_status = 'bearish' else: mcap_status = 'neutral' dataframe['mcap_trend'] = mcap_trend dataframe['mcap_status'] = mcap_status return dataframe def apply_correlation_filters(self, dataframe: pd.DataFrame, direction: str = 'long') -> pd.Series: """ Apply market correlation filters using ENHANCED correlation data Returns a boolean Series indicating whether market conditions are favorable """ conditions = pd.Series(True, index=dataframe.index) # =========================================== # ENHANCED BTC CORRELATION FILTER # =========================================== if self.enable_btc_correlation.value and 'btc_correlation_ok' in dataframe.columns: # Simple approach: Only trade when BTC conditions are favorable conditions &= dataframe['btc_correlation_ok'] # Optional: More restrictive based on BTC trend score if 'btc_trend_score' in dataframe.columns: if direction == 'long': # For longs: Allow when BTC trend score >= 2 (at least neutral) # OR when extremely oversold (RSI < 25) btc_long_ok = ( (dataframe['btc_trend_score'] >= 2) | (dataframe.get('rsi', 50) < 25) # Emergency oversold exception ) conditions &= btc_long_ok else: # short # For shorts: Allow when BTC trend score <= 3 (not too bullish) # OR when extremely overbought (RSI > 75) btc_short_ok = ( (dataframe['btc_trend_score'] <= 3) | (dataframe.get('rsi', 50) > 75) # Emergency overbought exception ) conditions &= btc_short_ok # =========================================== # MARKET BREADTH FILTER (Keep as is) # =========================================== if self.market_breadth_enabled.value and 'market_breadth' in dataframe.columns: if direction == 'long': # Long only when majority of market is bullish conditions &= (dataframe['market_breadth'] > self.market_breadth_threshold.value) else: # short # Short only when majority of market is bearish conditions &= (dataframe['market_breadth'] < (1 - self.market_breadth_threshold.value)) # =========================================== # MARKET CAP TREND FILTER (Keep as is) # =========================================== if self.total_mcap_filter_enabled.value and 'mcap_status' in dataframe.columns: if direction == 'long': conditions &= (dataframe['mcap_status'] != 'bearish') else: # short conditions &= (dataframe['mcap_status'] != 'bullish') # =========================================== # MARKET REGIME FILTER (Keep as is) # =========================================== if self.regime_filter_enabled.value and 'market_regime' in dataframe.columns: # Avoid trading in high volatility regimes conditions &= (dataframe['market_regime'] != 'high_volatility') return conditions def calculate_enhanced_trend_filters(self, dataframe: pd.DataFrame) -> pd.DataFrame: """ Enhanced trend detection to avoid choppy market entries """ # Multi-timeframe trend alignment dataframe['ema_5'] = ta.EMA(dataframe['close'], timeperiod=5) dataframe['ema_13'] = ta.EMA(dataframe['close'], timeperiod=13) dataframe['ema_21'] = ta.EMA(dataframe['close'], timeperiod=21) dataframe['ema_50'] = ta.EMA(dataframe['close'], timeperiod=50) # Trend alignment score dataframe['trend_alignment_bull'] = ( (dataframe['close'] > dataframe['ema_5']) & (dataframe['ema_5'] > dataframe['ema_13']) & (dataframe['ema_13'] > dataframe['ema_21']) & (dataframe['ema_21'] > dataframe['ema_50']) ).astype(int) dataframe['trend_alignment_bear'] = ( (dataframe['close'] < dataframe['ema_5']) & (dataframe['ema_5'] < dataframe['ema_13']) & (dataframe['ema_13'] < dataframe['ema_21']) & (dataframe['ema_21'] < dataframe['ema_50']) ).astype(int) # ADX for trend strength dataframe['adx'] = ta.ADX(dataframe, timeperiod=14) dataframe['strong_trend'] = dataframe['adx'] > 25 dataframe['very_strong_trend'] = dataframe['adx'] > 40 # Momentum indicators dataframe['macd'], dataframe['macd_signal'], dataframe['macd_hist'] = ta.MACD( dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9 ) # MACD momentum alignment dataframe['macd_bull'] = ( (dataframe['macd'] > dataframe['macd_signal']) & (dataframe['macd_hist'] > dataframe['macd_hist'].shift(1)) ) dataframe['macd_bear'] = ( (dataframe['macd'] < dataframe['macd_signal']) & (dataframe['macd_hist'] < dataframe['macd_hist'].shift(1)) ) # Price action patterns dataframe['higher_highs'] = ( (dataframe['high'] > dataframe['high'].shift(1)) & (dataframe['high'].shift(1) > dataframe['high'].shift(2)) ) dataframe['lower_lows'] = ( (dataframe['low'] < dataframe['low'].shift(1)) & (dataframe['low'].shift(1) < dataframe['low'].shift(2)) ) # Higher lows (bullish structure) dataframe['higher_lows'] = ( (dataframe['low'] > dataframe['low'].shift(1)) & (dataframe['low'].shift(1) > dataframe['low'].shift(2)) ) # Lower highs (bearish structure) dataframe['lower_highs'] = ( (dataframe['high'] < dataframe['high'].shift(1)) & (dataframe['high'].shift(1) < dataframe['high'].shift(2)) ) # Volatility filter using Bollinger Bands dataframe['bb_upper'], dataframe['bb_middle'], dataframe['bb_lower'] = ta.BBANDS( dataframe['close'], timeperiod=20, nbdevup=2.0, nbdevdn=2.0 ) # BB squeeze detection (low volatility) dataframe['bb_squeeze'] = ( (dataframe['bb_upper'] - dataframe['bb_lower']) / dataframe['bb_middle'] < 0.1 ) # BB expansion (high volatility breakout) dataframe['bb_expansion'] = ( (dataframe['bb_upper'] - dataframe['bb_lower']) / dataframe['bb_middle'] > 0.2 ) # Market regime classification dataframe['trending_up'] = ( dataframe['trend_alignment_bull'] & dataframe['strong_trend'] & dataframe['macd_bull'] & (~dataframe['bb_squeeze']) ) dataframe['trending_down'] = ( dataframe['trend_alignment_bear'] & dataframe['strong_trend'] & dataframe['macd_bear'] & (~dataframe['bb_squeeze']) ) dataframe['choppy_market'] = ( (dataframe['adx'] < 20) | dataframe['bb_squeeze'] | ( (~dataframe['trend_alignment_bull']) & (~dataframe['trend_alignment_bear']) ) ) # Market structure breaks dataframe['structure_break_bull'] = ( (dataframe['close'] > dataframe['high'].rolling(20).max().shift(1)) & dataframe['trending_up'] & (dataframe['volume'] > dataframe['volume'].rolling(20).mean() * 1.2) ) dataframe['structure_break_bear'] = ( (dataframe['close'] < dataframe['low'].rolling(20).min().shift(1)) & dataframe['trending_down'] & (dataframe['volume'] > dataframe['volume'].rolling(20).mean() * 1.2) ) # Trend momentum strength dataframe['trend_momentum'] = dataframe['close'].pct_change(10) dataframe['trend_momentum_strength'] = abs(dataframe['trend_momentum']) # Consistent trend detection (multiple timeframes) dataframe['short_term_trend'] = np.where( dataframe['close'] > dataframe['ema_5'], 1, np.where(dataframe['close'] < dataframe['ema_5'], -1, 0) ) dataframe['medium_term_trend'] = np.where( dataframe['close'] > dataframe['ema_21'], 1, np.where(dataframe['close'] < dataframe['ema_21'], -1, 0) ) dataframe['long_term_trend'] = np.where( dataframe['close'] > dataframe['ema_50'], 1, np.where(dataframe['close'] < dataframe['ema_50'], -1, 0) ) # Trend consistency score dataframe['trend_consistency'] = ( dataframe['short_term_trend'] + dataframe['medium_term_trend'] + dataframe['long_term_trend'] ) / 3 # Strong consistent trends dataframe['strong_consistent_uptrend'] = dataframe['trend_consistency'] > 0.6 dataframe['strong_consistent_downtrend'] = dataframe['trend_consistency'] < -0.6 # Trend change detection dataframe['trend_change_up'] = ( (dataframe['trend_consistency'] > 0) & (dataframe['trend_consistency'].shift(1) <= 0) ) dataframe['trend_change_down'] = ( (dataframe['trend_consistency'] < 0) & (dataframe['trend_consistency'].shift(1) >= 0) ) # Support and resistance levels based on EMAs dataframe['dynamic_resistance'] = np.maximum.reduce([ dataframe['ema_5'], dataframe['ema_13'], dataframe['ema_21'], dataframe['ema_50'] ]) dataframe['dynamic_support'] = np.minimum.reduce([ dataframe['ema_5'], dataframe['ema_13'], dataframe['ema_21'], dataframe['ema_50'] ]) # Price position relative to dynamic levels dataframe['above_all_emas'] = ( (dataframe['close'] > dataframe['ema_5']) & (dataframe['close'] > dataframe['ema_13']) & (dataframe['close'] > dataframe['ema_21']) & (dataframe['close'] > dataframe['ema_50']) ) dataframe['below_all_emas'] = ( (dataframe['close'] < dataframe['ema_5']) & (dataframe['close'] < dataframe['ema_13']) & (dataframe['close'] < dataframe['ema_21']) & (dataframe['close'] < dataframe['ema_50']) ) # Momentum divergence detection # Price vs RSI divergence price_peaks = dataframe['high'].rolling(10).max() price_troughs = dataframe['low'].rolling(10).min() dataframe['bullish_divergence'] = ( (dataframe['low'] == price_troughs) & (dataframe['low'] < dataframe['low'].shift(10)) & (dataframe['rsi'] > dataframe['rsi'].shift(10)) ) dataframe['bearish_divergence'] = ( (dataframe['high'] == price_peaks) & (dataframe['high'] > dataframe['high'].shift(10)) & (dataframe['rsi'] < dataframe['rsi'].shift(10)) ) return dataframe def calculate_trend_strength(self, df: pd.DataFrame) -> pd.DataFrame: """ Calculate trend strength to avoid entering against strong trends """ # Linear regression slope def calc_slope(series, period=10): """Calculate linear regression slope""" if len(series) < period: return 0 x = np.arange(period) y = series.iloc[-period:].values if np.isnan(y).any(): return 0 slope = np.polyfit(x, y, 1)[0] return slope # Calculate trend strength using multiple timeframes df['slope_5'] = df['close'].rolling(5).apply(lambda x: calc_slope(x, 5), raw=False) df['slope_10'] = df['close'].rolling(10).apply(lambda x: calc_slope(x, 10), raw=False) df['slope_20'] = df['close'].rolling(20).apply(lambda x: calc_slope(x, 20), raw=False) # Normalize slopes by price df['trend_strength_5'] = df['slope_5'] / df['close'] * 100 df['trend_strength_10'] = df['slope_10'] / df['close'] * 100 df['trend_strength_20'] = df['slope_20'] / df['close'] * 100 # Combined trend strength df['trend_strength'] = (df['trend_strength_5'] + df['trend_strength_10'] + df['trend_strength_20']) / 3 # Trend classification strong_up_threshold = self.trend_strength_threshold.value strong_down_threshold = -self.trend_strength_threshold.value df['strong_uptrend'] = df['trend_strength'] > strong_up_threshold df['strong_downtrend'] = df['trend_strength'] < strong_down_threshold df['ranging'] = (df['trend_strength'].abs() < strong_up_threshold * 0.5) return df @property def protections(self): prot = [{"method": "CooldownPeriod", "stop_duration_candles": self.cooldown_lookback.value}] if self.use_stop_protection.value: prot.append({ "method": "StoplossGuard", "lookback_period_candles": 72, "trade_limit": 2, "stop_duration_candles": self.stop_duration.value, "only_per_pair": False, }) return prot def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: Optional[float], max_stake: float, leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: # =========================================== # STAKE LIMITS DEFINIEREN # =========================================== # Maximaler Stake pro Trade (in USDT) - kannst du anpassen MAX_STAKE_PER_TRADE = self.max_stake_per_trade # Maximaler Stake basierend auf Portfolio try: total_portfolio = self.wallets.get_total_stake_amount() MAX_STAKE_PERCENTAGE = self.max_portfolio_percentage_per_trade max_stake_from_portfolio = total_portfolio * MAX_STAKE_PERCENTAGE except: # Fallback wenn wallets nicht verfügbar max_stake_from_portfolio = MAX_STAKE_PER_TRADE total_portfolio = 1000.0 # Dummy value # Market condition check für volatility-based stake reduction (DEIN CODE) dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) if not dataframe.empty: last_candle = dataframe.iloc[-1] current_volatility = last_candle.get("volatility", 0.02) # Reduziere Stake in hochvolatilen Märkten if current_volatility > 0.05: # 5% ATR/Price ratio volatility_reduction = min(0.5, current_volatility * 10) # Max 50% reduction proposed_stake *= (1 - volatility_reduction) logger.info(f"{pair} Stake reduced by {volatility_reduction:.1%} due to high volatility ({current_volatility:.2%})") # DCA Multiplier Berechnung (DEIN CODE) calculated_max_dca_multiplier = 1.0 if self.position_adjustment_enable: num_safety_orders = int(self.max_safety_orders.value) volume_scale = self.safety_order_volume_scale.value if num_safety_orders > 0 and volume_scale > 0: current_order_relative_size = 1.0 for _ in range(num_safety_orders): current_order_relative_size *= volume_scale calculated_max_dca_multiplier += current_order_relative_size else: logger.warning(f"{pair}: Could not calculate max_dca_multiplier due to " f"invalid max_safety_orders ({num_safety_orders}) or " f"safety_order_volume_scale ({volume_scale}). Defaulting to 1.0.") else: logger.debug(f"{pair}: Position adjustment not enabled. max_dca_multiplier is 1.0.") if calculated_max_dca_multiplier > 0: stake_amount = proposed_stake / calculated_max_dca_multiplier # =========================================== # NEUE STAKE LIMITS ANWENDEN # =========================================== # Verschiedene Limits prüfen final_stake = min( stake_amount, MAX_STAKE_PER_TRADE, max_stake_from_portfolio, max_stake # Freqtrade's max_stake ) # Bestimme welches Limit gegriffen hat limit_reason = "calculated" if final_stake == MAX_STAKE_PER_TRADE: limit_reason = "max_per_trade" elif final_stake == max_stake_from_portfolio: limit_reason = "portfolio_percentage" elif final_stake == max_stake: limit_reason = "freqtrade_max" logger.info(f"{pair} Initial stake calculated: {final_stake:.8f} (Proposed: {proposed_stake:.8f}, " f"Calculated Max DCA Multiplier: {calculated_max_dca_multiplier:.2f}, " f"Limited by: {limit_reason}, Portfolio %: {(final_stake/total_portfolio)*100:.1f}%)") # Min stake prüfen (DEIN CODE) if min_stake is not None and final_stake < min_stake: logger.info(f"{pair} Initial stake {final_stake:.8f} was below min_stake {min_stake:.8f}. " f"Adjusting to min_stake. Consider tuning your DCA parameters or proposed stake.") final_stake = min_stake return final_stake else: # Fallback (DEIN CODE) logger.warning( f"{pair} Calculated max_dca_multiplier is {calculated_max_dca_multiplier:.2f}, which is invalid. " f"Using proposed_stake: {proposed_stake:.8f}") return proposed_stake def custom_entry_price(self, pair: str, trade: Optional[Trade], current_time: datetime, proposed_rate: float, entry_tag: Optional[str], side: str, **kwargs) -> float: """ 🎯 IMPROVED: Wait for better entry prices to reduce immediate stop loss risk """ dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) if dataframe.empty: return proposed_rate last_candle = dataframe.iloc[-1] # Calculate volatility-adjusted entry atr = last_candle.get('atr', 0) if atr == 0: atr = (last_candle['high'] - last_candle['low']) # Get MML levels for better entry timing mml_25 = last_candle.get('[2/8]P', last_candle['close']) mml_50 = last_candle.get('[4/8]P', last_candle['close']) mml_75 = last_candle.get('[6/8]P', last_candle['close']) if side == "long": # For longs: Try to enter closer to support or on pullbacks if entry_tag in ["MML_50_Reclaim", "MML_Support_Bounce"]: # Enter slightly above support with buffer support_buffer = atr * 0.2 # 20% of ATR buffer optimal_entry = max(mml_25, mml_50) + support_buffer entry_price = min(proposed_rate, optimal_entry) elif entry_tag == "MML_Bullish_Breakout": # For breakouts: Enter on slight pullback, not at peak pullback_entry = proposed_rate - (atr * 0.1) # 10% ATR pullback entry_price = max(pullback_entry, proposed_rate * 0.999) # Max 0.1% below else: # Conservative entry for other signals entry_price = proposed_rate - (atr * 0.05) # Small discount elif side == "short": # For shorts: Try to enter closer to resistance if entry_tag in ["MML_50_Breakdown", "MML_Resistance_Reject"]: # Enter slightly below resistance with buffer resistance_buffer = atr * 0.2 optimal_entry = min(mml_75, mml_50) - resistance_buffer entry_price = max(proposed_rate, optimal_entry) elif entry_tag == "MML_Bearish_Breakdown": # For breakdowns: Enter on slight bounce, not at bottom bounce_entry = proposed_rate + (atr * 0.1) entry_price = min(bounce_entry, proposed_rate * 1.001) else: entry_price = proposed_rate + (atr * 0.05) else: entry_price = proposed_rate # Ensure entry price is reasonable max_deviation = proposed_rate * 0.005 # Max 0.5% from proposed if side == "long": entry_price = max(entry_price, proposed_rate - max_deviation) else: entry_price = min(entry_price, proposed_rate + max_deviation) logger.info(f"{pair} Optimized entry: {entry_price:.6f} (was {proposed_rate:.6f}, tag: {entry_tag})") return entry_price # ═══════════════════════════════════════════════════════════════ # 🛑 CRITICAL FIX: TIME-BASED STOP LOSS LOOSENING # ═══════════════════════════════════════════════════════════════ def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ 🔧 REVOLUTIONARY: Time-based stop loss loosening + MML support Based on your backtest results: - ROI hits at ~1.5 hours with 4% profit - Stop loss hits at ~1.25 hours with -6.5% loss - Need to give trades more time to develop before stopping out """ dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) if dataframe.empty or 'atr' not in dataframe.columns or len(dataframe) < 10: logger.warning(f"{pair} Insufficient data for dynamic stop loss. Using conservative default: -0.03") return -0.03 # Conservative default last_candle = dataframe.iloc[-1] last_atr = last_candle.get("atr", 0) # Handle missing or invalid ATR if pd.isna(last_atr) or last_atr == 0: valid_atr = dataframe["atr"].dropna() if not valid_atr.empty: last_atr = valid_atr.rolling(window=5).mean().iloc[-1] logger.info(f"{pair} Using smoothed ATR: {last_atr:.8f}") else: logger.warning(f"{pair} No valid ATR found. Using fallback stop loss -0.03") return -0.03 if last_atr == 0 or current_rate == 0: logger.warning(f"{pair} ATR or rate is 0. Using fallback stop loss -0.03") return -0.03 # ⏰ TIME IN TRADE CALCULATION time_in_trade = (current_time - trade.open_date_utc).total_seconds() / 3600 # Hours # Get MML levels for dynamic support/resistance if trade.is_short: # For shorts: Use MML resistance as dynamic stop resistance_level = last_candle.get('[6/8]P', current_rate * 1.02) if pd.isna(resistance_level): resistance_level = current_rate * 1.02 mml_stop_distance = abs((resistance_level - current_rate) / current_rate) else: # For longs: Use MML support as dynamic stop support_level = last_candle.get('[2/8]P', current_rate * 0.98) if pd.isna(support_level): support_level = current_rate * 0.98 mml_stop_distance = abs((current_rate - support_level) / current_rate) # Base ATR stop loss calculation base_atr_stop = (last_atr / current_rate) * 2.0 # Start with 2x ATR # 🕐 CRITICAL: TIME-BASED STOP LOSS LOOSENING # Based on your data: Need more room in first 1.5 hours if time_in_trade < 0.25: # First 15 minutes - tightest (but not too tight) time_multiplier = 1.2 # Give some room immediately elif time_in_trade < 0.5: # 15-30 minutes - slight loosening time_multiplier = 1.5 elif time_in_trade < 1.0: # 30-60 minutes - more room time_multiplier = 2.0 elif time_in_trade < 1.5: # 1-1.5 hours - CRITICAL PERIOD time_multiplier = 2.5 # Much more room where stop losses currently hit elif time_in_trade < 2.0: # 1.5-2 hours - ROI development time time_multiplier = 3.0 # Maximum room for ROI to develop elif time_in_trade < 4.0: # 2-4 hours - long-term holds time_multiplier = 2.8 # Slight tightening but still wide else: # 4+ hours - prevent runaway losses time_multiplier = 2.5 # 📈 PROFIT-BASED ADJUSTMENTS (override time loosening when profitable) if current_profit > 0.08: # 8%+ profit - lock in gains aggressively profit_stop = max(-0.015, -(base_atr_stop * 0.3)) # Very tight elif current_profit > 0.06: # 6%+ profit - good protection profit_stop = max(-0.02, -(base_atr_stop * 0.5)) elif current_profit > 0.04: # 4%+ profit - moderate protection profit_stop = max(-0.025, -(base_atr_stop * 0.7)) elif current_profit > 0.02: # 2%+ profit - light protection profit_stop = max(-0.03, -(base_atr_stop * 1.0)) elif current_profit > 0: # Any profit - don't tighten profit_stop = -(base_atr_stop * time_multiplier) else: # Apply time-based loosening for losing trades profit_stop = -(base_atr_stop * time_multiplier) # 🎯 MML-BASED STOP LOSS ADJUSTMENT # Use MML levels as natural stop areas, but don't make stops too wide mml_adjusted_stop = min( abs(profit_stop), # Don't make tighter than profit-based max(mml_stop_distance * 1.3, 0.02), # At least 2%, max 30% past MML 0.10 # Never wider than 10% ) # 🛡️ SAFETY BOUNDARIES # Ensure stop loss isn't too tight or too wide if current_profit <= 0: # For losing trades: Ensure minimum room based on time if time_in_trade < 1.5: # Critical period - be generous min_stop = -0.08 # Minimum 8% room else: min_stop = -0.06 # Standard minimum final_stop = -max(mml_adjusted_stop, abs(min_stop)) else: # For profitable trades: Use calculated stop final_stop = -mml_adjusted_stop # Final safety check: Never tighter than -1.5% unless big profit if final_stop > -0.015 and current_profit < 0.06: final_stop = -0.015 # Never wider than -12% final_stop = max(final_stop, -0.12) # 📊 DETAILED LOGGING logger.info(f"{pair} 🛑 DYNAMIC STOP LOSS:") logger.info(f" ⏰ Time in trade: {time_in_trade:.2f}h") logger.info(f" 📈 Current profit: {current_profit:.2%}") logger.info(f" 🔢 Time multiplier: {time_multiplier}x") logger.info(f" 💰 ATR base: {base_atr_stop:.4f}") logger.info(f" 🎯 MML distance: {mml_stop_distance:.4f}") logger.info(f" 🛡️ Final stop: {final_stop:.4f}") return final_stop def minimal_roi_market_adjusted(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float) -> Dict[int, float]: """ Dynamically adjust ROI based on market conditions """ dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if dataframe.empty: return self.minimal_roi last_candle = dataframe.iloc[-1] market_score = last_candle.get('market_score', 0.5) market_regime = last_candle.get('market_regime', 'normal') # Copy original ROI adjusted_roi = self.minimal_roi.copy() # In high volatility or bear market, take profits earlier if market_regime == 'high_volatility' or market_score < 0.3: # Reduce all ROI targets by 20% adjusted_roi = {k: v * 0.8 for k, v in adjusted_roi.items()} logger.info(f"{pair} ROI adjusted down due to market conditions") # In strong bull market, let winners run elif market_score > 0.7 and market_regime == 'strong_trend': # Increase ROI targets by 20% adjusted_roi = {k: v * 1.2 for k, v in adjusted_roi.items()} logger.info(f"{pair} ROI adjusted up due to bullish market") return adjusted_roi def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, **kwargs) -> Optional[float]: # Get market conditions dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) if not dataframe.empty: last_candle = dataframe.iloc[-1] btc_trend = last_candle.get('btc_trend', 0) market_score = last_candle.get('market_score', 0.5) market_breadth = last_candle.get('market_breadth', 0.5) rsi = last_candle.get('rsi', 50) else: market_score = 0.5 market_breadth = 0.5 rsi = 50 count_of_entries = trade.nr_of_successful_entries count_of_exits = trade.nr_of_successful_exits # === ENHANCED PROFIT TAKING BASED ON MARKET CONDITIONS === # More aggressive profit taking in overbought market if market_score > 0.75 and current_profit > 0.15 and count_of_exits == 0: logger.info(f"{trade.pair} Taking profit early due to market greed: {market_score:.2f}") amount_to_sell = (trade.amount * current_rate) * 0.33 # Sell 33% return -amount_to_sell # Original profit taking logic (keep as is) if current_profit > 0.25 and count_of_exits == 0: logger.info(f"{trade.pair} Taking partial profit (25%) at {current_profit:.2%}") amount_to_sell = (trade.amount * current_rate) * 0.25 return -amount_to_sell # === BEAR MARKET PROFIT TAKING === # Nimm Gewinne schneller mit im Bärenmarkt if not trade.is_short and btc_trend < 0: # Long im Downtrend if current_profit > 0.10 and count_of_exits == 0: # Statt 0.25 logger.info(f"{trade.pair} Bear market quick profit taking at {current_profit:.2%}") amount_to_sell = (trade.amount * current_rate) * 0.5 # 50% verkaufen return -amount_to_sell if current_profit > 0.40 and count_of_exits == 1: logger.info(f"{trade.pair} Taking additional profit (33%) at {current_profit:.2%}") amount_to_sell = (trade.amount * current_rate) * (1 / 3) return -amount_to_sell # === 🔧 ENHANCED DCA LOGIC WITH STRICT CONTROLS === if not self.position_adjustment_enable: return None # 🛑 USE STRATEGY VARIABLES FOR DCA LIMITS max_dca_for_pair = self.max_dca_orders max_total_stake = self.max_total_stake_per_pair max_single_dca = self.max_single_dca_amount # 🛑 CHECK: Already too many DCA orders? if count_of_entries > max_dca_for_pair: logger.info(f"{trade.pair} 🛑 MAX DCA REACHED: {count_of_entries}/{max_dca_for_pair}") return None # 🛑 CHECK: Total stake amount already too high? if trade.stake_amount >= max_total_stake: logger.info(f"{trade.pair} 🛑 MAX STAKE REACHED: {trade.stake_amount:.2f}/{max_total_stake} USDT") return None # Block DCA in crashing market if market_breadth < 0.25 and trade.is_short == False: logger.info(f"{trade.pair} Blocking DCA due to bearish market breadth: {market_breadth:.2%}") return None # More conservative DCA triggers in volatile markets if dataframe.iloc[-1].get('market_regime') == 'high_volatility': dca_multiplier = 1.5 # Require 50% deeper drawdown else: dca_multiplier = 1.0 # Apply multiplier to original DCA logic trigger = self.initial_safety_order_trigger.value * dca_multiplier if (current_profit > trigger / 2.0 and count_of_entries == 1) or \ (current_profit > trigger and count_of_entries == 2) or \ (current_profit > trigger * 1.5 and count_of_entries == 3): logger.info(f"{trade.pair} DCA condition not met. Current profit {current_profit:.2%} above threshold") return None # 🛑 CHECK: Original max safety orders (falls du das noch nutzt) if hasattr(self, 'max_safety_orders') and count_of_entries >= self.max_safety_orders.value + 1: logger.info(f"{trade.pair} 🛑 Original max_safety_orders reached: {count_of_entries}") return None try: filled_entry_orders = trade.select_filled_orders(trade.entry_side) if not filled_entry_orders: logger.error(f"{trade.pair} No filled entry orders found for DCA calculation") return None last_order_cost = filled_entry_orders[-1].cost # 🔧 USE STRATEGY VARIABLE FOR DCA SIZING base_dca_amount = max_single_dca # Use strategy variable # Progressive DCA sizing (each DCA gets smaller!) dca_multipliers = [1.0, 0.8, 0.6] # 1st: 5 USDT, 2nd: 4 USDT, 3rd: 3 USDT if count_of_entries <= len(dca_multipliers): current_multiplier = dca_multipliers[count_of_entries - 1] else: current_multiplier = 0.5 # Fallback für unerwartete Orders # Calculate DCA amount dca_stake_amount = base_dca_amount * current_multiplier # 🛑 HARD CAP: Never exceed remaining budget remaining_budget = max_total_stake - trade.stake_amount if dca_stake_amount > remaining_budget: if remaining_budget > 1: # Only proceed if at least 1 USDT remaining dca_stake_amount = remaining_budget logger.info(f"{trade.pair} 🔧 DCA capped to remaining budget: {dca_stake_amount:.2f} USDT") else: logger.info(f"{trade.pair} 🛑 Insufficient remaining budget: {remaining_budget:.2f} USDT") return None # Standard min/max stake checks if min_stake is not None and dca_stake_amount < min_stake: logger.warning(f"{trade.pair} DCA below min_stake. Adjusting to {min_stake:.2f} USDT") dca_stake_amount = min_stake if max_stake is not None and (trade.stake_amount + dca_stake_amount) > max_stake: available_for_dca = max_stake - trade.stake_amount if available_for_dca > (min_stake or 0): dca_stake_amount = available_for_dca logger.warning(f"{trade.pair} DCA reduced due to max_stake: {dca_stake_amount:.2f} USDT") else: logger.warning(f"{trade.pair} Cannot DCA due to max_stake limit") return None # 🔧 FINAL SAFETY CHECK new_total_stake = trade.stake_amount + dca_stake_amount if new_total_stake > max_total_stake: logger.error(f"{trade.pair} 🚨 SAFETY VIOLATION: Would exceed max total stake!") return None logger.info(f"{trade.pair} ✅ DCA #{count_of_entries}: +{dca_stake_amount:.2f} USDT " f"(Total: {new_total_stake:.2f}/{max_total_stake} USDT)") return dca_stake_amount except IndexError: logger.error(f"Error calculating DCA stake for {trade.pair}: IndexError accessing last_order") return None except Exception as e: logger.error(f"Error calculating DCA stake for {trade.pair}: {e}") return None def leverage(self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, side: str, **kwargs) -> float: window_size = self.leverage_window_size.value dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) if len(dataframe) < window_size: logger.warning( f"{pair} Not enough data ({len(dataframe)} candles) to calculate dynamic leverage (requires {window_size}). Using proposed: {proposed_leverage}") return proposed_leverage close_prices_series = dataframe["close"].tail(window_size) high_prices_series = dataframe["high"].tail(window_size) low_prices_series = dataframe["low"].tail(window_size) base_leverage = self.leverage_base.value rsi_array = ta.RSI(close_prices_series, timeperiod=14) atr_array = ta.ATR(high_prices_series, low_prices_series, close_prices_series, timeperiod=14) sma_array = ta.SMA(close_prices_series, timeperiod=20) macd_output = ta.MACD(close_prices_series, fastperiod=12, slowperiod=26, signalperiod=9) current_rsi = rsi_array[-1] if rsi_array.size > 0 and not np.isnan(rsi_array[-1]) else 50.0 current_atr = atr_array[-1] if atr_array.size > 0 and not np.isnan(atr_array[-1]) else 0.0 current_sma = sma_array[-1] if sma_array.size > 0 and not np.isnan(sma_array[-1]) else current_rate current_macd_hist = 0.0 if isinstance(macd_output, pd.DataFrame): if not macd_output.empty and 'macdhist' in macd_output.columns: valid_macdhist_series = macd_output['macdhist'].dropna() if not valid_macdhist_series.empty: current_macd_hist = valid_macdhist_series.iloc[-1] # Apply rules based on indicators if side == "long": if current_rsi < self.leverage_rsi_low.value: base_leverage *= self.leverage_long_increase_factor.value elif current_rsi > self.leverage_rsi_high.value: base_leverage *= self.leverage_long_decrease_factor.value if current_atr > 0 and current_rate > 0: if (current_atr / current_rate) > self.leverage_atr_threshold_pct.value: base_leverage *= self.leverage_volatility_decrease_factor.value if current_macd_hist > 0: base_leverage *= self.leverage_long_increase_factor.value if current_sma > 0 and current_rate < current_sma: base_leverage *= self.leverage_long_decrease_factor.value adjusted_leverage = round(max(1.0, min(base_leverage, max_leverage)), 2) logger.info( f"{pair} Dynamic Leverage: {adjusted_leverage:.2f} (Base: {base_leverage:.2f}, RSI: {current_rsi:.2f}, " f"ATR: {current_atr:.4f}, MACD Hist: {current_macd_hist:.4f}, SMA: {current_sma:.4f})") return adjusted_leverage def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ 🔧 FIXED: Enhanced indicator calculations with proper syntax """ dataframe['actual_stoploss'] = np.nan dataframe['follow_price_level'] = np.nan # =========================================== # YOUR EXISTING INDICATORS (KEEP ALL) # =========================================== dataframe["atr"] = ta.ATR(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=7) if dataframe["atr"].isna().all(): logger.warning(f"No valid ATR calculated. Filling with 0 temporarily.") dataframe["atr"] = 0.0 else: # Fill NaN with rolling mean of last 5 valid values dataframe["atr"] = dataframe["atr"].fillna(dataframe["atr"].rolling(window=5, min_periods=1).mean()) logger.info(f"ATR values: {', '.join(dataframe['atr'].tail(5).map('{:.6f}'.format))}") dataframe["ema20"] = ta.EMA(dataframe["close"], timeperiod=20) dataframe["ema50"] = ta.EMA(dataframe["close"], timeperiod=50) dataframe["rsi"] = ta.RSI(dataframe["close"], timeperiod=7) dataframe["rsi_overbought"] = (dataframe["rsi"] > 70).astype(int) dataframe["rsi_oversold"] = (dataframe["rsi"] < 30).astype(int) dataframe["plus_di"] = ta.PLUS_DI(dataframe) dataframe["minus_di"] = ta.MINUS_DI(dataframe) dataframe["DI_values"] = dataframe["plus_di"] - dataframe["minus_di"] dataframe["DI_cutoff"] = 0 # Extrema detection (your existing code) extrema_order = self.indicator_extrema_order.value dataframe["maxima"] = ( dataframe["close"] == dataframe["close"].shift(1).rolling(window=extrema_order).max() ).astype(int) dataframe["minima"] = ( dataframe["close"] == dataframe["close"].shift(1).rolling(window=extrema_order).min() ).astype(int) dataframe["s_extrema"] = 0 dataframe.loc[dataframe["minima"] == 1, "s_extrema"] = -1 dataframe.loc[dataframe["maxima"] == 1, "s_extrema"] = 1 # Heikin-Ashi and rolling extrema logger.debug(f"{metadata['pair']} 🛠️ Calculating Heikin-Ashi and extrema") dataframe["ha_close"] = (dataframe["open"] + dataframe["high"] + dataframe["low"] + dataframe["close"]) / 4 if dataframe["ha_close"].isna().all(): logger.error(f"{metadata['pair']} 🚨 All ha_close values are NaN!") dataframe["minh2"], dataframe["maxh2"] = calculate_minima_maxima(dataframe, self.h2.value) dataframe["minh1"], dataframe["maxh1"] = calculate_minima_maxima(dataframe, self.h1.value) dataframe["minh0"], dataframe["maxh0"] = calculate_minima_maxima(dataframe, self.h0.value) dataframe["mincp"], dataframe["maxcp"] = calculate_minima_maxima(dataframe, self.cp.value) logger.debug(f"{metadata['pair']} ✅ Extrema calculated: minh2={dataframe['minh2'].sum()}, maxh2={dataframe['maxh2'].sum()}") # Murrey Math levels (your existing code) mml_window = self.indicator_mml_window.value murrey_levels = self.calculate_rolling_murrey_math_levels_optimized(dataframe, window_size=mml_window) for level_name in MML_LEVEL_NAMES: if level_name in murrey_levels: dataframe[level_name] = murrey_levels[level_name] else: dataframe[level_name] = dataframe["close"] # MML oscillator (your existing code) mml_4_8 = dataframe.get("[4/8]P") mml_plus_3_8 = dataframe.get("[+3/8]P") mml_minus_3_8 = dataframe.get("[-3/8]P") if mml_4_8 is not None and mml_plus_3_8 is not None and mml_minus_3_8 is not None: osc_denominator = (mml_plus_3_8 - mml_minus_3_8).replace(0, np.nan) dataframe["mmlextreme_oscillator"] = 100 * ((dataframe["close"] - mml_4_8) / osc_denominator) else: dataframe["mmlextreme_oscillator"] = np.nan # DI Catch and checks (your existing code) dataframe["DI_catch"] = np.where(dataframe["DI_values"] > dataframe["DI_cutoff"], 0, 1) rolling_window_threshold = self.indicator_rolling_window_threshold.value dataframe["minima_sort_threshold"] = dataframe["close"].rolling( window=rolling_window_threshold, min_periods=1 ).min() dataframe["maxima_sort_threshold"] = dataframe["close"].rolling( window=rolling_window_threshold, min_periods=1 ).max() rolling_check_window = self.indicator_rolling_check_window.value dataframe["minima_check"] = ( dataframe["minima"].rolling(window=rolling_check_window, min_periods=1).sum() == 0 ).astype(int) dataframe["maxima_check"] = ( dataframe["maxima"].rolling(window=rolling_check_window, min_periods=1).sum() == 0 ).astype(int) # Your existing volatility calculations dataframe["volatility_range"] = dataframe["high"] - dataframe["low"] dataframe["avg_volatility"] = dataframe["volatility_range"].rolling(window=10).mean() dataframe["avg_volume"] = dataframe["volume"].rolling(window=20).mean() # =========================================== # 🚀 ENHANCED MARKET CORRELATION (PROPER ORDER) # =========================================== # Enhanced BTC correlation FIRST if self.enable_btc_correlation.value: logger.info(f"🔍 {metadata['pair']} Calculating enhanced BTC correlation...") dataframe = self.enhanced_btc_correlation_filter(dataframe, metadata) # Legacy BTC correlation (keep for compatibility) if self.btc_correlation_enabled.value or self.btc_trend_filter_enabled.value: logger.info(f"📈 {metadata['pair']} Calculating legacy BTC correlation...") dataframe = self.calculate_market_correlation_simple(dataframe, metadata) # =========================================== # 🔧 FIXED: Market breadth calculation # =========================================== if self.market_breadth_enabled.value: logger.info(f"📊 {metadata['pair']} Calculating weighted market breadth...") try: # Use the fixed market breadth method dataframe = self.calculate_market_breadth_weighted(dataframe, metadata) # Debug breadth calculation if not dataframe.empty and 'market_breadth' in dataframe.columns and 'breadth_pairs_checked' in dataframe.columns: breadth = dataframe['market_breadth'].iloc[-1] pairs_checked = dataframe['breadth_pairs_checked'].iloc[-1] logger.info(f"🎯 {metadata['pair']} Breadth result: {breadth:.2%} ({pairs_checked} pairs)") else: logger.warning(f"⚠️ {metadata['pair']} Market breadth debugging skipped: invalid DataFrame or missing columns") except Exception as e: logger.warning(f"⚠️ {metadata['pair']} Market breadth calculation failed: {str(e)}") # Fallback to simple values dataframe['market_breadth'] = 0.5 dataframe['market_score'] = 0.0 dataframe['breadth_pairs_checked'] = 0 # =========================================== # 🔧 FIXED: Market regime calculation # =========================================== if self.regime_filter_enabled.value: logger.info(f"🏛️ {metadata['pair']} Calculating market regime...") try: # Check parameter availability if hasattr(self, 'regime_lookback_period'): lookback = self.regime_lookback_period.value logger.info(f"📊 {metadata['pair']} Using lookback period: {lookback}") else: logger.warning(f"⚠️ {metadata['pair']} Missing regime_lookback_period parameter, using default") lookback = 48 # Call the regime calculation dataframe = self.calculate_market_regime_aligned(dataframe, metadata) # Debug regime calculation if len(dataframe) > 0: regime = dataframe['market_regime'].iloc[-1] volatility = dataframe.get('market_volatility', [0.02]).iloc[-1] adx = dataframe.get('market_adx', [25]).iloc[-1] logger.info(f"🎯 {metadata['pair']} Regime result: {regime} (vol: {volatility:.3f}, adx: {adx:.1f})") except Exception as e: logger.warning(f"⚠️ {metadata['pair']} Market regime calculation failed: {e}") # Fallback values dataframe['market_regime'] = 'neutral' dataframe['market_volatility'] = 0.02 dataframe['market_adx'] = 25 # Market cap trend (keep existing) if self.total_mcap_filter_enabled.value: logger.info(f"💰 {metadata['pair']} Calculating market cap trend...") try: dataframe = self.calculate_total_market_cap_trend(dataframe, metadata) except Exception as e: logger.warning(f"⚠️ {metadata['pair']} Market cap trend calculation failed: {e}") dataframe['mcap_trend'] = 0 # =========================================== # 🆕 ENHANCED SIGNAL FILTERING INDICATORS # =========================================== # Enhanced trend detection try: dataframe = self.calculate_enhanced_trend_filters(dataframe) logger.debug(f"✅ {metadata['pair']} Enhanced trend filters calculated") except Exception as e: logger.warning(f"⚠️ {metadata['pair']} Enhanced trend filters failed: {e}") # Add basic trend fallbacks dataframe['trend_alignment_bull'] = 0.5 dataframe['trend_alignment_bear'] = 0.5 # Market state detection try: dataframe = self.detect_market_state(dataframe) logger.debug(f"✅ {metadata['pair']} Market state detected") except Exception as e: logger.warning(f"⚠️ {metadata['pair']} Market state detection failed: {e}") # Add basic state fallbacks dataframe['market_state'] = 'normal' # Signal strength calculation try: dataframe = self.calculate_signal_strength(dataframe) logger.debug(f"✅ {metadata['pair']} Signal strength calculated") except Exception as e: logger.warning(f"⚠️ {metadata['pair']} Signal strength calculation failed: {e}") # Add basic strength fallbacks dataframe['signal_strength'] = 0.5 # =========================================== # 🔧 FIXED: ENHANCED MARKET SCORE CALCULATION # =========================================== # Base market score dataframe['market_score'] = 0.5 # BTC correlation component if 'btc_correlation' in dataframe.columns: dataframe['market_score'] += dataframe['btc_correlation'] * 0.15 # 🔧 FIXED: Removed the "6" that was causing the syntax error if 'btc_correlation_ok' in dataframe.columns: # Enhanced BTC correlation gets higher weight dataframe['market_score'] += dataframe['btc_correlation_ok'].astype(float) * 0.20 # Market breadth component (higher weight due to weighting) if 'market_breadth' in dataframe.columns: breadth_contribution = (dataframe['market_breadth'] - 0.5) * 0.25 dataframe['market_score'] += breadth_contribution # Market cap trend component if 'mcap_trend' in dataframe.columns: dataframe['market_score'] += dataframe['mcap_trend'] * 0.10 # Market regime component (aligned names) if 'market_regime' in dataframe.columns: regime_scores = { 'bull_run': 0.85, # Strong bullish 'bear_market': 0.15, # Strong bearish 'sideways': 0.50, # Neutral ranging 'high_volatility': 0.30, # Caution - volatile 'transitional': 0.50, # Neutral transition 'neutral': 0.50 # Default neutral } dataframe['regime_score'] = dataframe['market_regime'].map( lambda x: regime_scores.get(x, 0.5) ) dataframe['market_score'] += (dataframe['regime_score'] - 0.5) * 0.15 # Enhanced trend alignment (if available) if 'trend_alignment_bull' in dataframe.columns: dataframe['market_score'] += dataframe['trend_alignment_bull'] * 0.10 dataframe['market_score'] -= dataframe['trend_alignment_bear'] * 0.10 # MACD momentum (if available) if 'macd_bull' in dataframe.columns: dataframe['market_score'] += dataframe['macd_bull'].astype(int) * 0.05 dataframe['market_score'] -= dataframe['macd_bear'].astype(int) * 0.05 # Volume confirmation component if 'volume_confirmation' in dataframe.columns: volume_contribution = (dataframe['volume_confirmation'] - 0.5) * 0.10 dataframe['market_score'] += volume_contribution # Ensure market_score stays within bounds dataframe['market_score'] = dataframe['market_score'].clip(0, 1) # =========================================== # 🔧 SIGNAL QUALITY INDICATORS # =========================================== # Signal persistence dataframe['signal_persistence'] = 0 for window in [3, 5, 10]: dataframe[f'bullish_persistence_{window}'] = ( (dataframe['rsi'] < 50).rolling(window).sum() / window ) dataframe[f'bearish_persistence_{window}'] = ( (dataframe['rsi'] > 50).rolling(window).sum() / window ) # Volume momentum dataframe['volume_momentum'] = ( dataframe['volume'].rolling(3).mean() / dataframe['volume'].rolling(20).mean() ).fillna(1.0) # Price momentum strength dataframe['price_momentum_strength'] = abs( dataframe['close'].pct_change(5) ).rolling(10).mean() # =========================================== # 🔧 BREADTH-SPECIFIC INDICATORS # =========================================== # Breadth momentum (rate of change) if 'market_breadth' in dataframe.columns: dataframe['breadth_momentum'] = dataframe['market_breadth'].diff() dataframe['breadth_trend'] = dataframe['market_breadth'].rolling(5).mean() # Breadth divergence detection price_direction = (dataframe['close'] > dataframe['close'].shift(5)).astype(int) breadth_direction = (dataframe['market_breadth'] > 0.5).astype(int) dataframe['breadth_price_divergence'] = abs(price_direction - breadth_direction) # Market regime strength if 'market_adx' in dataframe.columns: dataframe['regime_strength'] = dataframe['market_adx'] / 50 # Normalize ADX dataframe['strong_regime'] = dataframe['market_adx'] > 30 # =========================================== # 📊 FINAL LOGGING & VALIDATION # =========================================== try: # Log final market conditions if len(dataframe) > 0: last_row = dataframe.iloc[-1] market_breadth = last_row.get('market_breadth', 0.5) market_regime = last_row.get('market_regime', 'unknown') market_score = last_row.get('market_score', 0.5) btc_ok = last_row.get('btc_correlation_ok', True) logger.info(f"📊 FINAL MARKET CONDITIONS for {metadata['pair']}:") logger.info(f" 🌐 Breadth: {market_breadth:.2%}") logger.info(f" 🏛️ Regime: {market_regime}") logger.info(f" 📈 Score: {market_score:.2%}") logger.info(f" ₿ BTC OK: {btc_ok}") # 🔍 REGIME DEBUGGING if market_regime == 'unknown': logger.warning(f"⚠️ {metadata['pair']} Market regime is unknown - check calculation method") else: logger.error(f"🚨 {metadata['pair']} Empty dataframe after indicators!") except Exception as e: logger.error(f"💥 {metadata['pair']} Final logging error: {e}") return dataframe def enhanced_btc_correlation_filter(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ 🛡️ ROBUST: BTC correlation with comprehensive error handling and logging """ pair = metadata['pair'] # Skip BTC correlation for BTC pairs if 'BTC' in pair: logger.info(f"{pair} 🟡 Skipping BTC correlation for BTC pair") dataframe['btc_correlation_ok'] = True return dataframe try: # =========================================== # BTC DATA RETRIEVAL WITH VALIDATION # =========================================== current_pair = metadata['pair'] if ':' in current_pair: # Futures mode - extract settlement currency settlement = current_pair.split(':')[1] btc_pair = f'BTC/USDT:{settlement}' else: # Spot mode btc_pair = 'BTC/USDT' logger.info(f"{current_pair} Using BTC pair: {btc_pair}") # Now use the detected format btc_15m = self.dp.get_pair_dataframe(btc_pair, '15m') btc_1h = self.dp.get_pair_dataframe(btc_pair, '1h') btc_4h = self.dp.get_pair_dataframe(btc_pair, '4h') # Comprehensive validation btc_data_available = False btc_timeframes_ok = [] for tf_name, btc_df in [('15m', btc_15m), ('1h', btc_1h), ('4h', btc_4h)]: if not btc_df.empty and len(btc_df) > 30: btc_timeframes_ok.append(tf_name) btc_data_available = True logger.info(f"{pair} ✅ BTC {tf_name} data: {len(btc_df)} candles") else: logger.warning(f"{pair} ❌ BTC {tf_name} data insufficient: {len(btc_df) if not btc_df.empty else 0} candles") if not btc_data_available: logger.error(f"{pair} 🚨 NO BTC DATA AVAILABLE - DISABLING BTC CORRELATION") logger.error(f"{pair} 📊 This means backtest results may not match live trading!") # Fallback: Allow all trades (backtest won't match live) dataframe['btc_correlation_ok'] = True dataframe['btc_status'] = 'NO_DATA' return dataframe # Use best available timeframe (prefer 1h, fallback to others) if '1h' in btc_timeframes_ok: btc_main = btc_1h main_tf = '1h' elif '4h' in btc_timeframes_ok: btc_main = btc_4h main_tf = '4h' elif '15m' in btc_timeframes_ok: btc_main = btc_15m main_tf = '15m' else: raise Exception("No usable BTC timeframe data") logger.info(f"{pair} 🎯 Using BTC {main_tf} data for correlation ({len(btc_main)} candles)") # =========================================== # BTC TREND ANALYSIS # =========================================== # Calculate BTC indicators btc_main['sma_24'] = btc_main['close'].rolling(24).mean() btc_main['sma_50'] = btc_main['close'].rolling(50).mean() btc_main['rsi'] = ta.RSI(btc_main['close'], timeperiod=14) # BTC trend determination btc_last = btc_main.iloc[-1] btc_prev = btc_main.iloc[-2] if len(btc_main) > 1 else btc_last # Multiple trend indicators btc_above_sma24 = btc_last['close'] > btc_last['sma_24'] btc_above_sma50 = btc_last['close'] > btc_last['sma_50'] btc_sma_bullish = btc_last['sma_24'] > btc_last['sma_50'] btc_price_momentum = (btc_last['close'] - btc_prev['close']) / btc_prev['close'] # BTC trend classification btc_bullish_signals = sum([ btc_above_sma24, btc_above_sma50, btc_sma_bullish, btc_price_momentum > 0.001, # 0.1% positive momentum 30 < btc_last['rsi'] < 70 # Healthy RSI range ]) btc_trend_strong = btc_bullish_signals >= 4 btc_trend_weak = btc_bullish_signals <= 1 btc_trend_neutral = not btc_trend_strong and not btc_trend_weak # =========================================== # CORRELATION FILTER LOGIC # =========================================== # Conservative approach: Allow trades when BTC is not strongly bearish btc_correlation_ok = not btc_trend_weak # Enhanced logging logger.info(f"{pair} 🔍 BTC CORRELATION ANALYSIS:") logger.info(f" 📊 BTC Price: ${btc_last['close']:.0f}") logger.info(f" 📈 Above SMA24: {btc_above_sma24}") logger.info(f" 📈 Above SMA50: {btc_above_sma50}") logger.info(f" 🎯 Bullish Signals: {btc_bullish_signals}/5") logger.info(f" 🚦 BTC Trend: {'STRONG' if btc_trend_strong else 'WEAK' if btc_trend_weak else 'NEUTRAL'}") logger.info(f" ✅ Correlation OK: {btc_correlation_ok}") # Apply to dataframe dataframe['btc_correlation_ok'] = btc_correlation_ok dataframe['btc_trend_score'] = btc_bullish_signals dataframe['btc_status'] = f"OK_{main_tf}" return dataframe except Exception as e: logger.error(f"{pair} 💥 BTC CORRELATION ERROR: {str(e)}") logger.error(f"{pair} 🚨 FALLING BACK TO NO BTC FILTER") # Emergency fallback dataframe['btc_correlation_ok'] = True dataframe['btc_status'] = 'ERROR' return dataframe def safe_time_diff(self, time1, time2): """Safe time difference calculation""" try: # Handle various timestamp formats if isinstance(time1, (int, float)): time1 = pd.Timestamp.fromtimestamp(time1) if isinstance(time2, (int, float)): time2 = pd.Timestamp.fromtimestamp(time2) return (pd.Timestamp(time1) - pd.Timestamp(time2)).total_seconds() / 60 except Exception as e: logger.warning(f"Time diff error: {e}") return 0 def populate_entry_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ 🔄 Optimized Implementation: Simplified long and short signals with adaptive filters 🔧 Incorporates community suggestions with relaxed constraints for longs and mirrored for shorts Enhanced with stricter trend confirmation, ADX, volume rising, and volatility filters """ pair = metadata['pair'] # Safe initialization for re-entry system self.get_or_init_reentry_data(pair) # =========================================== # INITIALIZE COLUMNS # =========================================== df["enter_long"] = 0 df["enter_short"] = 0 df["enter_tag"] = "" df["exit_long"] = 0 df["exit_short"] = 0 # =========================================== # PRELIMINARY CALCULATIONS (Enhanced) # =========================================== # Add ADX for trend strength (new: compute here if not in populate_indicators) if 'adx' not in df.columns: df['adx'] = ta.ADX(df['high'], df['low'], df['close'], timeperiod=14) df['plus_di'] = ta.PLUS_DI(df['high'], df['low'], df['close'], timeperiod=14) df['minus_di'] = ta.MINUS_DI(df['high'], df['low'], df['close'], timeperiod=14) # Basic volatility filter (new: skip high-vol entries unless extreme signal) volatility_high = df['atr'] / df['close'] > 0.03 # >3% ATR/close df['volume_rising'] = (df['volume'] > df['volume'].shift(1)) & \ (df['volume'] > df['volume'].rolling(5).mean() * 1.2) # New: stricter volume # Existing market condition calculations df['green_candle'] = (df['close'] > df['open']).astype(int) df['red_candle'] = (df['close'] < df['open']).astype(int) df['consecutive_green'] = df['green_candle'].rolling(3).sum() df['consecutive_red'] = df['red_candle'].rolling(3).sum() df['trend_consistency'] = (df['close'] / df['close'].shift(10) - 1).fillna(0) df['market_score'] = df.get('market_score', 0.5) # Conditions for long signals df['below_midpoint'] = df['close'] < df['[4/8]P'] lookback_candles = getattr(self, 'lookback_candles', 15) min_drop_pct = getattr(self, 'min_drop_pct', 0.015) df['highest_close'] = df['close'].rolling(lookback_candles).max() df['pct_drop_from_high'] = (df['highest_close'] - df['close']) / df['highest_close'] df['significant_pullback'] = df['pct_drop_from_high'] > min_drop_pct momentum_lookback = getattr(self, 'momentum_lookback', 3) max_drop_pct = getattr(self, 'max_drop_pct', 0.03) df['recent_drop'] = (df['close'].shift(1) - df['close'].rolling(momentum_lookback).min()) / df['close'].shift(1) df['momentum_safe'] = df['recent_drop'] <= max_drop_pct # Conditions for short signals df['above_midpoint'] = df['close'] > df['[4/8]P'] min_rise_pct = getattr(self, 'min_rise_pct', 0.015) df['lowest_close'] = df['close'].rolling(lookback_candles).min() df['pct_rise_from_low'] = (df['close'] - df['lowest_close']) / df['lowest_close'] df['significant_rise'] = df['pct_rise_from_low'] > min_rise_pct max_rise_pct = getattr(self, 'max_rise_pct', 0.03) df['recent_rise'] = (df['close'].rolling(momentum_lookback).max() - df['close'].shift(1)) / df['close'].shift(1) df['momentum_safe_short'] = df['recent_rise'] <= max_rise_pct # MML analysis (existing) bullish_mml = ( (df["close"] > df["[6/8]P"]) | ((df["close"] > df["[4/8]P"]) & (df["close"].shift(5) < df["[4/8]P"].shift(5))) ) bearish_mml = ( (df["close"] < df["[2/8]P"]) | ((df["close"] < df["[4/8]P"]) & (df["close"].shift(5) > df["[4/8]P"].shift(5))) ) range_bound = ( (df["close"] >= df["[2/8]P"]) & (df["close"] <= df["[6/8]P"]) & (~bullish_mml) & (~bearish_mml) ) mml_support_bounce = ( ((df["low"] <= df["[2/8]P"]) & (df["close"] > df["[2/8]P"])) | ((df["low"] <= df["[4/8]P"]) & (df["close"] > df["[4/8]P"])) ) mml_resistance_reject = ( ((df["high"] >= df["[6/8]P"]) & (df["close"] < df["[6/8]P"])) | ((df["high"] >= df["[8/8]P"]) & (df["close"] < df["[8/8]P"])) ) # =========================================== # LONG SIGNALS (Optimized with enhancements: volume_rising, volatility checks, ADX for trend_cont) # =========================================== long_signal_mml_breakout = ( (df["close"] > df["[6/8]P"]) & (df["close"].shift(1) <= df["[6/8]P"].shift(1)) & (df["volume"] > df["volume"].rolling(20).mean() * 1.1) & # Fixed typo, integrated volume_rising loosely (df["rsi"] > 50) & (df["close"] > df["ema20"]) & (df["below_midpoint"] | (df["rsi"].shift(1) < df["rsi"])) & (df['volume_rising']) & # New: stricter volume rising (~volatility_high | (df["rsi"] < 30)) # New: volatility filter, allow if oversold ) long_signal_support_bounce = ( mml_support_bounce & (df["rsi"] < 45) & (df["close"] > df["close"].shift(1)) & (df["volume"] > df["volume"].rolling(10).mean() * 1.1) & (df["minima"] == 1) & (df["significant_pullback"]) & (df["momentum_safe"]) & (df['volume_rising']) & # New: volume surge (~volatility_high | (df["rsi"] < 25)) # New: volatility filter ) long_signal_50_reclaim = ( (df["close"] > df["[4/8]P"]) & (df["close"].shift(1) <= df["[4/8]P"].shift(1)) & (df["rsi"] > 50) & (df["volume"] > df["volume"].rolling(15).mean() * 1.1) & (df["close"] > df["ema20"]) & (df["market_score"] > 0.2) & (df['adx'] > 20) & # New: some trend strength (df['volume_rising']) # New: volume confirmation ) long_signal_range_long = ( range_bound & (df["low"] <= df["[2/8]P"]) & (df["close"] > df["[2/8]P"]) & (df["rsi"] < 50) & (df["close"] > df["close"].shift(1)) & (df["minima"] == 1) & (df["volume"] > df["volume"].rolling(10).mean() * 1.1) & (df["significant_pullback"]) & (df['volume_rising']) & # New: volume surge (~volatility_high) # New: skip in high vol ) long_signal_trend_continuation = ( bullish_mml & (df["close"] > df["[5/8]P"]) & (df["rsi"].between(40, 65)) & (df["close"] > df["ema20"]) & (df["volume"] > df["volume"].rolling(15).mean() * 1.1) & (df['adx'] > 25) & # New: strong trend required (df['plus_di'] > df['minus_di']) & # New: bullish direction (df["close"] > df["[4/8]P"]) & # New: above MML midpoint (redundant but emphasized) (df['volume_rising']) & # New: volume confirmation (~volatility_high) # New: skip in high vol ) long_signal_extreme_oversold = ( (df["low"] <= df["[1/8]P"]) & (df["close"] > df["[1/8]P"]) & (df["rsi"] < 35) & (df["volume"] > df["volume"].rolling(20).mean() * 1.2) & (df["close"] > df["close"].shift(1)) & # New: starting to bounce (df["minima"] == 1) & (df["significant_pullback"]) & (df["momentum_safe"]) & (df['volume_rising']) & # New: volume surge (~volatility_high | (df["rsi"] < 20)) # New: allow in high vol only if very oversold ) any_long_signal = ( long_signal_mml_breakout | long_signal_support_bounce | long_signal_50_reclaim | long_signal_range_long | long_signal_trend_continuation | long_signal_extreme_oversold ) # =========================================== # SHORT SIGNALS (Optimized with symmetric enhancements) # =========================================== if self.can_short: short_signal_bearish_breakdown = ( (df["close"] < df["[2/8]P"]) & (df["close"].shift(1) >= df["[2/8]P"].shift(1)) & (df["volume"] > df["volume"].rolling(20).mean() * 1.1) & # Relaxed volume (df["rsi"] < 50) & # Relaxed RSI (df["close"] < df["ema20"]) & (df["above_midpoint"] | (df["rsi"].shift(1) > df["rsi"])) & # Avoid bottoms (df['volume_rising']) & # New: stricter volume rising (~volatility_high | (df["rsi"] > 70)) # New: volatility filter, allow if overbought ) short_signal_resistance_reject = ( mml_resistance_reject & (df["rsi"] > 50) & # Relaxed from rsi_overbought (df["close"] < df["close"].shift(1)) & (df["volume"] > df["volume"].rolling(10).mean() * 1.1) & (df["maxima"] == 1) & (df["significant_rise"]) & # Pullback (rise) filter (df["momentum_safe_short"]) & # Momentum filter (df['volume_rising']) & # New: volume surge (~volatility_high | (df["rsi"] > 75)) # New: volatility filter ) short_signal_50_breakdown = ( (df["close"] < df["[4/8]P"]) & (df["close"].shift(1) >= df["[4/8]P"].shift(1)) & (df["rsi"] < 50) & # Relaxed RSI (df["volume"] > df["volume"].rolling(15).mean() * 1.1) & (df["close"] < df["ema20"]) & (df["market_score"] < 0.8) & # No pullback/momentum filter (df['adx'] > 20) & # New: some trend strength (df['volume_rising']) # New: volume confirmation ) short_signal_range_short = ( range_bound & (df["high"] >= df["[6/8]P"]) & (df["close"] < df["[6/8]P"]) & (df["rsi"] > 50) & # Relaxed RSI (df["close"] < df["close"].shift(1)) & (df["maxima"] == 1) & (df["volume"] > df["volume"].rolling(10).mean() * 1.1) & (df["significant_rise"]) & # Pullback filter (df['volume_rising']) & # New: volume surge (~volatility_high) # New: skip in high vol ) short_signal_trend_continuation = ( bearish_mml & (df["close"] < df["[3/8]P"]) & (df["rsi"].between(35, 60)) & # Wider RSI range (df["close"] < df["ema20"]) & (df["volume"] > df["volume"].rolling(15).mean() * 1.1) & # No pullback/momentum filter (df['adx'] > 25) & # New: strong trend required (df['minus_di'] > df['plus_di']) & # New: bearish direction (df["close"] < df["[4/8]P"]) & # New: below MML midpoint (df['volume_rising']) & # New: volume confirmation (~volatility_high) # New: skip in high vol ) short_signal_extreme_overbought = ( (df["high"] >= df["[7/8]P"]) & (df["close"] < df["[7/8]P"]) & (df["rsi"] > 65) & (df["volume"] > df["volume"].rolling(20).mean() * 1.2) & (df["close"] < df["close"].shift(1)) & # New: starting to reject (df["maxima"] == 1) & (df["significant_rise"]) & (df["momentum_safe_short"]) & # Both filters (df['volume_rising']) & # New: volume surge (~volatility_high | (df["rsi"] > 80)) # New: allow in high vol only if very overbought ) # Keep short exposure focused on exhaustion / rejection setups. # The broader breakdown-style shorts clustered into stoplosses in # bullish tape during forward and 60d validation. any_short_signal = ( short_signal_resistance_reject | short_signal_range_short | short_signal_extreme_overbought ) else: any_short_signal = pd.Series([False] * len(df), index=df.index) # Apply market regime filter if 'market_regime' in df.columns: regime = df['market_regime'].iloc[-1] if regime == 'bear_market': any_long_signal = any_long_signal & (df['rsi'] < 35) any_short_signal = any_short_signal & (df['rsi'] > 45) # Looser for shorts elif regime == 'bull_run': any_long_signal = any_long_signal & (df['rsi'] > 40) any_short_signal = any_short_signal & (df['rsi'] > 65) # Stricter for shorts logger.info(f"{pair} 🏛️ Market regime: {regime}") # =========================================== # RE-ENTRY AND MARKET FILTERS (Unchanged) # =========================================== if self.enable_reentry.value: try: current_time = df.index[-1] if not df.empty else datetime.now() recent_exits = self.get_recent_exits(pair, current_time) logger.info(f"{pair} 🔄 Re-entry check: {len(recent_exits)} recent exits") reentry_long_signals = pd.Series([False] * len(df), index=df.index) reentry_short_signals = pd.Series([False] * len(df), index=df.index) market_score = df['market_score'].fillna(0.5) market_breadth = df.get('market_breadth', pd.Series([0.5] * len(df), index=df.index)).fillna(0.5) btc_ok = df.get('btc_correlation_ok', pd.Series([True] * len(df), index=df.index)).fillna(True) for exit_time, direction, reason, profit in recent_exits: try: time_since_exit = self.safe_time_diff(current_time, exit_time) if pair in self.reentry_count: current_count = self.reentry_count[pair].get(direction, 0) max_reentries = int(self.max_reentries_per_direction.value) if current_count >= max_reentries: logger.info(f"{pair} 🛑 Max {direction} re-entries: {current_count}") continue except Exception as e: logger.warning(f"{pair} Re-entry processing error: {e}, skipping this exit") continue if direction == 'long': if (reason == 'roi' and profit > 0.015 and self.quick_reentry_cooldown.value <= time_since_exit <= 30): roi_long_conditions = ( (df["rsi"] < 55) & (df["close"] > df["[2/8]P"]) & (df["volume"] > df["volume"].rolling(10).mean()) & (market_score > 0.4) & (market_breadth > 0.3) & btc_ok ) reentry_long_signals.iloc[-3:] = reentry_long_signals.iloc[-3:] | roi_long_conditions.iloc[-3:] if roi_long_conditions.iloc[-1]: logger.info(f"{pair} 🎯 LONG ROI re-entry: {profit:.2%} profit") elif (self.reentry_cooldown_minutes.value <= time_since_exit <= 120): original_long_active = ( long_signal_mml_breakout | long_signal_support_bounce | long_signal_50_reclaim | long_signal_range_long | long_signal_extreme_oversold ) std_long_conditions = ( original_long_active & (df["rsi"] < 45) & (df["close"] > df["[1/8]P"]) & (df["volume"] > df["volume"].rolling(10).mean()) & (market_breadth > 0.35) & btc_ok ) reentry_long_signals.iloc[-5:] = reentry_long_signals.iloc[-5:] | std_long_conditions.iloc[-5:] if std_long_conditions.iloc[-1]: logger.info(f"{pair} 🔄 LONG standard re-entry ({time_since_exit:.0f}m)") elif direction == 'short' and self.can_short: if (reason == 'roi' and profit > 0.015 and self.quick_reentry_cooldown.value <= time_since_exit <= 30): roi_short_conditions = ( (df["rsi"] > 45) & (df["close"] < df["[6/8]P"]) & (df["volume"] > df["volume"].rolling(10).mean()) & (market_score < 0.6) & (market_breadth < 0.7) & btc_ok ) reentry_short_signals.iloc[-3:] = reentry_short_signals.iloc[-3:] | roi_short_conditions.iloc[-3:] if roi_short_conditions.iloc[-1]: logger.info(f"{pair} 🎯 SHORT ROI re-entry: {profit:.2%} profit") elif (self.reentry_cooldown_minutes.value <= time_since_exit <= 120): original_short_active = ( short_signal_bearish_breakdown | short_signal_resistance_reject | short_signal_50_breakdown | short_signal_range_short | short_signal_extreme_overbought ) std_short_conditions = ( original_short_active & (df["rsi"] > 55) & (df["close"] < df["[7/8]P"]) & (df["volume"] > df["volume"].rolling(10).mean()) & (market_breadth < 0.65) & btc_ok ) reentry_short_signals.iloc[-5:] = reentry_short_signals.iloc[-5:] | std_short_conditions.iloc[-5:] if std_short_conditions.iloc[-1]: logger.info(f"{pair} 🔄 SHORT standard re-entry ({time_since_exit:.0f}m)") any_long_signal = any_long_signal | reentry_long_signals any_short_signal = any_short_signal | reentry_short_signals if pair not in self.reentry_count: self.reentry_count[pair] = {'long': 0, 'short': 0} reentry_long_count = reentry_long_signals.sum() reentry_short_count = reentry_short_signals.sum() if reentry_long_count > 0: self.reentry_count[pair]['long'] += 1 logger.info(f"{pair} 📈 LONG re-entry #{self.reentry_count[pair]['long']} ({reentry_long_count} signals)") if reentry_short_count > 0: self.reentry_count[pair]['short'] += 1 logger.info(f"{pair} 📉 SHORT re-entry #{self.reentry_count[pair]['short']} ({reentry_short_count} signals)") except Exception as e: logger.error(f"{pair} 💥 Re-entry error: {e}") # Apply market breadth filter if 'market_breadth' in df.columns: breadth = df['market_breadth'].fillna(0.5) breadth_long_ok = breadth > 0.25 breadth_short_ok = breadth < 0.60 any_long_signal = any_long_signal & breadth_long_ok any_short_signal = any_short_signal & breadth_short_ok logger.info(f"{pair} 📊 Market breadth: {breadth.iloc[-1]:.1%}") # Apply BTC correlation filter if 'btc_correlation_ok' in df.columns: btc_ok = df['btc_correlation_ok'].fillna(True).astype(bool) # any_long_signal = any_long_signal & btc_ok # Commented out to disable BTC correlation filter # any_short_signal = any_short_signal & btc_ok # Commented out to disable BTC correlation filter logger.info(f"{pair} ₿ BTC correlation: {'✅' if btc_ok.iloc[-1] else '❌'}") # Apply market score filter if 'market_score' in df.columns: score = df['market_score'].fillna(0.5) score_long_ok = score > 0.20 score_short_ok = score < 0.65 any_long_signal = any_long_signal & score_long_ok any_short_signal = any_short_signal & score_short_ok logger.info(f"{pair} 📈 Market score: {score.iloc[-1]:.1%}") # =========================================== # FINAL SIGNAL ASSIGNMENT # =========================================== df.loc[any_long_signal, "enter_long"] = 1 if self.can_short: df.loc[any_short_signal, "enter_short"] = 1 # Long signal tags df.loc[any_long_signal & long_signal_mml_breakout, "enter_tag"] = "MML_Breakout_75" df.loc[any_long_signal & long_signal_support_bounce & (df["enter_tag"] == ""), "enter_tag"] = "Support_Bounce" df.loc[any_long_signal & long_signal_50_reclaim & (df["enter_tag"] == ""), "enter_tag"] = "MML_50_Reclaim" df.loc[any_long_signal & long_signal_range_long & (df["enter_tag"] == ""), "enter_tag"] = "Range_Long" df.loc[any_long_signal & long_signal_trend_continuation & (df["enter_tag"] == ""), "enter_tag"] = "Trend_Cont" df.loc[any_long_signal & long_signal_extreme_oversold & (df["enter_tag"] == ""), "enter_tag"] = "Extreme_Oversold" # Short signal tags if self.can_short: df.loc[any_short_signal & short_signal_bearish_breakdown, "enter_tag"] = "Bear_Breakdown_25" df.loc[any_short_signal & short_signal_resistance_reject & (df["enter_tag"] == ""), "enter_tag"] = "Resistance_Reject" df.loc[any_short_signal & short_signal_50_breakdown & (df["enter_tag"] == ""), "enter_tag"] = "MML_50_Breakdown" df.loc[any_short_signal & short_signal_range_short & (df["enter_tag"] == ""), "enter_tag"] = "Range_Short" df.loc[any_short_signal & short_signal_trend_continuation & (df["enter_tag"] == ""), "enter_tag"] = "Bear_Trend_Cont" df.loc[any_short_signal & short_signal_extreme_overbought & (df["enter_tag"] == ""), "enter_tag"] = "Extreme_Overbought" # Add re-entry suffix if self.enable_reentry.value and hasattr(self, 'reentry_count') and pair in self.reentry_count: total_reentries = self.reentry_count[pair]['long'] + self.reentry_count[pair]['short'] if total_reentries > 0: reentry_suffix = f"_RE{total_reentries}" mask = df["enter_tag"] != "" df.loc[mask, "enter_tag"] = df.loc[mask, "enter_tag"] + reentry_suffix # Debug logging logger.info(f"{pair} 🎯 FINAL SIGNALS: Long={any_long_signal.sum()}, Short={any_short_signal.sum()}") # =========================================== # 🔧 FINAL VALIDATION & STATISTICS # =========================================== # Count different signal types for analysis final_long_count = any_long_signal.sum() final_short_count = any_short_signal.sum() if final_long_count > 0: signal_breakdown = { 'support_bounce': long_signal_support_bounce.sum(), '50_reclaim': long_signal_50_reclaim.sum(), 'range_long': long_signal_range_long.sum(), 'trend_continuation': long_signal_trend_continuation.sum(), 'extreme_oversold': long_signal_extreme_oversold.sum(), # 'pure_reversal': long_signal_pure_reversal.sum() # Commented out: undefined } logger.info(f"{pair} 📋 LONG SIGNAL BREAKDOWN:") for signal_type, count in signal_breakdown.items(): if count > 0: logger.info(f" {signal_type}: {count}") if final_short_count > 0 and self.can_short: short_breakdown = { 'bearish_breakdown': short_signal_bearish_breakdown.sum(), 'resistance_reject': short_signal_resistance_reject.sum(), '50_breakdown': short_signal_50_breakdown.sum(), 'range_short': short_signal_range_short.sum(), 'trend_continuation': short_signal_trend_continuation.sum(), 'extreme_overbought': short_signal_extreme_overbought.sum(), # 'pure_reversal': short_signal_pure_reversal.sum() # Commented out: undefined } logger.info(f"{pair} 📋 SHORT SIGNAL BREAKDOWN:") for signal_type, count in short_breakdown.items(): if count > 0: logger.info(f" {signal_type}: {count}") # Final safety check - ensure no NaN values in signals df["enter_long"] = df["enter_long"].fillna(0).astype(int) df["enter_short"] = df["enter_short"].fillna(0).astype(int) df["enter_tag"] = df["enter_tag"].fillna("") return df def calculate_mml_signal_strengths(self, df: pd.DataFrame, bullish_mml, bearish_mml, mml_support_bounce, mml_resistance_reject, range_bound) -> pd.DataFrame: """ 📊 Calculate signal strength for your MML signals """ df['long_signal_strength'] = 0.0 df['short_signal_strength'] = 0.0 # =========================================== # LONG SIGNAL STRENGTH COMPONENTS # =========================================== long_strength_components = [] # MML structure strength (40% weight) mml_long_strength = ( bullish_mml.astype(float) * 0.4 + # Strong bullish structure mml_support_bounce.astype(float) * 0.3 + # Support bounce ((df["close"] > df["[4/8]P"]).astype(float) * 0.2) + # Above midpoint ((df["close"] > df["[6/8]P"]).astype(float) * 0.1) # Above 75% ) long_strength_components.append(mml_long_strength.clip(0, 0.4)) # RSI strength (20% weight) rsi_long_strength = ( ((df["rsi"] < 30).astype(float) * 0.2) + # Oversold ((df["rsi"] < 40).astype(float) * 0.15) + # Getting oversold ((df["rsi"] > df["rsi"].shift(1)).astype(float) * 0.05) # RSI improving ).clip(0, 0.2) long_strength_components.append(rsi_long_strength) # Volume strength (20% weight) volume_long_strength = ( ((df["volume"] > df["volume"].rolling(20).mean() * 1.5).astype(float) * 0.2) + ((df["volume"] > df["volume"].rolling(10).mean()).astype(float) * 0.1) ).clip(0, 0.2) long_strength_components.append(volume_long_strength) # Momentum strength (20% weight) momentum_long_strength = ( ((df["close"] > df["close"].shift(1)).astype(float) * 0.1) + ((df["close"] > df["open"]).astype(float) * 0.1) # Green candle ).clip(0, 0.2) long_strength_components.append(momentum_long_strength) # =========================================== # SHORT SIGNAL STRENGTH COMPONENTS # =========================================== short_strength_components = [] # MML structure strength (40% weight) mml_short_strength = ( bearish_mml.astype(float) * 0.4 + # Strong bearish structure mml_resistance_reject.astype(float) * 0.3 + # Resistance rejection ((df["close"] < df["[4/8]P"]).astype(float) * 0.2) + # Below midpoint ((df["close"] < df["[2/8]P"]).astype(float) * 0.1) # Below 25% ) short_strength_components.append(mml_short_strength.clip(0, 0.4)) # RSI strength (20% weight) rsi_short_strength = ( ((df["rsi"] > 70).astype(float) * 0.2) + # Overbought ((df["rsi"] > 60).astype(float) * 0.15) + # Getting overbought ((df["rsi"] < df["rsi"].shift(1)).astype(float) * 0.05) # RSI declining ).clip(0, 0.2) short_strength_components.append(rsi_short_strength) # Volume strength (20% weight) - same as long short_strength_components.append(volume_long_strength) # Momentum strength (20% weight) momentum_short_strength = ( ((df["close"] < df["close"].shift(1)).astype(float) * 0.1) + ((df["close"] < df["open"]).astype(float) * 0.1) # Red candle ).clip(0, 0.2) short_strength_components.append(momentum_short_strength) # =========================================== # COMBINE STRENGTH COMPONENTS # =========================================== df['long_signal_strength'] = sum(long_strength_components).clip(0, 1) df['short_signal_strength'] = sum(short_strength_components).clip(0, 1) return df def apply_mml_reentry_logic(self, df: pd.DataFrame, pair: str, any_long_signal: pd.Series, any_short_signal: pd.Series) -> tuple: current_time = pd.Timestamp.now() if df.empty else pd.Timestamp(df.index[-1]) if pair not in self.reentry_count: self.reentry_count[pair] = {'long': 0, 'short': 0} recent_exits = self.get_recent_exits(pair, current_time) reentry_long_mask = pd.Series([False] * len(df), index=df.index) reentry_short_mask = pd.Series([False] * len(df), index=df.index) for i in range(len(df)): current_candle_time = df.index[i] for direction in ['long', 'short']: if (self.should_attempt_reentry(pair, direction, current_candle_time, recent_exits) and self.reentry_count[pair][direction] < self.max_reentries_per_direction.value): favorable = (df.iloc[i]['close'] > df.iloc[i]['[2/8]P'] if direction == 'long' else df.iloc[i]['close'] < df.iloc[i]['[6/8]P']) strength_threshold = df.iloc[i][f'{direction}_signal_strength'] >= self.reentry_signal_strength_threshold.value if favorable and strength_threshold: conditions = ( (df.iloc[i]['rsi'] < 45 if direction == 'long' else df.iloc[i]['rsi'] > 55) & (df.iloc[i]['volume'] > df.iloc[i]['volume_avg_10']) & (df.iloc[i]['close'] > df.iloc[i]['close'].shift(1) if direction == 'long' else df.iloc[i]['close'] < df.iloc[i]['close'].shift(1) if i > 0 else True) ) if conditions: if direction == 'long': reentry_long_mask.iloc[i] = True self.reentry_count[pair]['long'] += 1 logger.info(f"{pair} 🔄 STANDARD {direction.upper()} RE-ENTRY #{self.reentry_count[pair]['long']}") else: reentry_short_mask.iloc[i] = True self.reentry_count[pair]['short'] += 1 logger.info(f"{pair} 🔄 STANDARD {direction.upper()} RE-ENTRY #{self.reentry_count[pair]['short']}") return any_long_signal | reentry_long_mask, any_short_signal | reentry_short_mask def set_enhanced_entry_tags(self, df: pd.DataFrame, final_long_signal, final_short_signal, long_signal_mml_breakout, long_signal_support_bounce, long_signal_50_reclaim, long_signal_sextrema, long_signal_rolling_MinH2, short_signal_bearish_breakdown, short_signal_confirmed_max_entry, short_signal_rolling_MaxH2_Entry, pair: str) -> pd.DataFrame: """ 🏷️ Set enhanced entry tags that include re-entry information """ # Your existing tag logic with re-entry enhancement df.loc[final_long_signal & long_signal_mml_breakout, "enter_tag"] = "MML_Bullish_Breakout" df.loc[final_long_signal & long_signal_support_bounce & (df["enter_tag"] == ""), "enter_tag"] = "MML_Support_Bounce" df.loc[final_long_signal & long_signal_50_reclaim & (df["enter_tag"] == ""), "enter_tag"] = "MML_50_Reclaim" df.loc[final_long_signal & long_signal_rolling_MinH2 & (df["enter_tag"] == ""), "enter_tag"] = "Rolling_MinH2" df.loc[final_long_signal & long_signal_sextrema & (df["enter_tag"] == ""), "enter_tag"] = "Sextrema" if self.can_short: df.loc[final_short_signal & short_signal_bearish_breakdown, "enter_tag"] = "MML_Bearish_Breakdown" df.loc[final_short_signal & short_signal_rolling_MaxH2_Entry & (df["enter_tag"] == ""), "enter_tag"] = "Rolling_MaxH2_Short" df.loc[final_short_signal & short_signal_confirmed_max_entry & (df["enter_tag"] == ""), "enter_tag"] = "Confirmed_Max_Entry_Short" # Add re-entry indicators to tags if pair in self.reentry_count: reentry_suffix = "" if self.reentry_count[pair]['long'] > 0 or self.reentry_count[pair]['short'] > 0: total_reentries = self.reentry_count[pair]['long'] + self.reentry_count[pair]['short'] reentry_suffix = f"_RE{total_reentries}" # Append re-entry info to existing tags df.loc[df["enter_tag"] != "", "enter_tag"] = df.loc[df["enter_tag"] != "", "enter_tag"] + reentry_suffix return df # =========================================== # 🛠️ HELPER FUNCTIONS (same as before) # =========================================== def should_attempt_reentry(self, pair: str, direction: str, current_time: datetime, recent_exits: list) -> bool: """Check if we should attempt re-entry""" try: if self.reentry_count[pair][direction] >= self.max_reentries_per_direction.value: return False recent_exit_in_direction = None for exit_time, exit_direction, exit_reason, profit in recent_exits: if exit_direction == direction: recent_exit_in_direction = (exit_time, exit_reason) break if not recent_exit_in_direction: return False exit_time, exit_reason = recent_exit_in_direction if exit_reason == 'roi' and self.enable_quick_reentry_on_roi.value: cooldown = float(self.quick_reentry_cooldown.value) # Ensure float else: cooldown = float(self.reentry_cooldown_minutes.value) # Ensure float # Use your safe_time_diff function instead time_since_exit = self.safe_time_diff(current_time, exit_time) return time_since_exit >= cooldown except Exception as e: logger.warning(f"Re-entry check error for {pair} {direction}: {e}") return False # Don't attempt re-entry if there's an error def was_recent_roi_exit(self, pair: str, direction: str, current_time: datetime, recent_exits: list) -> bool: """Check if there was a recent ROI exit""" try: for exit_time, exit_direction, exit_reason, profit in recent_exits: if (exit_direction == direction and exit_reason == 'roi'): # Use safe_time_diff instead of manual calculation time_since_exit = self.safe_time_diff(current_time, exit_time) if time_since_exit <= float(self.quick_reentry_cooldown.value): return True return False except Exception as e: logger.warning(f"Recent ROI check error for {pair} {direction}: {e}") return False def get_recent_exits(self, pair: str, current_time: datetime) -> list: """Get recent exits for this pair""" try: if not hasattr(self, 'recent_exits') or pair not in self.recent_exits: return [] # Safe cutoff time calculation try: cutoff_time = pd.Timestamp(current_time) - pd.Timedelta(hours=1) except: cutoff_time = pd.Timestamp.now() - pd.Timedelta(hours=1) recent_exits = [] for exit_time, direction, reason, profit in self.recent_exits[pair]: try: # Safe time comparison using safe_time_diff time_since_exit = self.safe_time_diff(current_time, exit_time) # If less than 60 minutes ago, include it if time_since_exit <= 60: # 60 minutes recent_exits.append((exit_time, direction, reason, profit)) except Exception as e: logger.warning(f"Exit time comparison error: {e}") continue return recent_exits except Exception as e: logger.warning(f"Get recent exits error for {pair}: {e}") return [] def populate_exit_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ UNIFIED EXIT SYSTEM - Enhanced with stricter momentum, ADX for trend weakness, and volatility checks Blends custom MML exits with simple opposite signals for better reliability """ # =========================================== # INITIALIZE EXIT COLUMNS # =========================================== df["exit_long"] = 0 df["exit_short"] = 0 df["exit_tag"] = "" # =========================================== # PRELIMINARY CALCULATIONS (Enhanced) # =========================================== # Ensure ADX is available (from entries or add here) if 'adx' not in df.columns: df['adx'] = ta.ADX(df['high'], df['low'], df['close'], timeperiod=14) # Volume fading check (new: for exhaustion exits) df['volume_fading'] = (df['volume'] < df['volume'].rolling(20).mean() * 0.8) & \ (df['volume'] < df['volume'].shift(1)) # Volatility check (new: for emergency) volatility_high = df['atr'] / df['close'] > 0.03 # Existing MML structures (bullish_mml, bearish_mml, at_resistance, at_support) - assume from entries or add if needed bullish_mml = ( (df["close"] > df["[6/8]P"]) | ((df["close"] > df["[4/8]P"]) & (df["close"].shift(5) < df["[4/8]P"].shift(5))) ) bearish_mml = ( (df["close"] < df["[2/8]P"]) | ((df["close"] < df["[4/8]P"]) & (df["close"].shift(5) > df["[4/8]P"].shift(5))) ) at_resistance = ( (df["high"] >= df["[6/8]P"]) | (df["high"] >= df["[7/8]P"]) | (df["high"] >= df["[8/8]P"]) ) at_support = ( (df["low"] <= df["[2/8]P"]) | (df["low"] <= df["[1/8]P"]) | (df["low"] <= df["[0/8]P"]) ) # =========================================== # CHOOSE EXIT SYSTEM # =========================================== if self.use_custom_exits_advanced: # Use enhanced custom MML-based exits return self._populate_custom_exits_advanced(df, metadata) else: # Use simple opposite-signal exits (with enhancements) return self._populate_simple_exits(df, metadata) def _populate_custom_exits_advanced(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ ADVANCED MML-BASED EXIT SYSTEM - Enhanced for better trend handling """ pair = metadata['pair'] # Ensure ADX is available (from entries or add here) if 'adx' not in df.columns: df['adx'] = ta.ADX(df['high'], df['low'], df['close'], timeperiod=14) # Volume fading check (new: for exhaustion exits) df['volume_fading'] = (df['volume'] < df['volume'].rolling(20).mean() * 0.8) & \ (df['volume'] < df['volume'].shift(1)) # Volatility check (new: for emergency) volatility_high = df['atr'] / df['close'] > 0.03 # Add missing MML structure calculations (moved here to ensure definition) bullish_mml = ( (df["close"] > df["[6/8]P"]) | ((df["close"] > df["[4/8]P"]) & (df["close"].shift(5) < df["[4/8]P"].shift(5))) ) bearish_mml = ( (df["close"] < df["[2/8]P"]) | ((df["close"] < df["[4/8]P"]) & (df["close"].shift(5) > df["[4/8]P"].shift(5))) ) at_resistance = ( (df["high"] >= df["[6/8]P"]) | (df["high"] >= df["[7/8]P"]) | (df["high"] >= df["[8/8]P"]) ) at_support = ( (df["low"] <= df["[2/8]P"]) | (df["low"] <= df["[1/8]P"]) | (df["low"] <= df["[0/8]P"]) ) # 1. Profit-Taking Exits (tighter trailing) atr_adjusted = df["atr"].fillna(0).astype(float) * self.atr_multiplier.value # Use param (default 2.0) trailing_stop_level = df["high"] - atr_adjusted long_exit_resistance_profit = ( at_resistance & (df["close"] < df["high"]) & (df["rsi"] > 60) & (df["maxima"] == 1) & (df["volume"] > df["volume"].rolling(10).mean()) & (df["close"] < trailing_stop_level) & # Trailing ATR (df["market_score"] > 0.3) & (df["consecutive_red"] >= 1) & # Exit if downtrend starts (df['adx'] < 25) # New: only if trend weakening ) long_exit_extreme_overbought = ( (df["close"] > df["[7/8]P"]) & (df["rsi"] > 75) & (df["close"] < df["close"].shift(1)) & (df["maxima"] == 1) & (df['volume_fading']) # New: volume exhaustion ) long_exit_volume_exhaustion = ( at_resistance & (df['volume_fading']) & # New: stricter fading check (df["rsi"] > 70) & (df["close"] < df["close"].shift(1)) & (~volatility_high) # New: skip in high vol (false exhaustion) ) # 2. Structure Breakdown (stricter) long_exit_structure_breakdown = ( (df["close"] < df["[4/8]P"]) & (df["close"].shift(1) >= df["[4/8]P"].shift(1)) & bullish_mml.shift(1) & (df["close"] < df["[4/8]P"] * 0.995) & (df["close"] < df["close"].shift(1)) & (df["close"] < df["close"].shift(2)) & (df["rsi"] < 50) & (df["volume"] > df["volume"].rolling(15).mean() * 1.2) & (df["close"] < df["open"]) & (df["low"] < df["low"].shift(1)) & (df["close"] < df["close"].rolling(3).mean()) & (df["atr"] > df["atr"].rolling(10).mean()) & (df["market_regime"] != 'bull_run') & (df['adx'] < 20) # New: confirm trend loss ) # 3. Momentum Divergence (stricter: 3-candle RSI decline) long_exit_momentum_divergence = ( at_resistance & (df["rsi"] < df["rsi"].shift(1)) & (df["rsi"].shift(1) < df["rsi"].shift(2)) & (df["rsi"] < df["rsi"].shift(3)) & # New: 3-candle decline (df["close"] >= df["close"].shift(1)) & (df["maxima"] == 1) & (df["rsi"] > 60) & (df["consecutive_red"].shift(1) >= 1) # Potential reversal ) # 4. Range Exit (add volume) long_exit_range = ( (df["close"] >= df["[2/8]P"]) & (df["close"] <= df["[6/8]P"]) & # In range (df["high"] >= df["[6/8]P"]) & # HIGH touched 75% (df["close"] < df["[6/8]P"] * 0.995) & # Closed below (df["rsi"] > 65) & # Conservative RSI (df["maxima"] == 1) & (df["volume"] > df["volume"].rolling(10).mean() * 1.2) # Volume confirmation ) # 5. Emergency Exit (vol-aware) long_exit_emergency = ( (df["close"] < df["[0/8]P"]) & # At 0% (df["rsi"] < 25) & (df["volume"] > df["volume"].rolling(20).mean() * 2) & (df["close"] < df["close"].shift(1)) & (df["close"] < df["close"].shift(2)) & (df["close"] < df["open"]) & volatility_high # New: only in high vol dumps ) if self.use_emergency_exits else pd.Series([False] * len(df), index=df.index) # Combine all Long Exit signals any_long_exit = ( long_exit_resistance_profit | long_exit_extreme_overbought | long_exit_volume_exhaustion | long_exit_structure_breakdown | long_exit_momentum_divergence | long_exit_range | long_exit_emergency ) # =========================================== # SHORT EXIT SIGNALS (Symmetric enhancements) # =========================================== if self.can_short: atr_adjusted_short = df["atr"].fillna(0).astype(float) * self.atr_multiplier.value trailing_stop_level_short = df["low"] + atr_adjusted_short short_exit_support_profit = ( at_support & (df["close"] > df["low"]) & (df["rsi"] < 40) & (df["minima"] == 1) & (df["volume"] > df["volume"].rolling(10).mean()) & (df["close"] > trailing_stop_level_short) & # Trailing ATR (df["market_score"] < 0.7) & (~(df["consecutive_red"] >= 1)) & (df['volume_fading']) # New: volume exhaustion ) short_exit_extreme_oversold = ( (df["close"] < df["[1/8]P"]) & (df["rsi"] < 25) & (df["close"] > df["close"].shift(1)) & (df["minima"] == 1) & (df['volume_fading']) # New ) short_exit_volume_exhaustion = ( at_support & (df['volume_fading']) & (df["rsi"] < 30) & (df["close"] > df["close"].shift(1)) & (~volatility_high) ) short_exit_structure_breakout = ( (df["close"] > df["[4/8]P"]) & (df["close"].shift(1) <= df["[4/8]P"].shift(1)) & bearish_mml.shift(1) & (df["close"] > df["[4/8]P"] * 1.005) & (df["close"] > df["close"].shift(1)) & (df["close"] > df["close"].shift(2)) & (df["rsi"] > 50) & (df["volume"] > df["volume"].rolling(15).mean() * 1.2) & (df["close"] > df["open"]) & (df["high"] > df["high"].shift(1)) & (df["atr"] > df["atr"].rolling(10).mean()) & (df["market_regime"] != 'bear_market') & (df['adx'] < 20) # New: confirm trend loss ) short_exit_momentum_divergence = ( at_support & (df["rsi"] > df["rsi"].shift(1)) & (df["rsi"].shift(1) > df["rsi"].shift(2)) & (df["rsi"] > df["rsi"].shift(3)) & # New: 3-candle increase (df["close"] <= df["close"].shift(1)) & (df["minima"] == 1) & (df["rsi"] < 40) ) short_exit_range = ( (df["close"] >= df["[2/8]P"]) & (df["close"] <= df["[6/8]P"]) & (df["low"] <= df["[2/8]P"]) & (df["close"] > df["[2/8]P"] * 1.005) & (df["rsi"] < 35) & (df["minima"] == 1) & (df["volume"] > df["volume"].rolling(10).mean() * 1.2) ) short_exit_emergency = ( (df["close"] > df["[8/8]P"]) & (df["rsi"] > 85) & (df["volume"] > df["volume"].rolling(20).mean() * 2) & (df["close"] > df["close"].shift(1)) & (df["close"] > df["close"].shift(2)) & (df["close"] > df["open"]) & volatility_high # New: only in high vol pumps ) if self.use_emergency_exits else pd.Series([False] * len(df), index=df.index) any_short_exit = ( short_exit_support_profit | short_exit_extreme_oversold | short_exit_volume_exhaustion | short_exit_structure_breakout | short_exit_momentum_divergence | short_exit_range | short_exit_emergency ) else: any_short_exit = pd.Series([False] * len(df), index=df.index) # =========================================== # COORDINATION WITH ENTRY SIGNALS (Blend with simple) # =========================================== # Exit on opposite entry signals (existing) if 'enter_long' in df.columns and 'enter_short' in df.columns: df.loc[df['enter_short'] == 1, 'exit_long'] = 1 df.loc[df['enter_long'] == 1, 'exit_short'] = 1 df.loc[df['exit_long'] == 1, 'exit_tag'] = 'Reversal_Short_Entry' if self.can_short: df.loc[df['exit_short'] == 1, 'exit_tag'] = 'Reversal_Long_Entry' else: logger.warning("Missing 'enter_long' or 'enter_short' columns; skipping reversal-based exits") # Set final exits and tags df.loc[any_long_exit, "exit_long"] = 1 if self.can_short: df.loc[any_short_exit, "exit_short"] = 1 # Exit tags (updated for new conditions) df.loc[any_long_exit & long_exit_emergency, "exit_tag"] = "MML_Emergency_Long_Exit" df.loc[any_long_exit & long_exit_structure_breakdown & (df["exit_tag"] == ""), "exit_tag"] = "MML_Structure_Breakdown" df.loc[any_long_exit & long_exit_resistance_profit & (df["exit_tag"] == ""), "exit_tag"] = "MML_Resistance_Profit" df.loc[any_long_exit & long_exit_extreme_overbought & (df["exit_tag"] == ""), "exit_tag"] = "MML_Extreme_Overbought" df.loc[any_long_exit & long_exit_volume_exhaustion & (df["exit_tag"] == ""), "exit_tag"] = "MML_Volume_Exhaustion_Long" df.loc[any_long_exit & long_exit_momentum_divergence & (df["exit_tag"] == ""), "exit_tag"] = "MML_Momentum_Divergence_Long" df.loc[any_long_exit & long_exit_range & (df["exit_tag"] == ""), "exit_tag"] = "MML_Range_Exit_Long" if self.can_short: df.loc[any_short_exit & short_exit_emergency, "exit_tag"] = "MML_Emergency_Short_Exit" df.loc[any_short_exit & short_exit_structure_breakout & (df["exit_tag"] == ""), "exit_tag"] = "MML_Structure_Breakout" df.loc[any_short_exit & short_exit_support_profit & (df["exit_tag"] == ""), "exit_tag"] = "MML_Support_Profit" df.loc[any_short_exit & short_exit_extreme_oversold & (df["exit_tag"] == ""), "exit_tag"] = "MML_Extreme_Oversold" df.loc[any_short_exit & short_exit_volume_exhaustion & (df["exit_tag"] == ""), "exit_tag"] = "MML_Volume_Exhaustion_Short" df.loc[any_short_exit & short_exit_momentum_divergence & (df["exit_tag"] == ""), "exit_tag"] = "MML_Momentum_Divergence_Short" df.loc[any_short_exit & short_exit_range & (df["exit_tag"] == ""), "exit_tag"] = "MML_Range_Exit_Short" logger.info(f"{pair} 🚪 FINAL EXITS: Long={any_long_exit.sum()}, Short={any_short_exit.sum()}") return df def _populate_simple_exits(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ SIMPLE OPPOSITE SIGNAL EXIT SYSTEM - Enhanced with vol/momentum checks """ # Exit LONG on SHORT signal (existing, add vol confirmation) long_exit_on_short = (df["enter_short"] == 1) & df['volume_rising'] # New: confirm with volume # Exit SHORT on LONG signal short_exit_on_long = (df["enter_long"] == 1) & df['volume_rising'] # Emergency exits (if enabled, vol-aware) if self.use_emergency_exits: emergency_long_exit = ( (df['rsi'] > 85) & (df['volume'] > df['volume'].rolling(20).mean() * 3) & (df['close'] < df['open']) & (df['close'] < df['low'].shift(1)) & volatility_high # New ) | ( (df.get('structure_break_down', 0) == 1) & (df['volume'] > df['volume'].rolling(20).mean() * 2.5) & (df['atr'] > df['atr'].rolling(20).mean() * 2) ) emergency_short_exit = ( (df['rsi'] < 15) & (df['volume'] > df['volume'].rolling(20).mean() * 3) & (df['close'] > df['open']) & (df['close'] > df['high'].shift(1)) & volatility_high # New ) | ( (df.get('structure_break_up', 0) == 1) & (df['volume'] > df['volume'].rolling(20).mean() * 2.5) & (df['atr'] > df['atr'].rolling(20).mean() * 2) ) else: emergency_long_exit = pd.Series([False] * len(df), index=df.index) emergency_short_exit = pd.Series([False] * len(df), index=df.index) # Combine df.loc[long_exit_on_short | emergency_long_exit, 'exit_long'] = 1 df.loc[short_exit_on_long | emergency_short_exit, 'exit_short'] = 1 # Debugging (existing) if metadata['pair'] in ['BTC/USDT:USDT', 'ETH/USDT:USDT']: recent_exits = df['exit_long'].tail(5).sum() + df['exit_short'].tail(5).sum() if recent_exits > 0: exit_tag = df['exit_tag'].iloc[-1] logger.info(f"{metadata['pair']} EXIT SIGNAL - Tag: {exit_tag}") logger.info(f" Exit System: {'Custom MML' if self.use_custom_exits_advanced else 'Simple Opposite'}") logger.info(f" RSI: {df['rsi'].iloc[-1]:.1f}") return df 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: current_profit_ratio = trade.calc_profit_ratio(rate) time_in_trade = (current_time - trade.open_date_utc).total_seconds() / 3600 logger.warning(f"🚪 {pair} EXIT REQUEST: {exit_reason}") logger.warning(f" 💰 Profit: {current_profit_ratio:.4f} ({current_profit_ratio*100:.2f}%)") logger.warning(f" ⏰ Time in trade: {time_in_trade:.1f} hours") # 🛑 BLOCK FORCE EXITS if exit_reason in ["force_exit", "force_sell", "emergency_exit"]: logger.warning(f"{pair} 🛑 BLOCKED FORCE EXIT: {exit_reason} (Profit: {current_profit_ratio:.2%})") return False # 🛑 BLOCK PROTECTION EXITS if exit_reason in ["timeout", "protection", "cooldown"]: logger.warning(f"{pair} 🛑 BLOCKED PROTECTION EXIT: {exit_reason}") return False # ✅ BLOCK TRAILING STOPS (NEW!) if exit_reason in ["trailing_stop_loss", "trailing_stop"]: logger.warning(f"{pair} ✅ TRAILING STOP: {exit_reason} (Profit: {current_profit_ratio:.2%})") return True # ✅ ALLOW ROI EXITS if exit_reason == "roi": logger.info(f"{pair} ✅ ROI EXIT: {current_profit_ratio:.2%} after {time_in_trade:.1f}h") return True # ✅ ALLOW OTHER LEGITIMATE EXITS if exit_reason in ["stop_loss", "exit_signal", "sell_signal", "custom_exit"]: logger.info(f"{pair} ✅ EXIT: {exit_reason} ({current_profit_ratio:.2%})") return True # ✅ DEFAULT ALLOW (for any other exit reasons) logger.info(f"{pair} ✅ FINAL CHECK PASSED: Profit {current_profit_ratio:.2%}") return True # 🔍 ADD THIS METHOD HERE (anywhere in the class) def should_exit_early_warning(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float) -> bool: """ ⚠️ EARLY WARNING: Exit before stop loss if conditions deteriorate """ try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if dataframe.empty: return False last_candle = dataframe.iloc[-1] time_in_trade = (current_time - trade.open_date_utc).total_seconds() / 3600 # Don't exit too early if time_in_trade < 0.25: # Give trade at least 15 minutes return False # Critical deterioration signals rsi = last_candle.get('rsi', 50) if trade.is_short: # Short position warning signs warning_signals = ( (rsi < 30 and current_profit < -0.02) or # RSI turning bullish while losing (last_candle.get('close', 0) > last_candle.get('[6/8]P', 0)) or # Breaking above 75% (current_profit < -0.04 and time_in_trade > 2) # Deep loss after 2h ) else: # Long position warning signs warning_signals = ( (rsi > 70 and current_profit < -0.02) or # RSI turning bearish while losing (last_candle.get('close', 0) < last_candle.get('[2/8]P', 0)) or # Breaking below 25% (current_profit < -0.04 and time_in_trade > 2) # Deep loss after 2h ) if warning_signals: logger.warning(f"⚠️ {pair} EARLY WARNING: Deteriorating conditions detected") return True return False except Exception as e: logger.error(f"Early warning error for {pair}: {e}") return False def validate_backtest_conditions(self, dataframe: pd.DataFrame, metadata: dict) -> None: """ 📊 VALIDATION: Check if backtest conditions match live trading """ pair = metadata['pair'] # Check BTC correlation status if 'btc_status' in dataframe.columns: btc_status_counts = dataframe['btc_status'].value_counts() total_candles = len(dataframe) logger.warning(f"{pair} 📊 BACKTEST VALIDATION REPORT:") logger.warning(f" Total candles: {total_candles}") for status, count in btc_status_counts.items(): percentage = (count / total_candles) * 100 logger.warning(f" BTC Status '{status}': {count} candles ({percentage:.1f}%)") # Alert if significant portion has no BTC data no_data_percentage = btc_status_counts.get('NO_DATA', 0) / total_candles * 100 if no_data_percentage > 10: logger.error(f"{pair} 🚨 WARNING: {no_data_percentage:.1f}% of backtest has no BTC data!") logger.error(f"{pair} 🚨 BACKTEST RESULTS MAY NOT MATCH LIVE TRADING!") # Count signal reduction due to BTC filter if 'btc_correlation_ok' in df.columns: btc_ok = df['btc_correlation_ok'].fillna(True).astype(bool) # any_long_signal = any_long_signal & btc_ok # Commented out to disable BTC correlation filter # any_short_signal = any_short_signal & btc_ok # Commented out to disable BTC correlation filter logger.info(f"{pair} ₿ BTC correlation: {'✅' if btc_ok.iloc[-1] else '❌'}") else: logger.error(f"{pair} 🚨 NO BTC CORRELATION DATA IN BACKTEST!") logger.error(f"{pair} 🚨 BACKTEST IS DEFINITELY NOT MATCHING LIVE CONDITIONS!") def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[str]: """ 🚪 Smart exit logic to prevent force exits """ # Calculate how long trade has been running trade_duration_hours = (current_time - trade.open_date_utc).total_seconds() / 3600 # Get entry tag for smart handling entry_tag = getattr(trade, 'enter_tag', '') or '' # 🎯 PREVENT LONG-RUNNING TRADES THAT BECOME FORCE EXITS # MML_Breakout_75 - these are getting force exited at -33% if 'MML_Breakout_75' in entry_tag: # Exit after 7 days if still negative to prevent worse force exits if trade_duration_hours > (7 * 24) and current_profit < -0.05: # -5% logger.info(f"{pair} 🚪 Smart exit MML_Breakout_75: {current_profit:.2%} after {trade_duration_hours/24:.1f} days") return "smart_exit_mml75" # Bear_Breakdown_25 - these are better but still getting force exited elif 'Bear_Breakdown_25' in entry_tag: # Exit after 14 days if still negative if trade_duration_hours > (14 * 24) and current_profit < -0.02: # -2% logger.info(f"{pair} 🚪 Smart exit Bear_Breakdown_25: {current_profit:.2%} after {trade_duration_hours/24:.1f} days") return "smart_exit_bear25" # General rule for any trade running over 21 days if trade_duration_hours > (21 * 24): logger.info(f"{pair} 🚪 Smart exit long trade: {current_profit:.2%} after {trade_duration_hours/24:.1f} days") return "smart_exit_timeout" return None def bot_loop_start(self, **kwargs) -> None: """ 🔍 Log any trades that might be candidates for force exit """ try: trades = Trade.get_open_trades() current_time = datetime.now() for trade in trades: time_in_trade = (current_time - trade.open_date_utc).total_seconds() / 3600 current_profit = trade.calc_profit_ratio(trade.close_rate) if trade.close_rate else 0 # Warn about very old trades (might become force exits) if time_in_trade > 48: # 2+ days logger.warning(f"⚠️ OLD TRADE {trade.pair}: {time_in_trade:.1f}h old, " f"Profit: {current_profit:.2%}") # Warn about large losses (might become force exits) if current_profit < -0.10: # >10% loss logger.warning(f"⚠️ LARGE LOSS {trade.pair}: {current_profit:.2%}, " f"Time: {time_in_trade:.1f}h") except Exception as e: # Don't let debugging break the bot logger.debug(f"bot_loop_start debug error: {e}")