Bläddra i källkod

Add initial price alarms JSON and trading stats SQLite file - Created a new JSON file for price alarms to manage alarm states and added a SQLite database for trading statistics, enhancing data persistence and management. Updated trading commands to reflect changes in order cancellation notifications, including detailed summaries of cancelled orders.

Carles Sentis 4 dagar sedan
förälder
incheckning
c6d9f2f62a

+ 6 - 0
data/price_alarms.json

@@ -0,0 +1,6 @@
+{
+  "next_id": 1,
+  "alarms": [],
+  "triggered_alarms": [],
+  "last_update": "2025-06-03T14:39:09.411363"
+}

BIN
data/trading_stats.sqlite


+ 4 - 3
src/commands/trading_commands.py

@@ -802,12 +802,13 @@ This action cannot be undone.
         result = await self.trading_engine.execute_coo_order(token)
         result = await self.trading_engine.execute_coo_order(token)
         
         
         if result["success"]:
         if result["success"]:
-            cancelled_count = len(result.get("db_updates_successful_ids", []))
-            failed_count = len(result.get("db_updates_failed_ids", []))
+            cancelled_count = result.get("cancelled_count", 0)
+            failed_count = result.get("failed_count", 0)
             cancelled_linked_sls = result.get("cancelled_linked_stop_losses", 0)
             cancelled_linked_sls = result.get("cancelled_linked_stop_losses", 0)
+            cancelled_orders = result.get("cancelled_orders", [])
             
             
             await self.notification_manager.send_coo_success_notification(
             await self.notification_manager.send_coo_success_notification(
-                query, token, cancelled_count, failed_count, cancelled_linked_sls
+                query, token, cancelled_count, failed_count, cancelled_linked_sls, cancelled_orders
             )
             )
         else:
         else:
             await query.edit_message_text(f"❌ Cancel orders failed: {result['error']}") 
             await query.edit_message_text(f"❌ Cancel orders failed: {result['error']}") 

+ 34 - 17
src/notifications/notification_manager.py

@@ -4,7 +4,7 @@ Notification Manager - Handles all bot notifications and messages.
 """
 """
 
 
 import logging
 import logging
-from typing import Optional, Dict, Any
+from typing import Optional, Dict, Any, List
 from datetime import datetime
 from datetime import datetime
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -193,38 +193,55 @@ class NotificationManager:
         logger.info(f"Take profit set: {token} @ ${tp_price:,.2f}")
         logger.info(f"Take profit set: {token} @ ${tp_price:,.2f}")
     
     
     async def send_coo_success_notification(self, query, token: str, cancelled_count: int, 
     async def send_coo_success_notification(self, query, token: str, cancelled_count: int, 
-                                          failed_count: int, cancelled_linked_sls: int = 0):
+                                          failed_count: int, cancelled_linked_sls: int = 0,
+                                          cancelled_orders: List[Dict[str, Any]] = None):
         """Send notification for successful cancel all orders operation."""
         """Send notification for successful cancel all orders operation."""
         
         
         success_message = f"""
         success_message = f"""
-✅ <b>Cancel Orders Complete!</b>
+✅ <b>Cancel Orders Results</b>
 
 
-📊 <b>Results for {token}:</b>
-• Orders Cancelled: {cancelled_count}
-• Failed Cancellations: {failed_count}
-• Total Processed: {cancelled_count + failed_count}"""
+📊 <b>Summary:</b>
+• Token: {token}
+• Cancelled: {cancelled_count} orders
+• Failed: {failed_count} orders
+• Total Attempted: {cancelled_count + failed_count} orders"""
         
         
         if cancelled_linked_sls > 0:
         if cancelled_linked_sls > 0:
             success_message += f"""
             success_message += f"""
 • 🛑 Linked Stop Losses Cancelled: {cancelled_linked_sls}"""
 • 🛑 Linked Stop Losses Cancelled: {cancelled_linked_sls}"""
         
         
-        success_message += f"""
-
-{f"⚠️ {failed_count} orders could not be cancelled" if failed_count > 0 else "🎯 All orders successfully cancelled"}
+        # Show details of cancelled orders if available
+        if cancelled_orders and len(cancelled_orders) > 0:
+            success_message += f"""
 
 
-📋 <b>Summary:</b>
-• Token: {token}
-• Status: {'PARTIAL SUCCESS' if failed_count > 0 else 'SUCCESS'} ✅
-• Time: {datetime.now().strftime('%H:%M:%S')}"""
+🗑️ <b>Successfully Cancelled:</b>"""
+            for order in cancelled_orders:
+                side = order.get('side', 'Unknown')
+                amount = order.get('amount', 0)
+                price = order.get('price', 0)
+                side_emoji = "🟢" if side.lower() == 'buy' else "🔴"
+                success_message += f"""
+{side_emoji} {side.upper()} {amount} @ ${price:,.2f}"""
         
         
-        if cancelled_linked_sls > 0:
+        # Overall status
+        if cancelled_count == (cancelled_count + failed_count) and failed_count == 0:
             success_message += f"""
             success_message += f"""
 
 
-🛑 <b>Cleanup:</b> Automatically cancelled {cancelled_linked_sls} pending stop loss(es) linked to cancelled orders"""
+🎉 All {token} orders successfully cancelled!"""
+        elif cancelled_count > 0:
+            success_message += f"""
+
+⚠️ Some orders cancelled. {failed_count} failed."""
+        else:
+            success_message += f"""
+
+❌ Could not cancel any {token} orders."""
         
         
         success_message += f"""
         success_message += f"""
 
 
-💡 Use /orders to verify no pending orders remain.
+⏰ <b>Time:</b> {datetime.now().strftime('%H:%M:%S')}
+
+📊 Use /orders to verify no pending orders remain.
         """
         """
         
         
         await query.edit_message_text(success_message, parse_mode='HTML')
         await query.edit_message_text(success_message, parse_mode='HTML')

+ 114 - 91
src/trading/trading_engine.py

@@ -591,97 +591,78 @@ class TradingEngine:
             logger.error(f"Error executing take profit order: {e}")
             logger.error(f"Error executing take profit order: {e}")
             return {"success": False, "error": str(e)}
             return {"success": False, "error": str(e)}
     
     
-    async def cancel_all_orders(self, token: str) -> Dict[str, Any]:
-        """Cancel all orders for a token."""
+    def cancel_all_orders(self, symbol: str) -> Tuple[List[Dict[str, Any]], Optional[str]]:
+        """Cancel all open orders for a specific symbol. Returns (cancelled_orders, error_message)."""
         try:
         try:
-            symbol = f"{token}/USDC:USDC"
-            # Assuming self.client.cancel_all_orders either succeeds and returns data or raises an exception
-            # If it can return (None, error_msg) like other order methods, this would need adjustment
-            # For now, sticking to its likely current CCXT-like behavior.
-            # If it was changed in hyperliquid_client.py, this needs to be updated.
-            # Let's assume cancel_all_orders was NOT changed to return a tuple for now.
-            # It usually returns specific data from CCXT or raises an error.
-            
-            # If self.client.cancel_all_orders was modified to return (data, error_msg):
-            # data, error_msg = self.client.cancel_all_orders(symbol)
-            # if error_msg:
-            #     logger.error(f"Error cancelling orders for {symbol}: {error_msg}")
-            #     return {"success": False, "error": f"Failed to cancel orders: {error_msg}"}
-            # return {"success": True, "result": data}
-            
-            # Sticking to original assumption based on typical CCXT cancel_all_orders:
-            # result = self.client.cancel_all_orders(symbol)
-            # CCXT cancel_all_orders often returns a list of cancelled order structures or similar.
-            # If it fails, it typically raises an exception handled by the generic catch block below.
-            
-            # logger.info(f"Attempted to cancel all orders for {symbol}. Result: {result}")
-            # return {
-            #     "success": True,
-            #     "result": result # This might be a list of order dicts, or specific response from API
-            # }
-
-            # New approach: Assume client.cancel_all_orders returns a list of dicts for cancelled orders, or raises error.
-            # Each dict should have an 'id' field representing the exchange_order_id.
-            cancelled_orders_info, error_msg = self.client.cancel_all_orders(symbol)
-
-            if error_msg:
-                # If the client method returns an error message, it means a general failure, 
-                # not specific order cancellation statuses.
-                logger.error(f"Error cancelling all orders for {symbol}: {error_msg}")
-                return {"success": False, "error": f"Failed to cancel orders: {error_msg}"}
-            
-            if cancelled_orders_info is None: # Should ideally be an empty list if no orders, not None
-                cancelled_orders_info = []
-                logger.info(f"No orders found or reported as cancelled for {symbol} by the client.")
-
-            successful_cancellations_db = []
-            failed_cancellations_db_update = []
-            
-            for order_info in cancelled_orders_info:
-                exchange_oid_to_update = order_info.get('id')
-                if exchange_oid_to_update:
-                    success = self.stats.update_order_status(exchange_order_id=exchange_oid_to_update, new_status='cancelled')
-                    if success:
-                        successful_cancellations_db.append(exchange_oid_to_update)
-                    else:
-                        failed_cancellations_db_update.append(exchange_oid_to_update)
-            
-            # Cancel any pending stop losses linked to the cancelled orders
-            total_cancelled_linked = 0
-            if self.stats and successful_cancellations_db:
-                for exchange_oid in successful_cancellations_db:
-                    # Get the order from DB to find its bot_order_ref_id
-                    order_in_db = self.stats.get_order_by_exchange_id(exchange_oid)
-                    if order_in_db and order_in_db.get('bot_order_ref_id'):
-                        cancelled_linked = self.stats.cancel_linked_orders(
-                            parent_bot_order_ref_id=order_in_db['bot_order_ref_id'],
-                            new_status='cancelled_parent_cancelled'
-                        )
-                        total_cancelled_linked += cancelled_linked
-                        if cancelled_linked > 0:
-                            logger.info(f"🛑 Cancelled {cancelled_linked} linked stop losses for order {exchange_oid}")
-            
-            # self.bot_trade_ids might need to be pruned of these OIDs if they are fully cancelled.
-            # However, relying on the 'orders' table status is better long-term.
-            # For now, _save_state() is not called here as bot_trade_ids interaction is complex with cancellations.
-
-            return {
-                "success": True,
-                "message": f"Cancellation request processed for {symbol}. {len(successful_cancellations_db)} marked in DB.",
-                "cancelled_on_exchange_ids": [info.get('id') for info in cancelled_orders_info if info.get('id')],
-                "db_updates_successful_ids": successful_cancellations_db,
-                "db_updates_failed_ids": failed_cancellations_db_update,
-                "cancelled_linked_stop_losses": total_cancelled_linked
-            }
-
+            logger.info(f"Attempting to cancel all orders for {symbol}")
+            
+            # Get all open orders
+            all_orders = self.client.get_open_orders()
+            if all_orders is None:
+                error_msg = f"Could not fetch orders to cancel {symbol} orders"
+                logger.error(error_msg)
+                return [], error_msg
+            
+            # Filter orders for the specific symbol
+            symbol_orders = [order for order in all_orders if order.get('symbol') == symbol]
+            
+            if not symbol_orders:
+                logger.info(f"No open orders found for {symbol}")
+                return [], None  # No error, just no orders to cancel
+            
+            # Cancel each order individually
+            cancelled_orders = []
+            failed_orders = []
+            
+            for order in symbol_orders:
+                order_id = order.get('id')
+                if order_id:
+                    try:
+                        success = self.client.cancel_order(order_id, symbol)
+                        if success:
+                            cancelled_orders.append(order)
+                            logger.info(f"Successfully cancelled order {order_id} for {symbol}")
+                        else:
+                            failed_orders.append(order)
+                            logger.warning(f"Failed to cancel order {order_id} for {symbol}")
+                    except Exception as e:
+                        logger.error(f"Exception cancelling order {order_id}: {e}")
+                        failed_orders.append(order)
+            
+            # Update order status in database if we have stats
+            if self.stats:
+                for order in cancelled_orders:
+                    order_id = order.get('id')
+                    if order_id:
+                        # Try to find this order in our database and update its status
+                        db_order = self.stats.get_order_by_exchange_id(order_id)
+                        if db_order:
+                            self.stats.update_order_status(
+                                exchange_order_id=order_id, 
+                                new_status='cancelled_manually'
+                            )
+                
+                # Cancel any linked pending stop losses for this symbol
+                cleanup_count = self.stats.cancel_pending_stop_losses_by_symbol(
+                    symbol, 
+                    'cancelled_manual_exit'
+                )
+                if cleanup_count > 0:
+                    logger.info(f"Cleaned up {cleanup_count} pending stop losses for {symbol}")
+            
+            # Prepare result
+            if failed_orders:
+                error_msg = f"Cancelled {len(cancelled_orders)}/{len(symbol_orders)} orders. {len(failed_orders)} failed."
+                logger.warning(error_msg)
+                return cancelled_orders, error_msg
+            else:
+                logger.info(f"Successfully cancelled all {len(cancelled_orders)} orders for {symbol}")
+                return cancelled_orders, None
+                
         except Exception as e:
         except Exception as e:
-            # If client.cancel_all_orders raises an Exception that is caught here,
-            # we can use the _extract_error_message if it's available from client.
-            error_message = str(e)
-            if hasattr(self.client, '_extract_error_message'):
-                error_message = self.client._extract_error_message(e)
-            logger.error(f"Error cancelling orders for {token}: {error_message}", exc_info=True)
-            return {"success": False, "error": f"Failed to cancel orders: {error_message}"}
+            error_msg = f"Error cancelling orders for {symbol}: {str(e)}"
+            logger.error(error_msg, exc_info=True)
+            return [], error_msg
     
     
     # Alias methods for consistency with command handlers
     # Alias methods for consistency with command handlers
     async def execute_sl_order(self, token: str, stop_price: float) -> Dict[str, Any]:
     async def execute_sl_order(self, token: str, stop_price: float) -> Dict[str, Any]:
@@ -693,8 +674,50 @@ class TradingEngine:
         return await self.execute_take_profit_order(token, profit_price)
         return await self.execute_take_profit_order(token, profit_price)
     
     
     async def execute_coo_order(self, token: str) -> Dict[str, Any]:
     async def execute_coo_order(self, token: str) -> Dict[str, Any]:
-        """Alias for cancel_all_orders."""
-        return await self.cancel_all_orders(token)
+        """Cancel all orders for a token and format response like the old code expected."""
+        try:
+            symbol = f"{token}/USDC:USDC"
+            
+            # Call the synchronous cancel_all_orders method
+            cancelled_orders, error_msg = self.cancel_all_orders(symbol)
+            
+            if error_msg:
+                logger.error(f"Error cancelling all orders for {token}: {error_msg}")
+                return {"success": False, "error": error_msg}
+            
+            if not cancelled_orders:
+                logger.info(f"No orders found to cancel for {token}")
+                return {
+                    "success": True,
+                    "message": f"No orders found for {token}",
+                    "cancelled_orders": [],
+                    "cancelled_count": 0,
+                    "failed_count": 0,
+                    "cancelled_linked_stop_losses": 0
+                }
+            
+            # Get cleanup count from stats if available 
+            cleanup_count = 0
+            if self.stats:
+                cleanup_count = self.stats.cancel_pending_stop_losses_by_symbol(
+                    symbol, 
+                    'cancelled_manual_exit'
+                )
+            
+            logger.info(f"Successfully cancelled {len(cancelled_orders)} orders for {token}")
+            
+            return {
+                "success": True,
+                "message": f"Successfully cancelled {len(cancelled_orders)} orders for {token}",
+                "cancelled_orders": cancelled_orders,
+                "cancelled_count": len(cancelled_orders), 
+                "failed_count": 0,  # Failed orders are handled in the error case above
+                "cancelled_linked_stop_losses": cleanup_count
+            }
+            
+        except Exception as e:
+            logger.error(f"Error in execute_coo_order for {token}: {e}")
+            return {"success": False, "error": str(e)}
     
     
     def is_bot_trade(self, exchange_order_id: str) -> bool:
     def is_bot_trade(self, exchange_order_id: str) -> bool:
         """Check if an order (by its exchange ID) was recorded by this bot in the orders table."""
         """Check if an order (by its exchange ID) was recorded by this bot in the orders table."""