Переглянути джерело

Implement price formatting utilities and integrate into trading commands - Added a new PriceFormatter class for handling price formatting with exchange-specific precision. Updated InfoCommands and TradingCommands to utilize the new formatter for displaying prices, enhancing clarity and consistency in user notifications. Initialized the formatter in TradingEngine to ensure proper integration across the application.

Carles Sentis 4 днів тому
батько
коміт
cf7ab53c06

+ 32 - 11
src/commands/info_commands.py

@@ -10,6 +10,7 @@ from telegram import Update
 from telegram.ext import ContextTypes
 
 from src.config.config import Config
+from src.utils.price_formatter import format_price_with_symbol, get_formatter
 
 logger = logging.getLogger(__name__)
 
@@ -153,10 +154,15 @@ class InfoCommands:
                     pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
                     pnl_percent = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
                     
+                    # Format prices with proper precision for this token
+                    formatter = get_formatter()
+                    entry_price_str = formatter.format_price_with_symbol(entry_price, symbol)
+                    mark_price_str = formatter.format_price_with_symbol(mark_price, symbol)
+                    
                     positions_text += f"{pos_emoji} <b>{symbol} ({direction})</b>\n"
                     positions_text += f"   📏 Size: {abs(contracts):.6f} {symbol}\n"
-                    positions_text += f"   💰 Entry: ${entry_price:,.2f}\n"
-                    positions_text += f"   📊 Mark: ${mark_price:,.2f}\n"
+                    positions_text += f"   💰 Entry: {entry_price_str}\n"
+                    positions_text += f"   📊 Mark: {mark_price_str}\n"
                     positions_text += f"   💵 Value: ${position_value:,.2f}\n"
                     positions_text += f"   {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percent:+.2f}%)\n\n"
                 
@@ -327,19 +333,29 @@ class InfoCommands:
             # Market direction emoji
             trend_emoji = "🟢" if change_24h >= 0 else "🔴"
             
+            # Format prices with proper precision for this token
+            formatter = get_formatter()
+            current_price_str = formatter.format_price_with_symbol(current_price, token)
+            bid_price_str = formatter.format_price_with_symbol(bid_price, token)
+            ask_price_str = formatter.format_price_with_symbol(ask_price, token)
+            spread_str = formatter.format_price_with_symbol(ask_price - bid_price, token)
+            high_24h_str = formatter.format_price_with_symbol(high_24h, token)
+            low_24h_str = formatter.format_price_with_symbol(low_24h, token)
+            change_24h_str = formatter.format_price_with_symbol(change_24h, token)
+            
             market_text = f"""
 📊 <b>{token} Market Data</b>
 
 💰 <b>Price Information:</b>
-   💵 Current: ${current_price:,.2f}
-   🟢 Bid: ${bid_price:,.2f}
-   🔴 Ask: ${ask_price:,.2f}
-   📊 Spread: ${ask_price - bid_price:,.2f}
+   💵 Current: {current_price_str}
+   🟢 Bid: {bid_price_str}
+   🔴 Ask: {ask_price_str}
+   📊 Spread: {spread_str}
 
 📈 <b>24h Statistics:</b>
-   {trend_emoji} Change: ${change_24h:,.2f} ({change_percent:+.2f}%)
-   🔝 High: ${high_24h:,.2f}
-   🔻 Low: ${low_24h:,.2f}
+   {trend_emoji} Change: {change_24h_str} ({change_percent:+.2f}%)
+   🔝 High: {high_24h_str}
+   🔻 Low: {low_24h_str}
    📊 Volume: {volume_24h:,.2f} {token}
 
 ⏰ <b>Last Updated:</b> {datetime.now().strftime('%H:%M:%S')}
@@ -376,11 +392,16 @@ class InfoCommands:
             # Price direction emoji
             trend_emoji = "🟢" if change_24h >= 0 else "🔴"
             
+            # Format prices with proper precision for this token
+            formatter = get_formatter()
+            current_price_str = formatter.format_price_with_symbol(current_price, token)
+            change_24h_str = formatter.format_price_with_symbol(change_24h, token)
+            
             price_text = f"""
 💵 <b>{token} Price</b>
 
-💰 ${current_price:,.2f}
-{trend_emoji} {change_percent:+.2f}% (${change_24h:+.2f})
+💰 {current_price_str}
+{trend_emoji} {change_percent:+.2f}% ({change_24h_str})
 
 ⏰ {datetime.now().strftime('%H:%M:%S')}
             """

+ 53 - 48
src/commands/trading_commands.py

@@ -9,6 +9,7 @@ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
 from telegram.ext import ContextTypes
 
 from src.config.config import Config
+from src.utils.price_formatter import get_formatter
 
 logger = logging.getLogger(__name__)
 
@@ -92,31 +93,33 @@ class TradingCommands:
             
             # Validate stop loss for long positions
             if stop_loss_price and stop_loss_price >= price:
+                formatter = get_formatter()
                 await context.bot.send_message(chat_id=chat_id, text=(
                     f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n"
                     f"📊 Your order:\n"
-                    f"• Entry Price: ${price:,.2f}\n"
-                    f"• Stop Loss: ${stop_loss_price:,.2f} ❌\n\n"
-                    f"💡 Try a lower stop loss like: sl:{price * 0.95:.0f}"
+                    f"• Entry Price: {formatter.format_price_with_symbol(price, token)}\n"
+                    f"• Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)} ❌\n\n"
+                    f"💡 Try a lower stop loss like: sl:{formatter.format_price(price * 0.95, token)}"
                 ))
                 return
             
             # Create confirmation message
+            formatter = get_formatter()
             confirmation_text = f"""
 🟢 <b>Long Order Confirmation</b>
 
 📊 <b>Order Details:</b>
 • Token: {token}
-• USDC Amount: ${usdc_amount:,.2f}
+• USDC Amount: {formatter.format_price_with_symbol(usdc_amount)}
 • Token Amount: {token_amount:.6f} {token}
 • Order Type: {order_type}
-• Price: ${price:,.2f}
-• Current Price: ${current_price:,.2f}
-• Est. Value: ${token_amount * price:,.2f}"""
+• Price: {formatter.format_price_with_symbol(price, token)}
+• Current Price: {formatter.format_price_with_symbol(current_price, token)}
+• Est. Value: {formatter.format_price_with_symbol(token_amount * price)}"""
             
             if stop_loss_price:
                 confirmation_text += f"""
-• 🛑 Stop Loss: ${stop_loss_price:,.2f}"""
+• 🛑 Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)}"""
             
             confirmation_text += f"""
 
@@ -211,31 +214,33 @@ This will {"place a limit buy order" if limit_price else "execute a market buy o
             
             # Validate stop loss for short positions
             if stop_loss_price and stop_loss_price <= price:
+                formatter = get_formatter()
                 await context.bot.send_message(chat_id=chat_id, text=(
                     f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n"
                     f"📊 Your order:\n"
-                    f"• Entry Price: ${price:,.2f}\n"
-                    f"• Stop Loss: ${stop_loss_price:,.2f} ❌\n\n"
-                    f"💡 Try a higher stop loss like: sl:{price * 1.05:.0f}"
+                    f"• Entry Price: {formatter.format_price_with_symbol(price, token)}\n"
+                    f"• Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)} ❌\n\n"
+                    f"💡 Try a higher stop loss like: sl:{formatter.format_price(price * 1.05, token)}"
                 ))
                 return
             
             # Create confirmation message
+            formatter = get_formatter()
             confirmation_text = f"""
 🔴 <b>Short Order Confirmation</b>
 
 📊 <b>Order Details:</b>
 • Token: {token}
-• USDC Amount: ${usdc_amount:,.2f}
+• USDC Amount: {formatter.format_price_with_symbol(usdc_amount)}
 • Token Amount: {token_amount:.6f} {token}
 • Order Type: {order_type}
-• Price: ${price:,.2f}
-• Current Price: ${current_price:,.2f}
-• Est. Value: ${token_amount * price:,.2f}"""
+• Price: {formatter.format_price_with_symbol(price, token)}
+• Current Price: {formatter.format_price_with_symbol(current_price, token)}
+• Est. Value: {formatter.format_price_with_symbol(token_amount * price)}"""
             
             if stop_loss_price:
                 confirmation_text += f"""
-• 🛑 Stop Loss: ${stop_loss_price:,.2f}"""
+• 🛑 Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)}"""
             
             confirmation_text += f"""
 
@@ -315,15 +320,15 @@ This will {"place a limit sell order" if limit_price else "execute a market sell
 📊 <b>Position Details:</b>
 • Token: {token}
 • Position: {position_type}
-• Size: {contracts:.6f} contracts
-• Entry Price: ${entry_price:,.2f}
-• Current Price: ${current_price:,.2f}
-• {pnl_emoji} Unrealized P&L: ${unrealized_pnl:,.2f}
+• Size: {get_formatter(contracts)} contracts
+• Entry Price: {get_formatter(entry_price)}
+• Current Price: {get_formatter(current_price)}
+• {pnl_emoji} Unrealized P&L: {get_formatter(unrealized_pnl)}
 
 🎯 <b>Exit Order:</b>
 • Action: {exit_side.upper()} (Close {position_type})
-• Amount: {contracts:.6f} {token}
-• Est. Value: ~${exit_value:,.2f}
+• Amount: {get_formatter(contracts)} {token}
+• Est. Value: ~{get_formatter(exit_value)}
 • Order Type: Market Order
 
 ⚠️ <b>Are you sure you want to close this {position_type} position?</b>
@@ -377,18 +382,18 @@ This will {"place a limit sell order" if limit_price else "execute a market sell
                 await context.bot.send_message(chat_id=chat_id, text=(
                     f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n"
                     f"📊 Your {token} LONG position:\n"
-                    f"• Entry Price: ${entry_price:,.2f}\n"
-                    f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
-                    f"💡 Try a lower price like: /sl {token} {entry_price * 0.95:.0f}"
+                    f"• Entry Price: {get_formatter(entry_price)}\n"
+                    f"• Stop Price: {get_formatter(stop_price)} ❌\n\n"
+                    f"💡 Try a lower price like: /sl {token} {get_formatter(entry_price * 0.95)}\n"
                 ))
                 return
             elif position_type == "SHORT" and stop_price <= entry_price:
                 await context.bot.send_message(chat_id=chat_id, text=(
                     f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n"
                     f"📊 Your {token} SHORT position:\n"
-                    f"• Entry Price: ${entry_price:,.2f}\n"
-                    f"• Stop Price: ${stop_price:,.2f} ❌\n\n"
-                    f"💡 Try a higher price like: /sl {token} {entry_price * 1.05:.0f}"
+                    f"• Entry Price: {get_formatter(entry_price)}\n"
+                    f"• Stop Price: {get_formatter(stop_price)} ❌\n\n"
+                    f"💡 Try a higher price like: /sl {token} {get_formatter(entry_price * 1.05)}\n"
                 ))
                 return
             
@@ -412,20 +417,20 @@ This will {"place a limit sell order" if limit_price else "execute a market sell
 📊 <b>Position Details:</b>
 • Token: {token}
 • Position: {position_type}
-• Size: {contracts:.6f} contracts
-• Entry Price: ${entry_price:,.2f}
-• Current Price: ${current_price:,.2f}
+• Size: {get_formatter(contracts)} contracts
+• Entry Price: {get_formatter(entry_price)}
+• Current Price: {get_formatter(current_price)}
 
 🎯 <b>Stop Loss Order:</b>
-• Stop Price: ${stop_price:,.2f}
+• Stop Price: {get_formatter(stop_price)}
 • Action: {exit_side.upper()} (Close {position_type})
-• Amount: {contracts:.6f} {token}
+• Amount: {get_formatter(contracts)} {token}
 • Order Type: Limit Order
-• {pnl_emoji} Est. P&L: ${pnl_at_stop:,.2f}
+• {pnl_emoji} Est. P&L: {get_formatter(pnl_at_stop)}
 
 ⚠️ <b>Are you sure you want to set this stop loss?</b>
 
-This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your {position_type} position.
+This will place a limit {exit_side} order at {get_formatter(stop_price)} to protect your {position_type} position.
             """
             
             keyboard = [
@@ -478,18 +483,18 @@ This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your
                 await context.bot.send_message(chat_id=chat_id, text=(
                     f"⚠️ Take profit price should be ABOVE entry price for long positions\n\n"
                     f"📊 Your {token} LONG position:\n"
-                    f"• Entry Price: ${entry_price:,.2f}\n"
-                    f"• Take Profit: ${tp_price:,.2f} ❌\n\n"
-                    f"💡 Try a higher price like: /tp {token} {entry_price * 1.05:.0f}"
+                    f"• Entry Price: {get_formatter(entry_price)}\n"
+                    f"• Take Profit: {get_formatter(tp_price)} ❌\n\n"
+                    f"💡 Try a higher price like: /tp {token} {get_formatter(entry_price * 1.05)}\n"
                 ))
                 return
             elif position_type == "SHORT" and tp_price >= entry_price:
                 await context.bot.send_message(chat_id=chat_id, text=(
                     f"⚠️ Take profit price should be BELOW entry price for short positions\n\n"
                     f"📊 Your {token} SHORT position:\n"
-                    f"• Entry Price: ${entry_price:,.2f}\n"
-                    f"• Take Profit: ${tp_price:,.2f} ❌\n\n"
-                    f"💡 Try a lower price like: /tp {token} {entry_price * 0.95:.0f}"
+                    f"• Entry Price: {get_formatter(entry_price)}\n"
+                    f"• Take Profit: {get_formatter(tp_price)} ❌\n\n"
+                    f"💡 Try a lower price like: /tp {token} {get_formatter(entry_price * 0.95)}\n"
                 ))
                 return
             
@@ -513,20 +518,20 @@ This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your
 📊 <b>Position Details:</b>
 • Token: {token}
 • Position: {position_type}
-• Size: {contracts:.6f} contracts
-• Entry Price: ${entry_price:,.2f}
-• Current Price: ${current_price:,.2f}
+• Size: {get_formatter(contracts)} contracts
+• Entry Price: {get_formatter(entry_price)}
+• Current Price: {get_formatter(current_price)}
 
 🎯 <b>Take Profit Order:</b>
-• Take Profit Price: ${tp_price:,.2f}
+• Take Profit Price: {get_formatter(tp_price)}
 • Action: {exit_side.upper()} (Close {position_type})
-• Amount: {contracts:.6f} {token}
+• Amount: {get_formatter(contracts)} {token}
 • Order Type: Limit Order
-• {pnl_emoji} Est. P&L: ${pnl_at_tp:,.2f}
+• {pnl_emoji} Est. P&L: {get_formatter(pnl_at_tp)}
 
 ⚠️ <b>Are you sure you want to set this take profit?</b>
 
-This will place a limit {exit_side} order at ${tp_price:,.2f} to secure profits from your {position_type} position.
+This will place a limit {exit_side} order at {get_formatter(tp_price)} to secure profits from your {position_type} position.
             """
             
             keyboard = [

+ 4 - 0
src/trading/trading_engine.py

@@ -13,6 +13,7 @@ import uuid # For generating unique bot_order_ref_ids
 from src.config.config import Config
 from src.clients.hyperliquid_client import HyperliquidClient
 from src.trading.trading_stats import TradingStats
+from src.utils.price_formatter import set_global_trading_engine
 
 logger = logging.getLogger(__name__)
 
@@ -32,6 +33,9 @@ class TradingEngine:
         # Initialize stats (this will connect to/create the DB)
         self._initialize_stats()
         
+        # Initialize price formatter with this trading engine
+        set_global_trading_engine(self)
+        
     def _initialize_stats(self):
         """Initialize trading statistics."""
         try:

+ 1 - 0
src/utils/__init__.py

@@ -0,0 +1 @@
+# Utility modules 

+ 198 - 0
src/utils/price_formatter.py

@@ -0,0 +1,198 @@
+"""
+Price formatting utilities with exchange-specific precision handling.
+"""
+
+import logging
+import math
+from typing import Dict, Optional, Any
+from functools import lru_cache
+
+logger = logging.getLogger(__name__)
+
+
+class PriceFormatter:
+    """Handles price formatting with proper decimal precision from exchange data."""
+    
+    def __init__(self, trading_engine=None):
+        """
+        Initialize price formatter.
+        
+        Args:
+            trading_engine: TradingEngine instance for accessing exchange data
+        """
+        self.trading_engine = trading_engine
+        self._precision_cache: Dict[str, int] = {}
+        self._markets_cache: Optional[Dict[str, Any]] = None
+    
+    def _load_markets_data(self) -> Dict[str, Any]:
+        """Load markets data with caching."""
+        if self._markets_cache is None and self.trading_engine:
+            try:
+                markets = self.trading_engine.client.get_markets()
+                if markets:
+                    self._markets_cache = markets
+                    logger.info(f"📊 Loaded {len(markets)} markets for precision data")
+                else:
+                    logger.warning("⚠️ Could not load markets data from exchange")
+                    self._markets_cache = {}
+            except Exception as e:
+                logger.error(f"❌ Error loading markets data: {e}")
+                self._markets_cache = {}
+        
+        return self._markets_cache or {}
+    
+    def get_token_decimal_places(self, token: str) -> int:
+        """
+        Get the number of decimal places for a token price based on exchange precision.
+        
+        Args:
+            token: Token symbol (e.g., 'BTC', 'PEPE')
+            
+        Returns:
+            Number of decimal places for price formatting
+        """
+        # Check cache first
+        if token in self._precision_cache:
+            return self._precision_cache[token]
+        
+        # Default decimal places for fallback
+        default_decimals = 2
+        
+        try:
+            markets = self._load_markets_data()
+            if not markets:
+                logger.debug(f"No markets data available for {token}, using default {default_decimals} decimals")
+                return default_decimals
+            
+            # Try different symbol formats
+            possible_symbols = [
+                f"{token}/USDC:USDC",
+                f"{token}/USDC",
+                f"k{token}/USDC:USDC",  # For some tokens like kPEPE
+                f"H{token}/USDC",       # For some tokens like HPEPE
+            ]
+            
+            for symbol in possible_symbols:
+                if symbol in markets:
+                    market_info = markets[symbol]
+                    precision_info = market_info.get('precision', {})
+                    price_precision = precision_info.get('price')
+                    
+                    if price_precision and price_precision > 0:
+                        # Convert precision to decimal places
+                        # precision 0.01 -> 2 decimals, 0.001 -> 3 decimals, etc.
+                        decimal_places = int(-math.log10(price_precision))
+                        
+                        # Cache the result
+                        self._precision_cache[token] = decimal_places
+                        logger.debug(f"📊 {token} precision: {price_precision} -> {decimal_places} decimal places")
+                        return decimal_places
+            
+            # If no matching symbol found, use smart defaults based on token type
+            decimal_places = self._get_default_decimals_for_token(token)
+            self._precision_cache[token] = decimal_places
+            logger.debug(f"💡 {token} using default precision: {decimal_places} decimal places")
+            return decimal_places
+            
+        except Exception as e:
+            logger.error(f"❌ Error getting precision for {token}: {e}")
+            self._precision_cache[token] = default_decimals
+            return default_decimals
+    
+    def _get_default_decimals_for_token(self, token: str) -> int:
+        """Get smart default decimal places based on token characteristics."""
+        token = token.upper()
+        
+        # High-value tokens (usually need fewer decimals)
+        if token in ['BTC', 'ETH', 'BNB', 'SOL', 'ADA', 'DOT', 'AVAX', 'MATIC', 'LINK']:
+            return 2
+        
+        # Mid-range tokens
+        if token in ['DOGE', 'XRP', 'LTC', 'BCH', 'ETC', 'FIL', 'AAVE', 'UNI']:
+            return 4
+        
+        # Meme/micro-cap tokens (usually need more decimals)
+        if any(meme in token for meme in ['PEPE', 'SHIB', 'DOGE', 'FLOKI', 'BONK', 'WIF']):
+            return 6
+        
+        # Default for unknown tokens
+        return 4
+    
+    def format_price(self, price: float, token: str = None) -> str:
+        """
+        Format a price with appropriate decimal places.
+        
+        Args:
+            price: Price value to format
+            token: Token symbol for precision lookup (optional)
+            
+        Returns:
+            Formatted price string
+        """
+        try:
+            if token:
+                decimal_places = self.get_token_decimal_places(token)
+            else:
+                # Use smart default based on price magnitude
+                if price >= 1000:
+                    decimal_places = 2
+                elif price >= 1:
+                    decimal_places = 3
+                elif price >= 0.01:
+                    decimal_places = 4
+                elif price >= 0.0001:
+                    decimal_places = 6
+                else:
+                    decimal_places = 8
+            
+            # Format with proper decimal places and thousand separators
+            formatted = f"{price:,.{decimal_places}f}"
+            return formatted
+            
+        except Exception as e:
+            logger.error(f"❌ Error formatting price {price} for {token}: {e}")
+            return f"{price:,.2f}"  # Fallback to 2 decimals
+    
+    def format_price_with_symbol(self, price: float, token: str = None) -> str:
+        """
+        Format a price with currency symbol and appropriate decimal places.
+        
+        Args:
+            price: Price value to format
+            token: Token symbol for precision lookup (optional)
+            
+        Returns:
+            Formatted price string with $ symbol
+        """
+        formatted_price = self.format_price(price, token)
+        return f"${formatted_price}"
+    
+    def clear_cache(self):
+        """Clear the precision cache."""
+        self._precision_cache.clear()
+        self._markets_cache = None
+        logger.info("🗑️ Cleared price formatter cache")
+
+
+# Global formatter instance
+_global_formatter: Optional[PriceFormatter] = None
+
+def set_global_trading_engine(trading_engine):
+    """Set the trading engine for the global formatter."""
+    global _global_formatter
+    _global_formatter = PriceFormatter(trading_engine)
+
+def get_formatter() -> PriceFormatter:
+    """Get the global formatter instance."""
+    global _global_formatter
+    if _global_formatter is None:
+        _global_formatter = PriceFormatter()
+    return _global_formatter
+
+def format_price(price: float, token: str = None) -> str:
+    """Convenience function to format price using global formatter."""
+    return get_formatter().format_price(price, token)
+
+def format_price_with_symbol(price: float, token: str = None) -> str:
+    """Convenience function to format price with $ symbol using global formatter."""
+    return get_formatter().format_price_with_symbol(price, token)