浏览代码

Add RSI monitoring and configuration to enhance trading strategy

- Introduced new RSI notification configuration options in `env.example` and `config.py`, allowing users to enable/disable notifications and set parameters for RSI analysis.
- Implemented `get_candle_data` and `get_candle_data_formatted` methods in `HyperliquidClient` to fetch and format OHLCV candle data for trading symbols.
- Added an `RsiMonitor` to `MarketMonitor` for monitoring RSI signals based on new candle data, improving trading strategy responsiveness.
- Enhanced logging to provide detailed feedback on RSI monitoring status and configuration validation.
Carles Sentis 1 天之前
父节点
当前提交
86b75f533b
共有 7 个文件被更改,包括 575 次插入28 次删除
  1. 15 0
      config/env.example
  2. 2 1
      requirements.txt
  3. 84 0
      src/clients/hyperliquid_client.py
  4. 30 0
      src/config/config.py
  5. 48 26
      src/monitoring/market_monitor.py
  6. 395 0
      src/monitoring/rsi_monitor.py
  7. 1 1
      trading_bot.py

+ 15 - 0
config/env.example

@@ -28,6 +28,21 @@ RISK_MANAGEMENT_ENABLED=true
 # This is the percentage loss of your actual cash investment, not margin
 STOP_LOSS_PERCENTAGE=10.0
 
+# ========================================
+# RSI Notification Configuration
+# ========================================
+# Enable/disable RSI vs RSI_SMA notifications
+RSI_NOTIFICATION_ENABLED=true
+
+# Timeframe for RSI analysis
+RSI_TIMEFRAME=1h
+
+# RSI calculation period (typically 14)
+RSI_PERIOD=14
+
+# RSI SMA period for smoothing (typically 5-10)
+RSI_SMA_PERIOD=5
+
 # ========================================
 # Telegram Bot Configuration
 # ========================================

+ 2 - 1
requirements.txt

@@ -5,4 +5,5 @@ pandas
 psutil
 requests
 aiohttp
-hyperliquid 
+hyperliquid
+talib 

+ 84 - 0
src/clients/hyperliquid_client.py

@@ -289,6 +289,90 @@ class HyperliquidClient:
             logger.error(f"❌ Error fetching market data for {symbol}: {error_message} (Full exception: {e})")
             return None
     
+    def get_candle_data(self, symbol: str, timeframe: str = '1h', limit: int = 100, since: Optional[int] = None) -> Optional[List[List]]:
+        """
+        Get OHLCV candle data for a symbol.
+        
+        Args:
+            symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
+            timeframe: Timeframe for candles ('1m', '5m', '15m', '1h', '4h', '1d', etc.)
+            limit: Maximum number of candles to return (default: 100)
+            since: Timestamp in milliseconds to start from (optional)
+            
+        Returns:
+            List of OHLCV candles in format: [[timestamp, open, high, low, close, volume], ...]
+            Returns None if error occurs
+        """
+        try:
+            if not self.sync_client:
+                logger.error("❌ Client not initialized")
+                return None
+            
+            logger.debug(f"🕯️ Fetching {limit} candles for {symbol} ({timeframe})")
+            
+            # Fetch OHLCV data
+            params = {}
+            if since is not None:
+                candles = self.sync_client.fetch_ohlcv(symbol, timeframe, since=since, limit=limit, params=params)
+            else:
+                candles = self.sync_client.fetch_ohlcv(symbol, timeframe, limit=limit, params=params)
+            
+            if candles:
+                logger.info(f"✅ Successfully fetched {len(candles)} candles for {symbol} ({timeframe})")
+                logger.debug(f"📊 Candle data range: {candles[0][0]} to {candles[-1][0]} (timestamps)")
+            else:
+                logger.warning(f"⚠️ No candle data returned for {symbol} ({timeframe})")
+                
+            return candles
+            
+        except Exception as e:
+            error_message = self._extract_error_message(e)
+            logger.error(f"❌ Error fetching candle data for {symbol} ({timeframe}): {error_message} (Full exception: {e})")
+            return None
+    
+    def get_candle_data_formatted(self, symbol: str, timeframe: str = '1h', limit: int = 100, since: Optional[int] = None) -> Optional[List[Dict[str, Any]]]:
+        """
+        Get OHLCV candle data for a symbol in a formatted dictionary structure.
+        
+        Args:
+            symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
+            timeframe: Timeframe for candles ('1m', '5m', '15m', '1h', '4h', '1d', etc.)
+            limit: Maximum number of candles to return (default: 100)
+            since: Timestamp in milliseconds to start from (optional)
+            
+        Returns:
+            List of candle dictionaries with keys: timestamp, open, high, low, close, volume
+            Returns None if error occurs
+        """
+        try:
+            # Get raw candle data
+            raw_candles = self.get_candle_data(symbol, timeframe, limit, since)
+            
+            if not raw_candles:
+                return None
+            
+            # Format candles into dictionaries
+            formatted_candles = []
+            for candle in raw_candles:
+                if len(candle) >= 6:  # Ensure we have all OHLCV data
+                    formatted_candle = {
+                        'timestamp': candle[0],
+                        'open': candle[1],
+                        'high': candle[2],
+                        'low': candle[3],
+                        'close': candle[4],
+                        'volume': candle[5]
+                    }
+                    formatted_candles.append(formatted_candle)
+            
+            logger.info(f"✅ Successfully formatted {len(formatted_candles)} candles for {symbol} ({timeframe})")
+            return formatted_candles
+            
+        except Exception as e:
+            error_message = self._extract_error_message(e)
+            logger.error(f"❌ Error formatting candle data for {symbol} ({timeframe}): {error_message} (Full exception: {e})")
+            return None
+    
     def place_limit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
         """Place a limit order."""
         try:

+ 30 - 0
src/config/config.py

@@ -26,6 +26,12 @@ class Config:
     RISK_MANAGEMENT_ENABLED: bool = get_bool_env('RISK_MANAGEMENT_ENABLED', 'true')
     STOP_LOSS_PERCENTAGE: float = float(os.getenv('STOP_LOSS_PERCENTAGE', '10.0'))
     
+    # RSI Notification Configuration
+    RSI_NOTIFICATION_ENABLED: bool = get_bool_env('RSI_NOTIFICATION_ENABLED', 'true')
+    RSI_TIMEFRAME: str = os.getenv('RSI_TIMEFRAME', '1h')
+    RSI_PERIOD: int = int(os.getenv('RSI_PERIOD', '14'))
+    RSI_SMA_PERIOD: int = int(os.getenv('RSI_SMA_PERIOD', '5'))
+    
     # Telegram Bot Configuration
     TELEGRAM_BOT_TOKEN: Optional[str] = os.getenv('TELEGRAM_BOT_TOKEN')
     TELEGRAM_CHAT_ID: Optional[str] = os.getenv('TELEGRAM_CHAT_ID')
@@ -63,6 +69,7 @@ class Config:
             cls._validate_hyperliquid,
             cls._validate_telegram,
             cls._validate_bot_settings,
+            cls._validate_rsi,
             cls._validate_logging
         ]
         return all(validator() for validator in validators)
@@ -103,6 +110,25 @@ class Config:
             return False
         return True
 
+    @classmethod
+    def _validate_rsi(cls) -> bool:
+        """Validate RSI notification configuration."""
+        valid_timeframes = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M']
+        
+        if cls.RSI_TIMEFRAME not in valid_timeframes:
+            logger.error(f"❌ RSI_TIMEFRAME '{cls.RSI_TIMEFRAME}' is not valid. Valid: {valid_timeframes}")
+            return False
+        
+        if cls.RSI_PERIOD < 2:
+            logger.error("❌ RSI_PERIOD must be at least 2")
+            return False
+            
+        if cls.RSI_SMA_PERIOD < 2:
+            logger.error("❌ RSI_SMA_PERIOD must be at least 2")
+            return False
+        
+        return True
+
     @classmethod
     def _validate_logging(cls) -> bool:
         """Validate logging settings."""
@@ -153,6 +179,10 @@ class Config:
         logger.info(f"  💰 DEFAULT_TRADING_TOKEN: {cls.DEFAULT_TRADING_TOKEN}")
         logger.info(f"  🧮 RISK_MANAGEMENT_ENABLED: {cls.RISK_MANAGEMENT_ENABLED}")
         logger.info(f"  🔄 STOP_LOSS_PERCENTAGE: {cls.STOP_LOSS_PERCENTAGE}%")
+        logger.info(f"  📊 RSI_NOTIFICATION_ENABLED: {cls.RSI_NOTIFICATION_ENABLED}")
+        logger.info(f"  ⏰ RSI_TIMEFRAME: {cls.RSI_TIMEFRAME}")
+        logger.info(f"  🔢 RSI_PERIOD: {cls.RSI_PERIOD}")
+        logger.info(f"  📈 RSI_SMA_PERIOD: {cls.RSI_SMA_PERIOD}")
         logger.info(f"  🤖 TELEGRAM_BOT_TOKEN: {'✅ Set' if cls.TELEGRAM_BOT_TOKEN else '❌ Not Set'}")
         logger.info(f"  💬 TELEGRAM_CHAT_ID: {'✅ Set' if cls.TELEGRAM_CHAT_ID else '❌ Not Set'}")
         logger.info(f"  ⌨️ CUSTOM_KEYBOARD: {'✅ Enabled' if cls.TELEGRAM_CUSTOM_KEYBOARD_ENABLED else '❌ Disabled'}")

+ 48 - 26
src/monitoring/market_monitor.py

@@ -17,6 +17,7 @@ from src.monitoring.order_fill_processor import OrderFillProcessor
 from src.monitoring.position_monitor import PositionMonitor
 from src.monitoring.risk_cleanup_manager import RiskCleanupManager
 from src.monitoring.drawdown_monitor import DrawdownMonitor
+from src.monitoring.rsi_monitor import RsiMonitor
 
 logger = logging.getLogger(__name__)
 
@@ -81,6 +82,12 @@ class MarketMonitor:
             shared_state=self.shared_state # Pass shared_state here
         )
         
+        # Initialize RSI monitor for crossover notifications
+        self.rsi_monitor = RsiMonitor(
+            hyperliquid_client=self.trading_engine.client,
+            notification_manager=self.notification_manager
+        )
+        
         # Load minimal persistent state if any (most state is now in delegates or transient)
         self._load_state()
         
@@ -198,6 +205,9 @@ class MarketMonitor:
                 # Risk, cleanup, and sync tasks are now handled by the RiskCleanupManager
                 await self.risk_cleanup_manager.run_cleanup_tasks()
                 
+                # RSI monitoring - only calculates when there are new candles
+                await self._run_rsi_monitoring()
+                
                 loop_count += 1
                 if loop_count >= Config.MARKET_MONITOR_CLEANUP_INTERVAL_HEARTBEATS: # Use a config value
                     logger.info(f"Running periodic cleanup and sync tasks (Loop count: {loop_count})")
@@ -397,32 +407,44 @@ class MarketMonitor:
                 'simplified_position_tracker': 'active', 
                 'price_alarms': 'active',
                 'risk_cleanup_manager': 'active',
+                'rsi_monitor': 'active' if self.rsi_monitor.enabled else 'disabled',
                 'drawdown_monitor': 'active' if self.drawdown_monitor else 'disabled'
-            }
+            },
+            'rsi_monitor_status': self.rsi_monitor.get_status() if hasattr(self, 'rsi_monitor') else None
         }
 
-    # Methods that were moved are now removed from MarketMonitor.
-    # _check_order_fills -> OrderFillProcessor
-    # _process_disappeared_orders -> OrderFillProcessor
-    # _activate_pending_stop_losses_from_trades -> OrderFillProcessor
-    # _check_for_recent_fills_for_order -> OrderFillProcessor (helper)
-    # _auto_sync_orphaned_positions -> PositionSynchronizer
-    # _immediate_startup_auto_sync -> PositionSynchronizer
-    # _estimate_entry_price_for_orphaned_position -> PositionSynchronizer (helper)
-    # _send_startup_auto_sync_notification -> PositionSynchronizer (helper)
-    # _check_external_trades -> ExternalEventMonitor
-    # _check_price_alarms -> ExternalEventMonitor
-    # _send_alarm_notification -> ExternalEventMonitor (helper)
-    # _check_pending_triggers -> RiskCleanupManager
-    # _check_automatic_risk_management -> RiskCleanupManager
-    # _cleanup_orphaned_stop_losses -> RiskCleanupManager
-    # _check_external_stop_loss_orders -> RiskCleanupManager
-    # _cleanup_external_stop_loss_tracking -> RiskCleanupManager
-    # _cleanup_orphaned_pending_sl_activations -> RiskCleanupManager (new stub)
-
-    # Methods related to direct position/order processing like _process_filled_orders
-    # and _update_position_tracking are implicitly part of OrderFillProcessor's logic now.
-    # The complex internal logic of _check_external_trades for lifecycle updates is now within ExternalEventMonitor.
-    # The state for `external_stop_losses` is now managed by `RiskCleanupManager` via `shared_state`.
-    # The state for `last_processed_trade_time` for external fills is managed by `ExternalEventMonitor`.
-    # The state for `last_processed_trade_time_helper` for `_check_for_recent_fills_for_order` is in `MarketMonitorCache`.
+    async def _run_rsi_monitoring(self):
+        """
+        Run RSI monitoring for open positions and tracked symbols.
+        Only calculates RSI when there are new candles for efficiency.
+        """
+        try:
+            if not self.rsi_monitor.enabled:
+                return
+            
+            # Get symbols to monitor from open positions
+            symbols_to_monitor = set()
+            
+            # Add symbols from current positions
+            if self.cache.cached_positions:
+                for position in self.cache.cached_positions:
+                    symbol = position.get('symbol')
+                    if symbol and abs(float(position.get('contracts', 0))) > 1e-9:
+                        symbols_to_monitor.add(symbol)
+            
+            # Add default trading token if configured
+            default_symbol = f"{Config.DEFAULT_TRADING_TOKEN}/USDC:USDC"
+            symbols_to_monitor.add(default_symbol)
+            
+            # Convert to list and monitor
+            symbols_list = list(symbols_to_monitor)
+            
+            if symbols_list:
+                logger.debug(f"🔍 Running RSI monitoring for {len(symbols_list)} symbols: {symbols_list}")
+                await self.rsi_monitor.monitor_symbols_for_new_candles(symbols_list)
+            else:
+                logger.debug("📊 No symbols to monitor for RSI")
+                
+        except Exception as e:
+            logger.error(f"❌ Error in RSI monitoring: {e}")
+

+ 395 - 0
src/monitoring/rsi_monitor.py

@@ -0,0 +1,395 @@
+#!/usr/bin/env python3
+"""
+RSI Monitor - Monitors RSI vs RSI_SMA crossovers and sends notifications.
+"""
+
+import logging
+import asyncio
+import numpy as np
+from typing import Optional, Dict, Any, List
+from datetime import datetime, timezone
+from src.config.config import Config
+from src.clients.hyperliquid_client import HyperliquidClient
+from src.notifications.notification_manager import NotificationManager
+
+try:
+    import talib
+except ImportError:
+    logging.error("talib is required for RSI calculations. Install with: pip install talib")
+    raise
+
+logger = logging.getLogger(__name__)
+
+class RsiMonitor:
+    """Monitors RSI vs RSI_SMA crossovers and sends notifications."""
+    
+    def __init__(self, hyperliquid_client: HyperliquidClient, notification_manager: NotificationManager):
+        """
+        Initialize the RSI monitor.
+        
+        Args:
+            hyperliquid_client: Client for fetching market data
+            notification_manager: Manager for sending notifications
+        """
+        self.client = hyperliquid_client
+        self.notification_manager = notification_manager
+        
+        # RSI configuration from config
+        self.enabled = Config.RSI_NOTIFICATION_ENABLED
+        self.timeframe = Config.RSI_TIMEFRAME
+        self.rsi_period = Config.RSI_PERIOD
+        self.rsi_sma_period = Config.RSI_SMA_PERIOD
+        
+        # State tracking to avoid duplicate notifications
+        self.last_crossover_state: Dict[str, Optional[str]] = {}  # symbol -> 'above' or 'below'
+        self.last_notification_time: Dict[str, datetime] = {}  # symbol -> last notification time
+        
+        # New candle tracking - only calculate when there's a new candle
+        self.last_candle_timestamps: Dict[str, int] = {}  # symbol -> last candle timestamp (UTC)
+        
+        # Minimum time between notifications for same symbol (in seconds)
+        self.notification_cooldown = 300  # 5 minutes
+        
+        logger.info(f"🔧 RSI Monitor initialized: {self.timeframe} timeframe, RSI({self.rsi_period}), SMA({self.rsi_sma_period})")
+    
+    async def has_new_candle(self, symbol: str) -> bool:
+        """
+        Check if there's a new candle for the given symbol and timeframe.
+        Only returns True if there's actually a new candle since last check.
+        
+        Args:
+            symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
+            
+        Returns:
+            True if new candle detected, False otherwise
+        """
+        try:
+            # Get just the latest candle to check timestamp
+            latest_candles = self.client.get_candle_data(symbol, self.timeframe, limit=1)
+            
+            if not latest_candles or len(latest_candles) == 0:
+                logger.warning(f"⚠️ No candle data available for {symbol}")
+                return False
+            
+            # Get the timestamp of the latest candle
+            latest_timestamp = int(latest_candles[0][0])  # candle[0] is timestamp
+            
+            # Check if this is a new candle
+            last_known_timestamp = self.last_candle_timestamps.get(symbol, 0)
+            
+            if latest_timestamp > last_known_timestamp:
+                # New candle detected
+                self.last_candle_timestamps[symbol] = latest_timestamp
+                
+                # Convert timestamp to readable format for logging
+                candle_time = datetime.fromtimestamp(latest_timestamp / 1000, tz=timezone.utc)
+                logger.info(f"🕯️ New {self.timeframe} candle detected for {symbol} at {candle_time.strftime('%Y-%m-%d %H:%M:%S')} UTC")
+                return True
+            else:
+                # No new candle
+                logger.debug(f"🔄 No new candle for {symbol} (latest: {latest_timestamp}, known: {last_known_timestamp})")
+                return False
+                
+        except Exception as e:
+            logger.error(f"❌ Error checking for new candle {symbol}: {e}")
+            return False
+    
+    async def check_rsi_crossover(self, symbol: str) -> Optional[Dict[str, Any]]:
+        """
+        Check for RSI vs RSI_SMA crossovers for a given symbol.
+        
+        Args:
+            symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
+            
+        Returns:
+            Dictionary with crossover info if detected, None otherwise
+        """
+        if not self.enabled:
+            return None
+        
+        try:
+            # Get candle data - need enough candles for RSI + SMA calculation
+            # RSI needs at least rsi_period + 1, SMA needs rsi_sma_period on top of that
+            required_candles = self.rsi_period + self.rsi_sma_period + 20  # Extra buffer
+            
+            logger.debug(f"📊 Fetching {required_candles} candles for {symbol} ({self.timeframe})")
+            candles = self.client.get_candle_data(symbol, self.timeframe, limit=required_candles)
+            
+            if not candles or len(candles) < required_candles:
+                logger.warning(f"⚠️ Insufficient candle data for {symbol}: got {len(candles) if candles else 0}, need {required_candles}")
+                return None
+            
+            # Extract close prices
+            close_prices = np.array([float(candle[4]) for candle in candles])  # candle[4] is close price
+            
+            # Calculate RSI using talib
+            rsi_values = talib.RSI(close_prices, timeperiod=self.rsi_period)
+            
+            # Calculate RSI SMA (Simple Moving Average of RSI)
+            rsi_sma_values = talib.SMA(rsi_values, timeperiod=self.rsi_sma_period)
+            
+            # Get the latest values (skip NaN values)
+            valid_indices = ~(np.isnan(rsi_values) | np.isnan(rsi_sma_values))
+            if not np.any(valid_indices):
+                logger.warning(f"⚠️ No valid RSI/RSI_SMA values for {symbol}")
+                return None
+            
+            # Get last few valid values to detect crossover
+            valid_rsi = rsi_values[valid_indices]
+            valid_rsi_sma = rsi_sma_values[valid_indices]
+            
+            if len(valid_rsi) < 2:
+                logger.warning(f"⚠️ Not enough valid RSI data for crossover detection: {symbol}")
+                return None
+            
+            # Current and previous values
+            current_rsi = valid_rsi[-1]
+            current_rsi_sma = valid_rsi_sma[-1]
+            prev_rsi = valid_rsi[-2]
+            prev_rsi_sma = valid_rsi_sma[-2]
+            
+            logger.debug(f"📈 {symbol} RSI: {current_rsi:.2f} (prev: {prev_rsi:.2f}), RSI_SMA: {current_rsi_sma:.2f} (prev: {prev_rsi_sma:.2f})")
+            
+            # Detect crossover
+            crossover_detected = None
+            
+            # RSI crosses above RSI_SMA (bullish signal)
+            if prev_rsi <= prev_rsi_sma and current_rsi > current_rsi_sma:
+                crossover_detected = {
+                    'type': 'bullish',
+                    'direction': 'above',
+                    'current_rsi': current_rsi,
+                    'current_rsi_sma': current_rsi_sma,
+                    'prev_rsi': prev_rsi,
+                    'prev_rsi_sma': prev_rsi_sma,
+                    'timestamp': datetime.now()
+                }
+                logger.info(f"🟢 RSI Bullish Crossover detected for {symbol}: RSI {current_rsi:.2f} > RSI_SMA {current_rsi_sma:.2f}")
+            
+            # RSI crosses below RSI_SMA (bearish signal)
+            elif prev_rsi >= prev_rsi_sma and current_rsi < current_rsi_sma:
+                crossover_detected = {
+                    'type': 'bearish',
+                    'direction': 'below',
+                    'current_rsi': current_rsi,
+                    'current_rsi_sma': current_rsi_sma,
+                    'prev_rsi': prev_rsi,
+                    'prev_rsi_sma': prev_rsi_sma,
+                    'timestamp': datetime.now()
+                }
+                logger.info(f"🔴 RSI Bearish Crossover detected for {symbol}: RSI {current_rsi:.2f} < RSI_SMA {current_rsi_sma:.2f}")
+            
+            return crossover_detected
+            
+        except Exception as e:
+            logger.error(f"❌ Error checking RSI crossover for {symbol}: {e}")
+            return None
+    
+    async def should_send_notification(self, symbol: str, crossover_type: str) -> bool:
+        """
+        Check if we should send a notification based on cooldown and state.
+        
+        Args:
+            symbol: Trading symbol
+            crossover_type: 'bullish' or 'bearish'
+            
+        Returns:
+            True if notification should be sent
+        """
+        now = datetime.now()
+        
+        # Check if this is a different crossover type than last time
+        last_state = self.last_crossover_state.get(symbol)
+        if last_state == crossover_type:
+            logger.debug(f"🔄 Duplicate crossover state for {symbol}: {crossover_type}")
+            return False
+        
+        # Check cooldown period
+        last_notification = self.last_notification_time.get(symbol)
+        if last_notification:
+            time_since_last = (now - last_notification).total_seconds()
+            if time_since_last < self.notification_cooldown:
+                logger.debug(f"⏳ Cooldown active for {symbol}: {time_since_last:.0f}s < {self.notification_cooldown}s")
+                return False
+        
+        return True
+    
+    async def send_crossover_notification(self, symbol: str, crossover_info: Dict[str, Any]):
+        """
+        Send notification for RSI crossover.
+        
+        Args:
+            symbol: Trading symbol
+            crossover_info: Crossover details from check_rsi_crossover
+        """
+        try:
+            # Extract token from symbol
+            token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+            
+            crossover_type = crossover_info['type']
+            direction = crossover_info['direction']
+            current_rsi = crossover_info['current_rsi']
+            current_rsi_sma = crossover_info['current_rsi_sma']
+            timestamp = crossover_info['timestamp']
+            
+            # Determine emoji and message tone
+            if crossover_type == 'bullish':
+                main_emoji = "🟢"
+                signal_emoji = "📈"
+                signal_text = "BULLISH"
+                color_text = "GREEN"
+                action_hint = "Consider LONG position"
+            else:  # bearish
+                main_emoji = "🔴"
+                signal_emoji = "📉"
+                signal_text = "BEARISH"
+                color_text = "RED"
+                action_hint = "Consider SHORT position"
+            
+            message = f"""
+{main_emoji} <b>RSI CROSSOVER ALERT</b>
+
+{signal_emoji} <b>{signal_text} Signal Detected:</b>
+• Token: {token}
+• Timeframe: {self.timeframe}
+• RSI crossed {direction.upper()} RSI_SMA
+
+📊 <b>Current Values:</b>
+• RSI({self.rsi_period}): {current_rsi:.2f}
+• RSI_SMA({self.rsi_sma_period}): {current_rsi_sma:.2f}
+• Difference: {current_rsi - current_rsi_sma:+.2f}
+
+🎯 <b>Signal Strength:</b> {color_text} {signal_emoji}
+💡 <b>Suggestion:</b> {action_hint}
+
+⏰ <b>Time:</b> {timestamp.strftime('%H:%M:%S')}
+
+📱 <b>Quick Actions:</b>
+• /market {token} - View market data
+• /price {token} - Current price
+• /long {token} [amount] - Open long
+• /short {token} [amount] - Open short
+            """
+            
+            await self.notification_manager.send_generic_notification(message.strip())
+            
+            # Update state tracking
+            self.last_crossover_state[symbol] = crossover_type
+            self.last_notification_time[symbol] = timestamp
+            
+            logger.info(f"🔔 RSI crossover notification sent: {token} {signal_text} ({direction})")
+            
+        except Exception as e:
+            logger.error(f"❌ Error sending RSI crossover notification: {e}")
+    
+    async def monitor_symbol(self, symbol: str):
+        """
+        Monitor a single symbol for RSI crossovers.
+        Only calculates RSI if there's a new candle.
+        
+        Args:
+            symbol: Trading symbol to monitor
+        """
+        if not self.enabled:
+            return
+        
+        try:
+            # First check if there's a new candle - if not, skip calculation
+            if not await self.has_new_candle(symbol):
+                logger.debug(f"🔄 No new candle for {symbol}, skipping RSI calculation")
+                return
+            
+            logger.debug(f"🔍 New candle detected - checking RSI crossover for {symbol}")
+            
+            # Check for crossover (this will fetch full candle data for calculation)
+            crossover_info = await self.check_rsi_crossover(symbol)
+            
+            if crossover_info:
+                crossover_type = crossover_info['type']
+                
+                # Check if we should send notification
+                if await self.should_send_notification(symbol, crossover_type):
+                    await self.send_crossover_notification(symbol, crossover_info)
+                else:
+                    logger.debug(f"🔕 Skipping notification for {symbol} ({crossover_type}) - cooldown or duplicate")
+            
+        except Exception as e:
+            logger.error(f"❌ Error monitoring RSI for {symbol}: {e}")
+    
+    async def monitor_symbols_for_new_candles(self, symbols: List[str]):
+        """
+        Efficiently monitor multiple symbols for RSI crossovers.
+        Only calculates RSI for symbols that have new candles.
+        This method is optimized for integration with MarketMonitor heartbeat.
+        
+        Args:
+            symbols: List of trading symbols to monitor
+        """
+        if not self.enabled:
+            logger.debug("📊 RSI monitoring is disabled")
+            return
+        
+        if not symbols:
+            logger.debug("⚠️ No symbols provided for RSI monitoring")
+            return
+        
+        # First pass: Check which symbols have new candles (lightweight)
+        symbols_with_new_candles = []
+        for symbol in symbols:
+            try:
+                if await self.has_new_candle(symbol):
+                    symbols_with_new_candles.append(symbol)
+            except Exception as e:
+                logger.warning(f"⚠️ Error checking new candle for {symbol}: {e}")
+                continue
+        
+        if not symbols_with_new_candles:
+            logger.debug(f"🔄 No new candles detected for any of {len(symbols)} monitored symbols")
+            return
+        
+        logger.info(f"🕯️ New candles detected for {len(symbols_with_new_candles)}/{len(symbols)} symbols: {symbols_with_new_candles}")
+        
+        # Second pass: Only calculate RSI for symbols with new candles
+        for symbol in symbols_with_new_candles:
+            try:
+                logger.debug(f"🔍 Calculating RSI crossover for {symbol} (new candle)")
+                
+                # Check for crossover (this will fetch full candle data for calculation)
+                crossover_info = await self.check_rsi_crossover(symbol)
+                
+                if crossover_info:
+                    crossover_type = crossover_info['type']
+                    
+                    # Check if we should send notification
+                    if await self.should_send_notification(symbol, crossover_type):
+                        await self.send_crossover_notification(symbol, crossover_info)
+                    else:
+                        logger.debug(f"🔕 Skipping notification for {symbol} ({crossover_type}) - cooldown or duplicate")
+                
+            except Exception as e:
+                logger.error(f"❌ Error calculating RSI for {symbol}: {e}")
+                continue
+    
+    async def monitor_symbols(self, symbols: List[str]):
+        """
+        Monitor multiple symbols for RSI crossovers (legacy method).
+        For backward compatibility - calls the new efficient method.
+        
+        Args:
+            symbols: List of trading symbols to monitor
+        """
+        await self.monitor_symbols_for_new_candles(symbols)
+    
+    def get_status(self) -> Dict[str, Any]:
+        """Get current status of RSI monitor."""
+        return {
+            'enabled': self.enabled,
+            'timeframe': self.timeframe,
+            'rsi_period': self.rsi_period,
+            'rsi_sma_period': self.rsi_sma_period,
+            'notification_cooldown': self.notification_cooldown,
+            'monitored_symbols': list(self.last_crossover_state.keys()),
+            'last_states': dict(self.last_crossover_state),
+            'tracked_candle_timestamps': dict(self.last_candle_timestamps),
+            'symbols_with_candle_data': len(self.last_candle_timestamps)
+        } 

+ 1 - 1
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.4.272"
+BOT_VERSION = "2.5.273"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))