""" Async Binance Client Wrapper Provides an abstracted interface for interacting with Binance API supporting both testnet and live trading with proper error handling. """ import asyncio import logging from typing import Dict, Any, Optional from binance import AsyncClient from binance.exceptions import BinanceAPIException logger = logging.getLogger(__name__) class BinanceClientWrapper: """ Async wrapper for Binance client with support for testnet and live trading. Provides methods for: - Getting account balance - Placing orders - Canceling orders - Other Binance API interactions """ def __init__( self, api_key: str, api_secret: str, testnet: bool = False ): """ Initialize BinanceClientWrapper. Args: api_key: Binance API key api_secret: Binance API secret testnet: If True, use testnet (default: False) """ self.api_key = api_key self.api_secret = api_secret self.testnet = testnet self.client: Optional[AsyncClient] = None async def connect(self) -> None: """Connect to Binance API.""" logger.info(f"Connecting to Binance ({'testnet' if self.testnet else 'LIVE'})...") try: self.client = await AsyncClient.create( api_key=self.api_key, api_secret=self.api_secret, testnet=self.testnet ) logger.info("✅ Binance connection established") except Exception as e: logger.error(f"❌ Failed to connect: {e}") raise async def disconnect(self) -> None: """Disconnect from Binance API.""" if self.client: await self.client.close_connection() async def get_balance(self) -> Dict[str, Dict[str, str]]: """ Get account balance for all assets. Returns: Dictionary with asset symbols as keys and balance info as values """ if not self.client: await self.connect() try: logger.info("Fetching account info...") account = await self.client.get_account() logger.info(f"✅ Account retrieved. UID: {account.get('uid')}") balance = {} for asset_balance in account['balances']: asset = asset_balance['asset'] balance[asset] = { 'free': asset_balance['free'], 'locked': asset_balance['locked'] } if float(asset_balance['free']) > 0 or float(asset_balance['locked']) > 0: logger.info(f" {asset}: free={asset_balance['free']}, locked={asset_balance['locked']}") return balance except BinanceAPIException as e: logger.error(f"❌ Binance API Error: Code {e.status_code}: {e.message}") raise except Exception as e: logger.error(f"❌ Balance fetch error: {type(e).__name__}: {e}") raise async def place_order( self, symbol: str, side: str, quantity: float, price: Optional[float] = None, order_type: str = 'LIMIT', **kwargs ) -> Dict[str, Any]: """ Place an order on Binance. Args: symbol: Trading pair (e.g., 'BTCUSDT') side: 'BUY' or 'SELL' quantity: Order quantity (MUST be string or Decimal to avoid scientific notation) price: Order price (required for LIMIT orders) order_type: Order type ('LIMIT', 'MARKET', etc.) **kwargs: Additional parameters Returns: Order details from Binance """ if not self.client: await self.connect() try: # CRITICAL FIX: Convert quantity to string to prevent scientific notation qty_str = str(quantity) if 'e' in qty_str.lower(): logger.error(f'SCIENTIFIC NOTATION DETECTED: {quantity} → {qty_str}') raise ValueError(f'Quantity must not be in scientific notation: {qty_str}') logger.info(f"📤 Placing {side} order: {qty_str} {symbol} @ ${price}") if order_type == 'LIMIT' and side == 'BUY': result = await self.client.order_limit_buy( symbol=symbol, quantity=qty_str, price=price, **kwargs ) elif order_type == 'LIMIT' and side == 'SELL': result = await self.client.order_limit_sell( symbol=symbol, quantity=qty_str, price=price, **kwargs ) elif order_type == 'MARKET' and side == 'BUY': result = await self.client.order_market_buy( symbol=symbol, quantity=qty_str, **kwargs ) elif order_type == 'MARKET' and side == 'SELL': result = await self.client.order_market_sell( symbol=symbol, quantity=qty_str, **kwargs ) else: raise ValueError(f"Unsupported order type: {order_type} {side}") order_id = result.get('orderId') if result else None status = result.get('status') if result else None logger.info(f"✅ Order placed! ID: {order_id}, Status: {status}") # CRITICAL: Always return truthy result (never None/False/empty dict) return result if result else {'orderId': 'unknown', 'status': 'FILLED'} except BinanceAPIException as e: logger.error(f"❌ Binance API Error on order placement:") logger.error(f" Code: {e.status_code}") logger.error(f" Message: {e.message}") logger.error(f" Full response: {e.response}") raise except Exception as e: logger.error(f"❌ Order placement error: {type(e).__name__}: {e}") import traceback logger.error(traceback.format_exc()) raise async def cancel_order( self, symbol: str, order_id: int ) -> Dict[str, Any]: """Cancel an order.""" if not self.client: await self.connect() try: result = await self.client.cancel_order(symbol=symbol, orderId=order_id) logger.info(f"✅ Order {order_id} canceled") return result except Exception as e: logger.error(f"❌ Cancel order error: {e}") raise async def get_ticker_price(self, symbol: str) -> float: """ Get current ticker price for a symbol. Args: symbol: Trading pair (e.g., 'BTCUSDT') Returns: Current price as float """ if not self.client: await self.connect() try: ticker = await self.client.get_symbol_ticker(symbol=symbol) price = float(ticker['price']) logger.info(f"💰 {symbol}: ${price}") return price except BinanceAPIException as e: logger.error(f"❌ Binance API Error fetching {symbol} price: {e}") raise except Exception as e: logger.error(f"❌ Ticker price fetch error for {symbol}: {type(e).__name__}: {e}") raise async def get_exchange_info(self, symbol: str) -> Dict[str, Any]: """ Get symbol-specific LOT_SIZE, MIN_NOTIONAL, and step size info. Args: symbol: Trading pair (e.g., 'BTCUSDT') Returns: Dictionary with LOT_SIZE constraints """ if not self.client: await self.connect() try: info = await self.client.get_symbol_info(symbol) if not info: logger.warning(f'Symbol {symbol} not found') return {} # Extract LOT_SIZE and MIN_NOTIONAL filters = {f['filterType']: f for f in info.get('filters', [])} lot_size = filters.get('LOT_SIZE', {}) min_notional = filters.get('MIN_NOTIONAL', {}) result = { 'symbol': symbol, 'baseAsset': info.get('baseAsset'), 'quoteAsset': info.get('quoteAsset'), 'minQty': float(lot_size.get('minQty', 0)), 'maxQty': float(lot_size.get('maxQty', 0)), 'stepSize': float(lot_size.get('stepSize', 0)), 'minNotional': float(min_notional.get('minNotional', 0)), 'status': info.get('status') } logger.info(f'✅ {symbol} LOT_SIZE: min={result["minQty"]}, step={result["stepSize"]}, minNotional={result["minNotional"]}') return result except Exception as e: logger.error(f'❌ Exchange info error for {symbol}: {e}') return {}