BrainDock/src/bot/binance_client.py

266 lines
9.2 KiB
Python
Executable File

"""
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 {}