/** * Binance Client Service * * CCXT wrapper for Binance operations. * Provides a unified interface for both Spot and Futures trading. * * @version 1.0.0 * @author Trading Platform Trading Platform */ import ccxt, { Ticker, OrderBook, OHLCV, Balance, Order, Trade } from 'ccxt'; import { createBinanceSpotClient, createBinanceFuturesClient, binanceConfig } from '../config'; import { logger } from '../utils/logger'; // ========================================== // Types // ========================================== export interface BinanceTicker { symbol: string; price: number; bid: number; ask: number; high24h: number; low24h: number; volume24h: number; change24h: number; timestamp: number; } export interface BinanceOrderBook { symbol: string; bids: [number, number][]; asks: [number, number][]; spread: number; spreadPercentage: number; timestamp: number; } export interface BinanceKline { timestamp: number; open: number; high: number; low: number; close: number; volume: number; } export interface BinanceAccountBalance { asset: string; free: number; locked: number; total: number; } export interface BinanceAccount { accountType: string; balances: BinanceAccountBalance[]; canTrade: boolean; canWithdraw: boolean; updateTime: number; } export interface BinanceOrder { id: string; symbol: string; side: string; type: string; price: number | null; amount: number; filled: number; remaining: number; status: string; createdAt: number; } export interface CreateOrderParams { symbol: string; side: 'buy' | 'sell'; type: 'market' | 'limit' | 'stop_loss' | 'take_profit'; amount: number; price?: number; stopPrice?: number; } export interface OrderResult { success: boolean; order?: BinanceOrder; error?: string; } // ========================================== // Binance Client Class // ========================================== export class BinanceClient { private spotClient: ccxt.binance; private futuresClient: ccxt.binance; private marketsLoaded: boolean = false; constructor() { this.spotClient = createBinanceSpotClient(); this.futuresClient = createBinanceFuturesClient(); } /** * Check if client is properly configured */ isConfigured(): boolean { return binanceConfig.apiKey !== '' && binanceConfig.apiSecret !== ''; } /** * Test connectivity to Binance */ async isConnected(): Promise { try { await this.spotClient.fetchTime(); return true; } catch { return false; } } /** * Load markets if not already loaded */ private async ensureMarketsLoaded(): Promise { if (!this.marketsLoaded) { await this.spotClient.loadMarkets(); this.marketsLoaded = true; } } // ========================================== // Market Data Methods // ========================================== /** * Get ticker for a symbol */ async getTicker(symbol: string): Promise { try { await this.ensureMarketsLoaded(); const ticker: Ticker = await this.spotClient.fetchTicker(symbol); return { symbol: ticker.symbol, price: ticker.last ?? 0, bid: ticker.bid ?? 0, ask: ticker.ask ?? 0, high24h: ticker.high ?? 0, low24h: ticker.low ?? 0, volume24h: ticker.baseVolume ?? 0, change24h: ticker.percentage ?? 0, timestamp: ticker.timestamp ?? Date.now(), }; } catch (error) { logger.error(`Failed to get ticker for ${symbol}`, { error }); throw error; } } /** * Get order book for a symbol */ async getOrderBook(symbol: string, limit: number = 20): Promise { try { await this.ensureMarketsLoaded(); const orderbook: OrderBook = await this.spotClient.fetchOrderBook(symbol, limit); const topBid = orderbook.bids[0]?.[0] ?? 0; const topAsk = orderbook.asks[0]?.[0] ?? 0; const spread = topAsk - topBid; const spreadPercentage = topBid > 0 ? (spread / topBid) * 100 : 0; return { symbol, bids: orderbook.bids.slice(0, limit) as [number, number][], asks: orderbook.asks.slice(0, limit) as [number, number][], spread, spreadPercentage, timestamp: orderbook.timestamp ?? Date.now(), }; } catch (error) { logger.error(`Failed to get order book for ${symbol}`, { error }); throw error; } } /** * Get OHLCV (klines/candles) for a symbol */ async getKlines( symbol: string, interval: string = '5m', limit: number = 100 ): Promise { try { await this.ensureMarketsLoaded(); const ohlcv: OHLCV[] = await this.spotClient.fetchOHLCV(symbol, interval, undefined, limit); return ohlcv.map((candle) => ({ timestamp: candle[0] as number, open: candle[1] as number, high: candle[2] as number, low: candle[3] as number, close: candle[4] as number, volume: candle[5] as number, })); } catch (error) { logger.error(`Failed to get klines for ${symbol}`, { error }); throw error; } } // ========================================== // Account Methods // ========================================== /** * Get account balance */ async getAccount(): Promise { try { if (!this.isConfigured()) { throw new Error('Binance API keys not configured'); } const balance: Balance = await this.spotClient.fetchBalance(); // Filter non-zero balances const balances: BinanceAccountBalance[] = Object.entries(balance.total) .filter(([_, amount]) => (amount as number) > 0) .map(([asset, total]) => ({ asset, free: (balance.free[asset] as number) ?? 0, locked: (balance.used[asset] as number) ?? 0, total: total as number, })); return { accountType: 'SPOT', balances, canTrade: true, canWithdraw: true, updateTime: Date.now(), }; } catch (error) { logger.error('Failed to get account info', { error }); throw error; } } /** * Get open orders */ async getOpenOrders(symbol?: string): Promise { try { if (!this.isConfigured()) { throw new Error('Binance API keys not configured'); } const orders: Order[] = await this.spotClient.fetchOpenOrders(symbol); return orders.map((order) => ({ id: order.id, symbol: order.symbol, side: order.side, type: order.type, price: order.price, amount: order.amount, filled: order.filled, remaining: order.remaining, status: order.status, createdAt: order.timestamp ?? Date.now(), })); } catch (error) { logger.error('Failed to get open orders', { error }); throw error; } } /** * Get trade history */ async getTradeHistory(symbol: string, limit: number = 50): Promise { try { if (!this.isConfigured()) { throw new Error('Binance API keys not configured'); } return await this.spotClient.fetchMyTrades(symbol, undefined, limit); } catch (error) { logger.error(`Failed to get trade history for ${symbol}`, { error }); throw error; } } // ========================================== // Order Methods // ========================================== /** * Create a new order */ async createOrder(params: CreateOrderParams): Promise { try { if (!this.isConfigured()) { throw new Error('Binance API keys not configured'); } await this.ensureMarketsLoaded(); let order: Order; switch (params.type) { case 'market': order = await this.spotClient.createMarketOrder( params.symbol, params.side, params.amount ); break; case 'limit': if (!params.price) { return { success: false, error: 'Price is required for limit orders' }; } order = await this.spotClient.createLimitOrder( params.symbol, params.side, params.amount, params.price ); break; case 'stop_loss': if (!params.stopPrice) { return { success: false, error: 'Stop price is required for stop loss orders' }; } order = await this.spotClient.createOrder( params.symbol, 'stop_loss', params.side, params.amount, undefined, { stopPrice: params.stopPrice } ); break; case 'take_profit': if (!params.stopPrice) { return { success: false, error: 'Stop price is required for take profit orders' }; } order = await this.spotClient.createOrder( params.symbol, 'take_profit', params.side, params.amount, undefined, { stopPrice: params.stopPrice } ); break; default: return { success: false, error: `Unsupported order type: ${params.type}` }; } return { success: true, order: { id: order.id, symbol: order.symbol, side: order.side, type: order.type, price: order.price ?? order.average ?? null, amount: order.amount, filled: order.filled, remaining: order.remaining, status: order.status, createdAt: order.timestamp ?? Date.now(), }, }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; logger.error('Failed to create order', { error, params }); return { success: false, error: message }; } } /** * Cancel an order */ async cancelOrder(orderId: string, symbol: string): Promise { try { if (!this.isConfigured()) { throw new Error('Binance API keys not configured'); } const result = await this.spotClient.cancelOrder(orderId, symbol); return { success: true, order: { id: result.id, symbol: result.symbol, side: result.side, type: result.type, price: result.price, amount: result.amount, filled: result.filled, remaining: result.remaining, status: 'CANCELLED', createdAt: result.timestamp ?? Date.now(), }, }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; logger.error('Failed to cancel order', { error, orderId, symbol }); return { success: false, error: message }; } } /** * Cancel all orders for a symbol */ async cancelAllOrders(symbol: string): Promise<{ success: boolean; cancelledCount: number; error?: string }> { try { if (!this.isConfigured()) { throw new Error('Binance API keys not configured'); } const result = await this.spotClient.cancelAllOrders(symbol); return { success: true, cancelledCount: Array.isArray(result) ? result.length : 0, }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; logger.error('Failed to cancel all orders', { error, symbol }); return { success: false, cancelledCount: 0, error: message }; } } /** * Get current price for a symbol (helper method) */ async getCurrentPrice(symbol: string): Promise { const ticker = await this.getTicker(symbol); return ticker.price; } } // ========================================== // Singleton Instance // ========================================== let clientInstance: BinanceClient | null = null; export function getBinanceClient(): BinanceClient { if (!clientInstance) { clientInstance = new BinanceClient(); } return clientInstance; } export function resetBinanceClient(): void { clientInstance = null; }