266 lines
9.2 KiB
Python
Executable File
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 {}
|