Просмотр исходного кода

Refactor order and position data handling for improved performance and accuracy

- Updated OrdersCommands to retrieve open orders from the database, enhancing speed and reliability.
- Simplified position data retrieval in PositionsCommands to rely solely on database data, ensuring consistency and reducing dependency on live exchange data.
- Enhanced logging and error handling for better traceability in order and position calculations.
- Added configurable data update intervals to improve monitoring responsiveness.
Carles Sentis 17 часов назад
Родитель
Сommit
75bd81be26
4 измененных файлов с 45 добавлено и 119 удалено
  1. 1 1
      src/commands/info/balance.py
  2. 19 20
      src/commands/info/orders.py
  3. 24 97
      src/commands/info/positions.py
  4. 1 1
      trading_bot.py

+ 1 - 1
src/commands/info/balance.py

@@ -75,7 +75,7 @@ class BalanceCommands(InfoCommandsBase):
             trading_engine_active = "✅ Active" if self.trading_engine else "❌ Inactive (Error)"
             balance_text_parts.append("\n⚙️ <b>System Status:</b>")
             balance_text_parts.append(f"• Trading Engine: {trading_engine_active}")
-            balance_text_parts.append(f"• Data Source: Exchange (Live)")  # Balance is usually live
+            balance_text_parts.append(f"• Data Source: Exchange (Live)")  # Balance is fetched live for accuracy
             balance_text_parts.append(f"• Last Update: {datetime.now().strftime('%H:%M:%S')}")
             
             final_message = "\n".join(balance_text_parts)

+ 19 - 20
src/commands/info/orders.py

@@ -16,17 +16,21 @@ class OrdersCommands(InfoCommandsBase):
                 await self._reply(update, "❌ Unauthorized access.")
                 return
 
-            # Get open orders from the exchange
-            open_orders = self.trading_engine.get_orders() or []
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                await self._reply(update, "❌ Trading stats not available.")
+                return
+
+            # Get open orders from database (much faster than exchange query)
+            db_orders = stats.get_orders_by_status('open', limit=50)
+            db_orders.extend(stats.get_orders_by_status('submitted', limit=50))
+            db_orders.extend(stats.get_orders_by_status('pending_trigger', limit=50))
 
             # Get pending stop loss orders from the database
-            stats = self.trading_engine.get_stats()
-            pending_sl_lifecycles = []
-            if stats:
-                pending_sl_lifecycles = stats.get_pending_stop_losses_from_lifecycles()
+            pending_sl_lifecycles = stats.get_pending_stop_losses_from_lifecycles()
             
             # Combine both lists
-            all_orders = open_orders + pending_sl_lifecycles
+            all_orders = db_orders + pending_sl_lifecycles
 
             if not all_orders:
                 await self._reply(update, "📭 No open or pending orders")
@@ -52,15 +56,15 @@ class OrdersCommands(InfoCommandsBase):
                         status = f"Awaiting {order.get('status', '').upper()} Entry" # e.g. Awaiting PENDING Entry
                         order_id = order.get('trade_lifecycle_id', 'unknown')
                     else:
-                        # This is a regular exchange order
+                        # This is a regular database order
                         order_type = order.get('type', 'unknown').upper()
                         side = order.get('side', 'unknown').upper()
                         price = float(order.get('price', 0))
-                        amount = float(order.get('amount', 0))
-                        filled = float(order.get('filled', 0))
-                        amount = amount - filled # Show remaining amount
+                        amount_requested = float(order.get('amount_requested', 0))
+                        amount_filled = float(order.get('amount_filled', 0))
+                        amount = amount_requested - amount_filled # Show remaining amount
                         status = order.get('status', 'unknown').upper()
-                        order_id = order.get('id', 'unknown')
+                        order_id = order.get('exchange_order_id') or order.get('bot_order_ref_id', 'unknown')
 
                     # Skip fully filled orders
                     if amount <= 0 and not is_pending_sl:
@@ -77,13 +81,6 @@ class OrdersCommands(InfoCommandsBase):
                     orders_text += f"   Status: {status.replace('_', ' ')}\n"
                     orders_text += f"   📏 Amount: {amount_str}\n"
                     orders_text += f"   💰 {'Trigger Price' if 'STOP' in order_type else 'Price'}: {price_str}\n"
-
-                    # Add order type specific info for exchange orders
-                    if not is_pending_sl and (order_type == "STOP_LOSS" or order_type == "TAKE_PROFIT"):
-                        trigger_price = order.get('info', {}).get('triggerPrice')
-                        if trigger_price:
-                            trigger_price_str = await formatter.format_price_with_symbol(float(trigger_price), base_asset)
-                            orders_text += f"   🎯 Trigger: {trigger_price_str}\n"
                     
                     # Add order ID
                     id_label = "Lifecycle ID" if is_pending_sl else "Order ID"
@@ -94,7 +91,9 @@ class OrdersCommands(InfoCommandsBase):
                     continue
             
             # Add footer
-            orders_text += "💡 Pending SL orders are activated when the entry order fills."
+            orders_text += "💡 Pending SL orders are activated when the entry order fills.\n"
+            from src.config.config import Config
+            orders_text += f"🔄 Data updated every {Config.BOT_HEARTBEAT_SECONDS}s via monitoring system"
 
             await self._reply(update, orders_text.strip())
 

+ 24 - 97
src/commands/info/positions.py

@@ -22,45 +22,18 @@ class PositionsCommands(InfoCommandsBase):
                 await self._reply(update, "❌ Trading stats not available.")
                 return
 
-            # Get open positions from DB
+            # Get open positions from DB (updated every heartbeat by PositionTracker)
             open_positions = stats.get_open_positions()
             if not open_positions:
                 await self._reply(update, "📭 No open positions\n\n💡 Use /long or /short to open a position")
                 return
 
-            # Get current exchange data for live ROE and mark prices
-            exchange_positions = self.trading_engine.get_positions() or []
-            exchange_orders = self.trading_engine.get_orders() or []
-            
-            # Debug: Log the exchange positions data structure
-            logger.info(f"🔍 Exchange positions data: {[{k: v for k, v in pos.items() if k in ['symbol']} for pos in exchange_positions[:2]]}")
-            
-            # Create lookup for exchange data by symbol
-            exchange_data_by_symbol = {}
-            for ex_pos in exchange_positions:
-                # Access the nested position data in info.position
-                position_data = ex_pos.get('info', {}).get('position', {})
-                if position_data:
-                    # Get the coin symbol from the nested data
-                    coin = position_data.get('coin', '')
-                    if coin:
-                        exchange_data_by_symbol[coin] = position_data
-                
-                # Also map by the CCXT symbol for backup
-                symbol = ex_pos.get('symbol', '')
-                if symbol and '/' in symbol:
-                    base_token = symbol.split('/')[0]
-                    if base_token not in exchange_data_by_symbol and position_data:
-                        exchange_data_by_symbol[base_token] = position_data
-                    
-            logger.info(f"🔍 Exchange data keys: {list(exchange_data_by_symbol.keys())}")
-
             # Initialize totals
             total_position_value = 0.0
             total_unrealized = 0.0
             total_margin_used = 0.0
 
-            # Build position details
+            # Build position details using only database data
             positions_text = "📊 <b>Open Positions</b>\n\n"
             
             for position_trade in open_positions:
@@ -70,7 +43,7 @@ class PositionsCommands(InfoCommandsBase):
                     base_asset = symbol.split('/')[0] if '/' in symbol else symbol
                     position_side = position_trade.get('position_side', 'unknown')
                     
-                    # Safely convert numeric values with proper null checks
+                    # Get all data from database (updated every heartbeat)
                     entry_price = 0.0
                     if position_trade.get('entry_price') is not None:
                         try:
@@ -115,56 +88,25 @@ class PositionsCommands(InfoCommandsBase):
                             logger.warning(f"Could not parse position_opened_at: {position_opened_at_str} for {symbol}")
                             duration_str = "Error"
                     
-                    # Get exchange data for this position (used for ROE and mark price)
-                    exchange_data = exchange_data_by_symbol.get(base_asset)
-                    logger.info(f"🔍 Looking for '{base_asset}' in exchange data. Found: {exchange_data is not None}")
-                    if exchange_data:
-                        logger.info(f"🔍 Exchange data for {base_asset}: ROE={exchange_data.get('returnOnEquity')}, entryPx={exchange_data.get('entryPx')}")
-                    
-                    # Get price data with defaults - prioritize live exchange data
-                    mark_price = entry_price  # Default to entry price
-                    
-                    # For Hyperliquid, we need to calculate current mark price from position value and size
-                    if exchange_data:
-                        try:
-                            position_value = float(exchange_data.get('positionValue', 0))
-                            position_size = float(exchange_data.get('szi', 0))
-                            if position_size != 0:
-                                mark_price = position_value / abs(position_size)
-                        except (ValueError, TypeError):
-                            logger.warning(f"Could not calculate mark price from exchange data for {symbol}")
-                    
-                    # Fallback to database mark price if calculation fails
-                    if mark_price == entry_price and position_trade.get('mark_price') is not None:
+                    # Get mark price from database (updated every heartbeat)
+                    mark_price = entry_price
+                    if position_trade.get('mark_price') is not None:
                         try:
                             mark_price = float(position_trade['mark_price'])
                         except (ValueError, TypeError):
                             logger.warning(f"Could not convert database mark_price for {symbol}")
                     
-                    # Get unrealized PnL from exchange data first, then database
+                    # Get unrealized PnL from database (updated every heartbeat)
                     unrealized_pnl = 0.0
-                    if exchange_data and exchange_data.get('unrealizedPnl') is not None:
-                        try:
-                            unrealized_pnl = float(exchange_data['unrealizedPnl'])
-                        except (ValueError, TypeError):
-                            logger.warning(f"Could not convert exchange unrealizedPnl for {symbol}")
-                    elif position_trade.get('unrealized_pnl') is not None:
+                    if position_trade.get('unrealized_pnl') is not None:
                         try:
                             unrealized_pnl = float(position_trade['unrealized_pnl'])
                         except (ValueError, TypeError):
                             logger.warning(f"Could not convert database unrealized_pnl for {symbol}")
                     
-                    # Get ROE from live exchange data (much more accurate)
+                    # Get ROE from database (updated every heartbeat)
                     roe_percentage = 0.0
-                    if exchange_data and exchange_data.get('returnOnEquity') is not None:
-                        try:
-                            # Convert from decimal (0.118) to percentage (11.8%)
-                            roe_percentage = float(exchange_data['returnOnEquity']) * 100
-                        except (ValueError, TypeError):
-                            logger.warning(f"Could not convert exchange ROE for {symbol}")
-                    
-                    # Fallback to database ROE if exchange data not available
-                    if roe_percentage == 0.0 and position_trade.get('roe_percentage') is not None:
+                    if position_trade.get('roe_percentage') is not None:
                         try:
                             roe_percentage = float(position_trade['roe_percentage'])
                         except (ValueError, TypeError):
@@ -199,14 +141,20 @@ class PositionsCommands(InfoCommandsBase):
                     pos_emoji = "🟢" if position_side == 'long' else "🔴"
                     direction_text = position_side.upper()
                     
-                    leverage = position_trade.get('leverage')
+                    # Get leverage from database (updated every heartbeat)
+                    leverage = None
+                    if position_trade.get('leverage') is not None:
+                        try:
+                            leverage = float(position_trade['leverage'])
+                        except (ValueError, TypeError):
+                            logger.warning(f"Could not convert database leverage for {symbol}")
+                    
                     if leverage is not None:
                         try:
-                            leverage_val = float(leverage)
-                            leverage_str = f"x{leverage_val:.1f}".rstrip('0').rstrip('.') if '.' in f"{leverage_val:.1f}" else f"x{int(leverage_val)}"
+                            leverage_str = f"x{leverage:.1f}".rstrip('0').rstrip('.') if '.' in f"{leverage:.1f}" else f"x{int(leverage)}"
                             direction_text = f"{direction_text} {leverage_str}"
                         except ValueError:
-                            logger.warning(f"Could not parse leverage value: {leverage} for {symbol}")
+                            logger.warning(f"Could not format leverage value: {leverage} for {symbol}")
 
                     # --- Format Output String ---
                     formatter = self._get_formatter()
@@ -266,29 +214,6 @@ class PositionsCommands(InfoCommandsBase):
                     logger.error(f"Error processing position {position_trade.get('symbol', 'unknown')}: {e}")
                     continue
 
-            # Calculate total unrealized P&L and total ROE
-            total_unrealized_pnl = 0.0
-            total_roe = 0.0
-            for pos in open_positions:
-                try:
-                    size = float(pos.get('size', 0)) if pos.get('size') is not None else 0.0
-                    entry_price = float(pos.get('entryPrice', 0)) if pos.get('entryPrice') is not None else 0.0
-                    # Handle None markPrice safely
-                    mark_price_raw = pos.get('markPrice')
-                    mark_price = float(mark_price_raw) if mark_price_raw is not None else entry_price
-                    roe = float(pos.get('roe_percentage', 0)) if pos.get('roe_percentage') is not None else 0.0
-                    if size != 0 and entry_price != 0:
-                        position_value = abs(size * entry_price)
-                        total_unrealized_pnl += size * (mark_price - entry_price)
-                        total_roe += roe * position_value
-                        total_position_value += position_value
-                except (ValueError, TypeError) as e:
-                    logger.warning(f"Error calculating portfolio totals for position: {e}")
-                    continue
-            # Weighted average ROE
-            avg_roe = (total_roe / total_position_value) if total_position_value > 0 else 0.0
-            roe_emoji = "🟢" if avg_roe >= 0 else "🔴"
-
             # Add portfolio summary
             portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
             positions_text += f"💼 <b>Total Portfolio:</b>\n"
@@ -302,8 +227,10 @@ class PositionsCommands(InfoCommandsBase):
                 margin_pnl_percentage = (total_unrealized / total_margin_used) * 100
                 positions_text += f"   📊 Portfolio Return: {margin_pnl_percentage:+.2f}% (on margin)\n"
             positions_text += "\n"
-            positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced • 🛡️ External SL\n"
-            positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
+            positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
+            positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management\n"
+            from src.config.config import Config
+            positions_text += f"🔄 Data updated every {Config.BOT_HEARTBEAT_SECONDS}s via monitoring system"
 
             await self._reply(update, positions_text.strip())
 

+ 1 - 1
trading_bot.py

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