Browse Source

Enhance position and order display in Telegram bot.

- Improved Telegram bot notifications to include counts of open positions and orders for better user insights.
- Refined logic in info_commands to display accurate counts of open orders and positions, enhancing user experience.
- Added checks in external event monitoring to prevent duplicate notifications and improve logging for filled orders.
Carles Sentis 1 ngày trước cách đây
mục cha
commit
46ced09e99

+ 6 - 4
src/backup/telegram_bot.py

@@ -627,11 +627,13 @@ Tap any button below for instant access to bot functions:
         positions = self.client.get_positions()
         
         if positions is not None:  # Successfully fetched (could be empty list)
-            positions_text = "📈 <b>Open Positions</b>\n\n"
-            
             # Filter for actual open positions
             open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
             
+            # Add position count to header
+            position_count = len(open_positions)
+            positions_text = f"📈 <b>Open Positions ({position_count})</b>\n\n"
+            
             if open_positions:
                 total_unrealized = 0
                 total_position_value = 0
@@ -715,13 +717,13 @@ Tap any button below for instant access to bot functions:
         
         if orders is not None:  # Successfully fetched (could be empty list)
             if token_filter:
-                orders_text = f"📋 <b>Open Orders - {token_filter}</b>\n\n"
                 # Filter orders for specific token
                 target_symbol = f"{token_filter}/USDC:USDC"
                 filtered_orders = [order for order in orders if order.get('symbol') == target_symbol]
+                orders_text = f"📋 <b>Open Orders - {token_filter} ({len(filtered_orders)})</b>\n\n"
             else:
-                orders_text = "📋 <b>All Open Orders</b>\n\n"
                 filtered_orders = orders
+                orders_text = f"📋 <b>All Open Orders ({len(orders)})</b>\n\n"
             
             if filtered_orders and len(filtered_orders) > 0:
                 for order in filtered_orders:

+ 5 - 3
src/commands/info_commands.py

@@ -162,7 +162,9 @@ class InfoCommands:
             # Get open positions from unified trades table
             open_positions = stats.get_open_positions()
             
-            positions_text = f"📈 <b>Open Positions</b>\n\n{sync_msg}" # sync_msg will be empty
+            # Add position count to header
+            position_count = len(open_positions) if open_positions else 0
+            positions_text = f"📈 <b>Open Positions ({position_count})</b>\n\n{sync_msg}" # sync_msg will be empty
             
             if open_positions:
                 total_unrealized = 0
@@ -417,7 +419,7 @@ class InfoCommands:
             
             if orders is not None:
                 if len(orders) > 0:
-                    orders_text = "📋 <b>Open Orders</b>\n\n"
+                    orders_text = f"📋 <b>Open Orders ({len(orders)})</b>\n\n"
                     
                     # Group orders by symbol
                     orders_by_symbol = {}
@@ -511,7 +513,7 @@ class InfoCommands:
                     orders_text += f"💡 Use /coo [token] to cancel orders"
                     
                 else:
-                    orders_text = "📋 <b>Open Orders</b>\n\n"
+                    orders_text = "📋 <b>Open Orders (0)</b>\n\n"
                     orders_text += "📭 No open orders\n\n"
                     # Check for purely conceptual pending SLs even if no exchange orders are open
                     stats_for_empty_check = self.trading_engine.get_stats()

+ 11 - 14
src/monitoring/external_event_monitor.py

@@ -285,11 +285,8 @@ class ExternalEventMonitor:
 📊 Position remains open. Use /positions to view details
                 """
             else:
-                # Fallback to generic notification
-                await self.notification_manager.send_external_trade_notification(
-                    full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                    action_type, timestamp_dt.isoformat()
-                )
+                # No fallback notification sent - only position-based notifications per user preference
+                logger.debug(f"No notification sent for action_type: {action_type}")
                 return
             
             await self.notification_manager.send_generic_notification(message.strip())
@@ -338,6 +335,11 @@ class ExternalEventMonitor:
                     if self.last_processed_trade_time and timestamp_dt <= self.last_processed_trade_time:
                         continue
                     
+                    # Check if this fill has already been processed to prevent duplicates
+                    if trade_id and stats.has_exchange_fill_been_processed(str(trade_id)):
+                        logger.debug(f"Skipping already processed fill: {trade_id}")
+                        continue
+                    
                     fill_processed_this_iteration = False
                     
                     if not (symbol_from_fill and side_from_fill and amount_from_fill > 0 and price_from_fill > 0):
@@ -438,7 +440,7 @@ class ExternalEventMonitor:
                                     'position_closed', timestamp_dt, active_lc, realized_pnl
                                 )
                                 
-                                stats._migrate_trade_to_aggregated_stats(lc_id)
+                                stats.migrate_trade_to_aggregated_stats(lc_id)
                                 if bot_order_db_id_to_update:
                                     stats.update_order_status(order_db_id=bot_order_db_id_to_update, new_status='filled', amount_filled_increment=amount_from_fill)
                                 fill_processed_this_iteration = True
@@ -468,7 +470,7 @@ class ExternalEventMonitor:
                                             stop_loss_info, full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
                                             f'{lc_pos_side}_closed_external_sl', timestamp_dt.isoformat(), realized_pnl
                                         )
-                                    stats._migrate_trade_to_aggregated_stats(lc_id)
+                                    stats.migrate_trade_to_aggregated_stats(lc_id)
                                     # Modify shared state carefully
                                     if exchange_order_id_from_fill in self.shared_state['external_stop_losses']:
                                         del self.shared_state['external_stop_losses'][exchange_order_id_from_fill]
@@ -543,7 +545,7 @@ class ExternalEventMonitor:
                                     action_type, timestamp_dt, existing_lc, realized_pnl
                                 )
                                 
-                                stats._migrate_trade_to_aggregated_stats(lc_id)
+                                stats.migrate_trade_to_aggregated_stats(lc_id)
                                 fill_processed_this_iteration = True
                         
                         elif action_type in ['position_increased', 'position_decreased'] and existing_lc:
@@ -591,12 +593,7 @@ class ExternalEventMonitor:
                         )
                         logger.info(f"📋 Recorded trade via FALLBACK: {trade_id} (Unmatched External Fill)")
                         
-                        # Send generic notification for unmatched trade
-                        if self.notification_manager:
-                            await self.notification_manager.send_external_trade_notification(
-                                full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                "external_unmatched", timestamp_dt.isoformat()
-                            )
+                        # No notification sent for unmatched external trades per user preference
                         fill_processed_this_iteration = True
 
                     if fill_processed_this_iteration:

+ 14 - 1
src/monitoring/order_fill_processor.py

@@ -156,7 +156,20 @@ class OrderFillProcessor:
                             logger.info(f"ℹ️ Stop losses for order {exchange_oid} (status: {parent_order_current_state.get('status') if parent_order_current_state else 'N/A'}) preserved as parent order is considered filled or partially filled.")
                             continue
                 else:
-                    logger.warning(f"Order {exchange_oid} disappeared from exchange but was not found in our DB. This might be an order placed externally.")
+                    # Check if this order was actually filled by looking for recent fills 
+                    # This is normal behavior for external orders that get filled
+                    recent_fills = self.trading_engine.get_recent_fills()
+                    order_was_filled = False
+                    if recent_fills:
+                        for fill in recent_fills[-10:]:  # Check last 10 fills
+                            if fill.get('info', {}).get('oid') == exchange_oid:
+                                order_was_filled = True
+                                break
+                    
+                    if order_was_filled:
+                        logger.info(f"Order {exchange_oid} disappeared but was filled externally. This is normal for external orders.")
+                    else:
+                        logger.warning(f"Order {exchange_oid} disappeared from exchange but was not found in our DB. This might be an external order or a timing issue.")
 
             if len(external_cancellations) > 1:
                 tokens_affected = list(set(item['token'] for item in external_cancellations))

+ 14 - 0
src/stats/trading_stats.py

@@ -135,6 +135,20 @@ class TradingStats:
     def get_order_by_bot_ref_id(self, bot_order_ref_id: str) -> Optional[Dict[str, Any]]:
         """Get order by bot reference ID."""
         return self.order_manager.get_order_by_bot_ref_id(bot_order_ref_id)
+
+    def has_exchange_fill_been_processed(self, exchange_fill_id: str) -> bool:
+        """Check if an exchange fill ID has already been processed."""
+        try:
+            # Check trades table which is the primary place where all processed fills are recorded
+            trade_exists = self.db_manager._fetch_query(
+                "SELECT 1 FROM trades WHERE exchange_fill_id = ? LIMIT 1",
+                (exchange_fill_id,)
+            )
+            
+            return bool(trade_exists)
+        except Exception as e:
+            logger.error(f"Error checking if fill {exchange_fill_id} was processed: {e}")
+            return False
     
     def get_orders_by_symbol(self, symbol: str, limit: int = 50) -> List[Dict[str, Any]]:
         """Get orders by symbol."""

+ 1 - 1
trading_bot.py

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