UNPKG

behemoth-cli

Version:

🌍 BEHEMOTH CLIv3.760.4 - Level 50+ POST-SINGULARITY Intelligence Trading AI

1,368 lines (1,192 loc) 341 kB
#!/usr/bin/env node // /** * 🌌 BEHEMOTH Cosmic Trading DXT - MCP Server * Ultra-high performance crypto trading with real exchange APIs * Production-ready Desktop Extension implementation */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import https from 'https'; import { EventEmitter } from 'events'; import crypto from 'crypto'; // Enhanced logging system class Logger { constructor(level = 'info') { this.level = level; this.levels = { error: 0, warn: 1, info: 2, debug: 3 }; } log(level, message, data = null) { if (this.levels[level] <= this.levels[this.level]) { const timestamp = new Date().toISOString(); const logData = data ? ` | ${JSON.stringify(data)}` : ''; } } error(message, data) { this.log('error', message, data); } warn(message, data) { this.log('warn', message, data); } info(message, data) { this.log('info', message, data); } debug(message, data) { this.log('debug', message, data); } } // Initialize logger - only show errors and warnings for clean startup const logger = new Logger(process.env.LOG_LEVEL || 'error'); // Exchange API classes with defensive programming class ExchangeAPI { constructor(name, baseURL) { this.name = name; this.baseURL = baseURL; this.requestCount = 0; this.errorCount = 0; } async makeRequest(endpoint, params = {}, options = {}) { // NOTE: This function makes live, real-time API requests. No data caching is performed. const { timeout = 10000, method = 'GET', signed = false, body = null, baseURL = this.baseURL } = options; this.requestCount++; const queryString = new URLSearchParams(params).toString(); const url = `${baseURL}${endpoint}${queryString ? '?' + queryString : ''}`; const requestOptions = { method: method, timeout: timeout, headers: { 'Content-Type': 'application/json' } }; if (signed) { if (!this.apiKey || !this.apiSecret) { throw new Error(`${this.name} API credentials not configured.`); } this._signRequest(requestOptions, endpoint, queryString, body); } logger.debug(`Making ${this.name} API request`, { url, method, signed }); return new Promise((resolve, reject) => { const req = https.request(url, requestOptions, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { try { // Enhanced validation for Bitget HTML response issues if (this.name === 'bitget' && data.trim().startsWith('<html')) { this.errorCount++; logger.error(`${this.name} returned HTML instead of JSON`, { endpoint, statusCode: res.statusCode, contentType: res.headers['content-type'], dataPreview: data.substring(0, 200) }); reject(new Error(`${this.name} API returned HTML page instead of JSON for endpoint ${endpoint}. This usually indicates an API service issue or incorrect endpoint.`)); return; } // Enhanced validation for empty or malformed responses if (!data || data.trim() === '') { this.errorCount++; logger.error(`${this.name} returned empty response`, { endpoint, statusCode: res.statusCode }); reject(new Error(`${this.name} API returned empty response for endpoint ${endpoint}`)); return; } const response = JSON.parse(data); if (res.statusCode >= 400) { this.errorCount++; logger.error(`${this.name} API Error`, { status: res.statusCode, response, endpoint }); reject(new Error(`${this.name} API error (${res.statusCode}) for ${endpoint}: ${response.retMsg || response.msg || JSON.stringify(response)}`)); } else { logger.debug(`${this.name} API response received`, { status: res.statusCode, endpoint }); resolve({ data: response, statusCode: res.statusCode }); } } catch (e) { this.errorCount++; // Enhanced error logging for JSON parse failures const isLikelyHTML = data.trim().startsWith('<') || data.includes('<html') || data.includes('<!DOCTYPE'); const responseType = isLikelyHTML ? 'HTML' : 'malformed JSON'; logger.error(`${this.name} JSON parse error - received ${responseType}`, { endpoint, error: e.message, statusCode: res.statusCode, contentType: res.headers['content-type'], dataLength: data.length, dataPreview: data.substring(0, 500) }); if (this.name === 'bitget') { reject(new Error(`Bitget API returned ${responseType} instead of JSON for endpoint ${endpoint}. Status: ${res.statusCode}. This may indicate API service issues or maintenance.`)); } else { reject(new Error(`Invalid JSON response from ${this.name}: ${e.message}`)); } } }); }); req.on('error', (error) => { this.errorCount++; logger.error(`${this.name} request error`, { error: error.message, url }); reject(new Error(`${this.name} API error: ${error.message}`)); }); req.on('timeout', () => { this.errorCount++; req.destroy(); reject(new Error(`Request timeout after ${timeout}ms`)); }); if (body) { req.write(JSON.stringify(body)); } req.end(); }); } getStats() { return { exchange: this.name, requests: this.requestCount, errors: this.errorCount, success_rate: this.requestCount > 0 ? ((this.requestCount - this.errorCount) / this.requestCount * 100).toFixed(2) + '%' : '0%' }; } } class BinanceAPI extends ExchangeAPI { constructor() { super('binance', 'https://api.binance.com'); this.futuresBaseURL = 'https://fapi.binance.com'; } async getFuturesTicker(symbol = 'BTCUSDT') { try { const response = await this.makeRequest('/fapi/v1/ticker/24hr', { symbol }, { baseURL: this.futuresBaseURL }); if (!response.data) { throw new Error('Invalid futures ticker data structure'); } const ticker = Array.isArray(response.data) ? response.data.find(t => t.symbol === symbol) : response.data; if (!ticker) { throw new Error(`Ticker for ${symbol} not found in response.`); } return { exchange: 'binance', type: 'futures', symbol: ticker.symbol, price: parseFloat(ticker.lastPrice), change24h: parseFloat(ticker.priceChangePercent), volume24h: parseFloat(ticker.volume), quoteVolume24h: parseFloat(ticker.quoteVolume), high24h: parseFloat(ticker.highPrice), low24h: parseFloat(ticker.lowPrice), openTime: ticker.openTime, closeTime: ticker.closeTime, timestamp: Date.now(), real_data: true }; } catch (error) { logger.error('Binance futures ticker error', { symbol, error: error.message }); throw new Error(`Binance futures ticker error for ${symbol}: ${error.message}`); } } async getSpotTicker(symbol = 'BTCUSDT') { try { const response = await this.makeRequest('/api/v3/ticker/24hr', { symbol }); if (!response.data || !response.data.lastPrice) { throw new Error('Invalid ticker data structure'); } return { exchange: 'binance', symbol, price: parseFloat(response.data.lastPrice), change24h: parseFloat(response.data.priceChangePercent), volume24h: parseFloat(response.data.volume), high24h: parseFloat(response.data.highPrice), low24h: parseFloat(response.data.lowPrice), timestamp: Date.now(), real_data: true }; } catch (error) { logger.error('Binance spot ticker error', { symbol, error: error.message }); throw new Error(`Binance ticker error for ${symbol}: ${error.message}`); } } async getKlines(symbol = 'BTCUSDT', interval = '1h', limit = 100) { try { const response = await this.makeRequest('/api/v3/klines', { symbol, interval, limit }); if (!Array.isArray(response.data) || response.data.length === 0) { throw new Error('Invalid klines data structure'); } return { exchange: 'binance', symbol, interval, data: response.data.map(candle => ({ timestamp: candle[0], open: parseFloat(candle[1]), high: parseFloat(candle[2]), low: parseFloat(candle[3]), close: parseFloat(candle[4]), volume: parseFloat(candle[5]) })), real_data: true }; } catch (error) { logger.error('Binance klines error', { symbol, interval, error: error.message }); throw new Error(`Binance klines error for ${symbol}: ${error.message}`); } } async getOrderbook(symbol = 'BTCUSDT', limit = 100) { try { const response = await this.makeRequest('/api/v3/depth', { symbol, limit }); if (!response.data) { throw new Error('Invalid orderbook data structure'); } // Handle different response formats const orderbook = response.data.result || response.data; if (!orderbook.bids || !orderbook.asks) { throw new Error('Missing bids or asks data'); } return { exchange: 'binance', symbol, bids: orderbook.bids.map(([price, size]) => [parseFloat(price), parseFloat(size)]), asks: orderbook.asks.map(([price, size]) => [parseFloat(price), parseFloat(size)]), timestamp: Date.now(), real_data: true }; } catch (error) { logger.error('Binance orderbook error', { symbol, error: error.message }); throw new Error(`Binance orderbook error for ${symbol}: ${error.message}`); } } async getFuturesOrderbook(symbol = 'BTCUSDT', limit = 100) { try { const response = await this.makeRequest('/fapi/v1/depth', { symbol, limit }, { baseURL: this.futuresBaseURL }); if (!response.data || !response.data.bids || !response.data.asks) { throw new Error('Invalid futures orderbook data structure'); } return { exchange: 'binance', type: 'futures', symbol, bids: response.data.bids.map(([price, size]) => [parseFloat(price), parseFloat(size)]), asks: response.data.asks.map(([price, size]) => [parseFloat(price), parseFloat(size)]), timestamp: response.data.T || Date.now(), real_data: true }; } catch (error) { logger.error('Binance futures orderbook error', { symbol, error: error.message }); throw new Error(`Binance futures orderbook error for ${symbol}: ${error.message}`); } } async getFuturesTrades(symbol = 'BTCUSDT', limit = 100) { try { const response = await this.makeRequest('/fapi/v1/trades', { symbol, limit }, { baseURL: this.futuresBaseURL }); if (!Array.isArray(response.data)) { throw new Error('Invalid futures trades data structure'); } return { exchange: 'binance', type: 'futures', symbol, trades: response.data.map(trade => ({ id: trade.id, price: parseFloat(trade.price), size: parseFloat(trade.qty), quote_size: parseFloat(trade.quoteQty), side: trade.isBuyerMaker ? 'SELL' : 'BUY', // If buyer is the maker, it was a sell order that filled it. time: trade.time })), real_data: true }; } catch (error) { logger.error('Binance futures trades error', { symbol, error: error.message }); throw new Error(`Binance futures trades error for ${symbol}: ${error.message}`); } } async getFuturesFundingRate(symbol = 'BTCUSDT', limit = 1) { try { const params = { symbol, limit }; const response = await this.makeRequest('/fapi/v1/fundingRate', params, { baseURL: this.futuresBaseURL }); if (!Array.isArray(response.data)) { throw new Error('Invalid futures funding rate data structure'); } return { exchange: 'binance', type: 'futures', symbol, history: response.data.map(rate => ({ symbol: rate.symbol, rate: parseFloat(rate.fundingRate), timestamp: rate.fundingTime })), real_data: true }; } catch (error) { logger.error('Binance futures funding rate error', { symbol, error: error.message }); throw new Error(`Binance futures funding rate error for ${symbol}: ${error.message}`); } } async getGlobalLongShortRatio(symbol = 'BTCUSDT', period = '1h', limit = 30) { try { const params = { symbol, period, limit }; // This endpoint is under /futures/data, not /fapi/v1 const response = await this.makeRequest('/futures/data/globalLongShortAccountRatio', params, { baseURL: this.futuresBaseURL }); if (!Array.isArray(response.data)) { throw new Error('Invalid long/short ratio data structure'); } return { exchange: 'binance', type: 'futures_sentiment', symbol, period, history: response.data.map(item => ({ long_short_ratio: parseFloat(item.longShortRatio), long_account_ratio: parseFloat(item.longAccount), short_account_ratio: parseFloat(item.shortAccount), timestamp: parseInt(item.timestamp, 10) })), real_data: true }; } catch (error) { logger.error('Binance long/short ratio error', { symbol, error: error.message }); throw new Error(`Binance long/short ratio error for ${symbol}: ${error.message}`); } } } class BybitAPI extends ExchangeAPI { constructor() { const useTestnet = process.env.BYBIT_USE_TESTNET === 'true'; super('bybit', useTestnet ? 'https://api-testnet.bybit.com' : 'https://api.bybit.com'); this.apiKey = process.env.BYBIT_API_KEY; this.apiSecret = process.env.BYBIT_API_SECRET; } _signRequest(options, endpoint, queryString, body) { const timestamp = Date.now().toString(); const recvWindow = '5000'; // Proper Bybit signature: timestamp + apikey + recv_window + querystring + body let params = ''; if (options.method === 'POST' && body) { params = JSON.stringify(body); } else if (queryString) { params = queryString; } const signaturePayload = timestamp + this.apiKey + recvWindow + params; const signature = crypto.createHmac('sha256', this.apiSecret).update(signaturePayload).digest('hex'); options.headers['X-BAPI-API-KEY'] = this.apiKey; options.headers['X-BAPI-SIGN'] = signature; options.headers['X-BAPI-SIGN-TYPE'] = '2'; options.headers['X-BAPI-TIMESTAMP'] = timestamp; options.headers['X-BAPI-RECV-WINDOW'] = '5000'; } async getSpotTicker(symbol = 'BTCUSDT') { try { const response = await this.makeRequest('/v5/market/tickers', { category: 'spot', symbol }); if (!response.data || !response.data.result || !response.data.result.list || response.data.result.list.length === 0) { throw new Error('Invalid ticker data structure'); } const ticker = response.data.result.list[0]; return { exchange: 'bybit', symbol, price: parseFloat(ticker.lastPrice), change24h: parseFloat(ticker.price24hPcnt) * 100, volume24h: parseFloat(ticker.volume24h), high24h: parseFloat(ticker.highPrice24h), low24h: parseFloat(ticker.lowPrice24h), timestamp: Date.now(), real_data: true }; } catch (error) { logger.error('Bybit spot ticker error', { symbol, error: error.message }); throw new Error(`Bybit ticker error for ${symbol}: ${error.message}`); } } async getDerivativesTicker(symbol = 'BTCUSDT') { try { const response = await this.makeRequest('/v5/market/tickers', { category: 'linear', symbol }); if (!response.data || !response.data.result || !response.data.result.list || response.data.result.list.length === 0) { throw new Error('Invalid derivatives ticker data structure'); } const ticker = response.data.result.list[0]; return { exchange: 'bybit', type: 'derivatives', symbol, price: parseFloat(ticker.lastPrice), mark_price: parseFloat(ticker.markPrice), index_price: parseFloat(ticker.indexPrice), funding_rate: parseFloat(ticker.fundingRate), open_interest: parseFloat(ticker.openInterestValue), change24h: parseFloat(ticker.price24hPcnt) * 100, volume24h: parseFloat(ticker.volume24h), turnover24h: parseFloat(ticker.turnover24h), high24h: parseFloat(ticker.highPrice24h), low24h: parseFloat(ticker.lowPrice24h), timestamp: Date.now(), real_data: true }; } catch (error) { logger.error('Bybit derivatives ticker error', { symbol, error: error.message }); throw new Error(`Bybit derivatives ticker error for ${symbol}: ${error.message}`); } } async getWalletBalance() { const response = await this.makeRequest('/v5/account/wallet-balance', { accountType: 'UNIFIED' }, { signed: true }); if (response.data.retCode !== 0) { throw new Error(`Bybit balance error: ${response.data.retMsg}`); } const usdtBalance = response.data.result.list[0].coin.find(c => c.coin === 'USDT'); return { exchange: 'bybit', verified: true, usdt_futures_balance: parseFloat(usdtBalance?.walletBalance || 0) }; } async placeOrder(symbol, side, orderType, qty, price = null) { const body = { category: 'linear', // For USDT-M Futures symbol: symbol, side: side, // 'Buy' or 'Sell' orderType: orderType, // 'Market' or 'Limit' qty: qty.toString(), }; if (orderType.toLowerCase() === 'limit' && price !== null && price !== undefined) { body.price = price.toString(); } const response = await this.makeRequest('/v5/order/create', {}, { method: 'POST', signed: true, body }); if (response.data.retCode !== 0) { throw new Error(`Bybit order placement error: ${response.data.retMsg}`); } return { exchange: 'bybit', status: 'order_placed', orderId: response.data.result.orderId, symbol: symbol, side: side, qty: qty }; } async getOpenOrders(symbol = null) { const params = { category: 'linear' }; if (symbol) { params.symbol = symbol; } const response = await this.makeRequest('/v5/order/realtime', params, { signed: true }); if (response.data.retCode !== 0) { throw new Error(`Bybit get open orders error: ${response.data.retMsg}`); } return response.data.result.list; } async getPositions(symbol = null) { const params = { category: 'linear' }; if (symbol) { params.symbol = symbol; } const response = await this.makeRequest('/v5/position/list', params, { signed: true }); if (response.data.retCode !== 0) { throw new Error(`Bybit get positions error: ${response.data.retMsg}`); } return response.data.result.list; } async setTradingStop(symbol, takeProfit = null, stopLoss = null, tpTriggerBy = 'LastPrice', slTriggerBy = 'LastPrice', tpslMode = 'Full', positionIdx = 0) { const body = { category: 'linear', symbol: symbol, positionIdx: positionIdx, tpslMode: tpslMode, // 'Full' or 'Partial' }; if (takeProfit !== null && takeProfit !== undefined) { body.takeProfit = takeProfit.toString(); body.tpTriggerBy = tpTriggerBy; // 'LastPrice', 'IndexPrice', 'MarkPrice' } if (stopLoss !== null && stopLoss !== undefined) { body.stopLoss = stopLoss.toString(); body.slTriggerBy = slTriggerBy; // 'LastPrice', 'IndexPrice', 'MarkPrice' } const response = await this.makeRequest('/v5/position/trading-stop', {}, { method: 'POST', signed: true, body }); if (response.data.retCode !== 0) { throw new Error(`Bybit set trading stop error: ${response.data.retMsg}`); } return { exchange: 'bybit', status: 'trading_stop_set', symbol: symbol, takeProfit: takeProfit, stopLoss: stopLoss, result: response.data.result }; } async setTrailingStop(symbol, trailingStop, activePrice = null, positionIdx = 0) { const body = { category: 'linear', symbol: symbol, positionIdx: positionIdx, trailingStop: trailingStop.toString(), // Percentage like "0.01" for 1% }; if (activePrice !== null && activePrice !== undefined) { body.activePrice = activePrice.toString(); } const response = await this.makeRequest('/v5/position/trading-stop', {}, { method: 'POST', signed: true, body }); if (response.data.retCode !== 0) { throw new Error(`Bybit set trailing stop error: ${response.data.retMsg}`); } return { exchange: 'bybit', status: 'trailing_stop_set', symbol: symbol, trailingStop: trailingStop, activePrice: activePrice, result: response.data.result }; } async getOrderBook(symbol = 'BTCUSDT', category = 'linear', limit = 25) { try { const params = { category, symbol, limit }; const response = await this.makeRequest('/v5/market/orderbook', params); if (response.data.retCode !== 0 || !response.data.result) { throw new Error(`Invalid order book data structure: ${response.data.retMsg}`); } const orderbook = response.data.result; return { exchange: 'bybit', symbol, bids: orderbook.b.map(([price, size]) => [parseFloat(price), parseFloat(size)]), asks: orderbook.a.map(([price, size]) => [parseFloat(price), parseFloat(size)]), timestamp: orderbook.ts, real_data: true }; } catch (error) { logger.error('Bybit order book error', { symbol, category, error: error.message }); throw new Error(`Bybit order book error for ${symbol}: ${error.message}`); } } async getTrades(symbol = 'BTCUSDT', category = 'linear', limit = 50) { try { const params = { category, symbol, limit }; const response = await this.makeRequest('/v5/market/recent-trade', params); if (response.data.retCode !== 0 || !response.data.result || !response.data.result.list) { throw new Error(`Invalid trades data structure: ${response.data.retMsg}`); } const trades = response.data.result.list; return { exchange: 'bybit', symbol, trades: trades.map(trade => ({ id: trade.execId, price: parseFloat(trade.price), size: parseFloat(trade.size), side: trade.side, time: parseInt(trade.time, 10) })), real_data: true }; } catch (error) { logger.error('Bybit trades error', { symbol, category, error: error.message }); throw new Error(`Bybit trades error for ${symbol}: ${error.message}`); } } async getFundingRateHistory(symbol = 'BTCUSDT', category = 'linear', limit = 1) { try { const params = { category, symbol, limit }; const response = await this.makeRequest('/v5/market/funding/history', params); if (response.data.retCode !== 0 || !response.data.result || !response.data.result.list) { throw new Error(`Invalid funding rate data structure: ${response.data.retMsg}`); } const history = response.data.result.list; return { exchange: 'bybit', symbol, history: history.map(item => ({ rate: parseFloat(item.fundingRate), timestamp: parseInt(item.fundingRateTimestamp, 10) })), real_data: true }; } catch (error) { logger.error('Bybit funding rate error', { symbol, category, error: error.message }); throw new Error(`Bybit funding rate error for ${symbol}: ${error.message}`); } } async getOpenInterest(symbol = 'BTCUSDT', category = 'linear', interval = '1h', limit = 50) { try { const params = { category, symbol, interval, limit }; const response = await this.makeRequest('/v5/market/open-interest', params); if (response.data.retCode !== 0 || !response.data.result || !response.data.result.list) { throw new Error(`Invalid open interest data structure: ${response.data.retMsg}`); } const history = response.data.result.list; return { exchange: 'bybit', symbol, interval, history: history.map(item => ({ value: parseFloat(item.openInterest), timestamp: parseInt(item.timestamp, 10) })).reverse(), // Bybit returns newest first, reverse to get chronological order real_data: true }; } catch (error) { logger.error('Bybit open interest error', { symbol, category, error: error.message }); throw new Error(`Bybit open interest error for ${symbol}: ${error.message}`); } } } class BitgetAPI extends ExchangeAPI { constructor() { super('bitget', 'https://api.bitget.com'); this.apiKey = process.env.BITGET_API_KEY; this.apiSecret = process.env.BITGET_API_SECRET; this.passphrase = process.env.BITGET_API_PASSPHRASE; } _signRequest(options, endpoint, queryString, body) { if (!this.apiKey || !this.apiSecret || !this.passphrase) { throw new Error('Bitget API credentials not configured'); } const timestamp = Date.now().toString(); const method = options.method.toUpperCase(); const bodyStr = body ? JSON.stringify(body) : ''; // Bitget signature format: timestamp + method + requestPath + queryString + body const queryPart = queryString ? '?' + queryString : ''; const prehash = timestamp + method + endpoint + queryPart + bodyStr; const signature = crypto.createHmac('sha256', this.apiSecret).update(prehash).digest('base64'); // Set required headers according to Bitget API v2 spec options.headers['ACCESS-KEY'] = this.apiKey; options.headers['ACCESS-SIGN'] = signature; options.headers['ACCESS-TIMESTAMP'] = timestamp; options.headers['ACCESS-PASSPHRASE'] = this.passphrase; options.headers['locale'] = 'en-US'; options.headers['Content-Type'] = 'application/json'; } async makeRequest(endpoint, params = {}, options = {}) { try { const response = await super.makeRequest(endpoint, params, options); // Additional Bitget-specific response validation if (response.data && response.data.code && response.data.code !== '00000') { throw new Error(`Bitget API error: ${response.data.msg || response.data.code}`); } // Enhanced validation for Bitget-specific response issues if (!response.data) { logger.error('Bitget returned empty data object', { endpoint, statusCode: response.statusCode }); throw new Error(`Bitget API returned empty data for ${endpoint}`); } // Validate response structure for common Bitget issues if (typeof response.data === 'object') { // Additional checks for Bitget API success indicators if (response.data.code === '00000' || response.data.data !== undefined) { return response; } else if (response.data.code) { throw new Error(`Bitget API error code ${response.data.code}: ${response.data.msg || 'Unknown error'}`); } return response; } else { logger.warn('Bitget response validation warning', { endpoint, responseType: typeof response.data, hasData: !!response.data, statusCode: response.statusCode }); return response; } } catch (error) { // Enhanced Bitget error handling with detailed logging if (error.message.includes('returned HTML') || error.message.includes('Invalid JSON')) { logger.error('Bitget API response format issue', { endpoint, error: error.message, exchange: 'bitget', params: JSON.stringify(params) }); // Provide more specific error message for HTML responses if (error.message.includes('HTML')) { throw new Error(`Bitget API endpoint ${endpoint} is returning HTML instead of JSON. This may indicate the endpoint is incorrect, under maintenance, or experiencing issues. Original error: ${error.message}`); } else { throw new Error(`Bitget API returned invalid JSON for ${endpoint}. This may indicate a temporary service issue. Original error: ${error.message}`); } } // Handle network or other connection errors if (error.message.includes('timeout') || error.message.includes('ECONNREFUSED') || error.message.includes('ENOTFOUND')) { logger.error('Bitget API connection issue', { endpoint, error: error.message, exchange: 'bitget' }); throw new Error(`Bitget API connection failed for ${endpoint}: ${error.message}. Please check network connectivity and API status.`); } throw error; } } async getSpotTicker(symbol = 'BTCUSDT') { try { // Convert to Bitget format const bitgetSymbol = symbol.includes('_') ? symbol : symbol + '_SPBL'; const response = await this.makeRequest('/api/spot/v1/market/ticker', { symbol: bitgetSymbol }); if (!response.data || !response.data.data || !response.data.data.close) { throw new Error('Invalid ticker data structure'); } const data = response.data.data; return { exchange: 'bitget', symbol, price: parseFloat(data.close), change24h: parseFloat(data.change), volume24h: parseFloat(data.baseVol), high24h: parseFloat(data.high), low24h: parseFloat(data.low), timestamp: Date.now(), real_data: true }; } catch (error) { logger.error('Bitget spot ticker error', { symbol, error: error.message }); throw new Error(`Bitget ticker error for ${symbol}: ${error.message}`); } } async getFuturesTicker(symbol = 'BTCUSDT') { try { const bitgetSymbol = `${symbol}_UMCBL`; const response = await this.makeRequest('/api/v2/futures/market/ticker', { symbol: bitgetSymbol, productType: 'USDT-FUTURES' }); if (response.data.code !== '00000' || !response.data.data || response.data.data.length === 0) { throw new Error(`Invalid futures ticker data structure: ${response.data.msg}`); } const ticker = response.data.data[0]; return { exchange: 'bitget', type: 'futures', symbol, price: parseFloat(ticker.lastPrice), mark_price: parseFloat(ticker.markPrice), index_price: parseFloat(ticker.indexPrice), funding_rate: parseFloat(ticker.fundingRate), open_interest: parseFloat(ticker.openInterest), change24h: parseFloat(ticker.priceChangePercent), volume24h: parseFloat(ticker.volume24H), turnover24h: parseFloat(ticker.amount24H), high24h: parseFloat(ticker.high24H), low24h: parseFloat(ticker.low24H), timestamp: Date.now(), real_data: true }; } catch (error) { logger.error('Bitget futures ticker error', { symbol, error: error.message }); throw new Error(`Bitget futures ticker error for ${symbol}: ${error.message}`); } } async getWalletBalance() { // Use v2 mix account balance endpoint for futures const response = await this.makeRequest('/api/v2/mix/account/accounts', { productType: 'USDT-FUTURES' }, { signed: true }); if (response.data.code !== '00000') { throw new Error(`Bitget futures balance error: ${response.data.msg}`); } // Get USDT futures balance from accounts list const accounts = response.data.data || []; const usdtAccount = accounts.find(acc => acc.marginCoin === 'USDT'); const balance = parseFloat(usdtAccount?.available || 0); return { exchange: 'bitget', verified: true, usdt_futures_balance: balance }; } async placeOrder(symbol, side, orderType, qty, price = null) { // Map side values correctly for Bitget API v2 let bitgetSide; const normalizedSide = side.toLowerCase(); if (normalizedSide === 'buy' || normalizedSide === 'long') { bitgetSide = 'open_long'; } else if (normalizedSide === 'sell' || normalizedSide === 'short') { bitgetSide = 'open_short'; } else if (normalizedSide === 'open_long' || normalizedSide === 'open_short') { bitgetSide = normalizedSide; } else { throw new Error(`Invalid side parameter: ${side}. Use 'buy', 'sell', 'open_long', or 'open_short'`); } const body = { symbol: `${symbol}_UMCBL`, productType: 'USDT-FUTURES', marginMode: 'crossed', marginCoin: 'USDT', side: bitgetSide, orderType: orderType.toLowerCase() === 'market' ? 'market' : 'limit', size: qty.toString(), force: 'gtc' // Good Till Cancelled - default time in force }; if (orderType.toLowerCase() === 'limit' && price !== null && price !== undefined) { body.price = price.toString(); } logger.debug('Bitget order payload', { body }); const response = await this.makeRequest('/api/v2/mix/order/place-order', {}, { method: 'POST', signed: true, body }); if (response.data.code !== '00000') { throw new Error(`Bitget order placement error: ${response.data.msg}`); } return { exchange: 'bitget', status: 'order_placed', orderId: response.data.data.orderId, symbol: symbol, side: side, qty: qty }; } async getOpenOrders(symbol = null) { const params = { productType: 'USDT-FUTURES' }; if (symbol) { params.symbol = `${symbol}_UMCBL`; } const response = await this.makeRequest('/api/v2/futures/order/open-orders', params, { signed: true }); if (response.data.code !== '00000') { throw new Error(`Bitget get open orders error: ${response.data.msg}`); } return response.data.data; } async getPositions(symbol = null) { const params = { productType: 'USDT-FUTURES', marginCoin: 'USDT' }; const response = await this.makeRequest('/api/v2/futures/position/all-position', params, { signed: true }); if (response.data.code !== '00000') { throw new Error(`Bitget get positions error: ${response.data.msg}`); } // Filter by symbol if provided, as the API returns all positions if (symbol) { const bitgetSymbol = `${symbol}_UMCBL`; return response.data.data.filter(p => p.symbol === bitgetSymbol); } return response.data.data; } async placeTpslOrder(symbol, planType, triggerPrice, holdSide, size, triggerType = 'fill_price', executePrice = null) { const body = { symbol: `${symbol}_UMCBL`, productType: 'USDT-FUTURES', marginMode: 'crossed', marginCoin: 'USDT', planType: planType, // 'profit_plan' for TP or 'loss_plan' for SL triggerPrice: triggerPrice.toString(), triggerType: triggerType, // 'fill_price' or 'mark_price' holdSide: holdSide, // 'long' or 'short' - indicates which side to close size: size.toString() }; if (executePrice !== null && executePrice !== undefined) { body.executePrice = executePrice.toString(); } const response = await this.makeRequest('/api/v2/mix/order/place-tpsl-order', {}, { method: 'POST', signed: true, body }); if (response.data.code !== '00000') { throw new Error(`Bitget TP/SL order error: ${response.data.msg}`); } return { exchange: 'bitget', status: 'tpsl_order_placed', symbol: symbol, planType: planType, triggerPrice: triggerPrice, holdSide: holdSide, size: size, orderId: response.data.data.orderId }; } async placeTrailingStop(symbol, triggerPrice, callbackRatio, holdSide, size, triggerType = 'fill_price') { const body = { symbol: `${symbol}_UMCBL`, productType: 'USDT-FUTURES', marginMode: 'crossed', marginCoin: 'USDT', planType: 'moving_plan', // Trailing stop type triggerPrice: triggerPrice.toString(), triggerType: triggerType, callbackRatio: callbackRatio.toString(), // Trailing percentage like "0.01" for 1% holdSide: holdSide, // 'long' or 'short' size: size.toString() }; const response = await this.makeRequest('/api/v2/mix/order/place-plan-order', {}, { method: 'POST', signed: true, body }); if (response.data.code !== '00000') { throw new Error(`Bitget trailing stop error: ${response.data.msg}`); } return { exchange: 'bitget', status: 'trailing_stop_placed', symbol: symbol, triggerPrice: triggerPrice, callbackRatio: callbackRatio, holdSide: holdSide, size: size, orderId: response.data.data.orderId }; } async getOrderBook(symbol = 'BTCUSDT', limit = 50) { try { const bitgetSymbol = `${symbol}_UMCBL`; const params = { symbol: bitgetSymbol, productType: 'USDT-FUTURES', limit }; const response = await this.makeRequest('/api/v2/futures/market/order-book', params); if (response.data.code !== '00000' || !response.data.data || response.data.data.length === 0) { throw new Error(`Invalid order book data structure: ${response.data.msg}`); } const orderbook = response.data.data[0]; return { exchange: 'bitget', symbol, bids: orderbook.bids.map(([price, size]) => [parseFloat(price), parseFloat(size)]), asks: orderbook.asks.map(([price, size]) => [parseFloat(price), parseFloat(size)]), timestamp: orderbook.ts, real_data: true }; } catch (error) { logger.error('Bitget order book error', { symbol, error: error.message }); throw new Error(`Bitget order book error for ${symbol}: ${error.message}`); } } async getTrades(symbol = 'BTCUSDT', limit = 50) { try { const bitgetSymbol = `${symbol}_UMCBL`; const params = { symbol: bitgetSymbol, productType: 'USDT-FUTURES', limit }; const response = await this.makeRequest('/api/v2/futures/market/fills', params); if (response.data.code !== '00000' || !response.data.data) { throw new Error(`Invalid trades data structure: ${response.data.msg}`); } const trades = response.data.data; return { exchange: 'bitget', symbol, trades: trades.map(trade => ({ id: trade.tradeId, price: parseFloat(trade.price), size: parseFloat(trade.size), side: trade.side, time: parseInt(trade.ts, 10) })), real_data: true }; } catch (error) { logger.error('Bitget trades error', { symbol, error: error.message }); throw new Error(`Bitget trades error for ${symbol}: ${error.message}`); } } async getFundingRateHistory(symbol = 'BTCUSDT', limit = 1) { try { const bitgetSymbol = `${symbol}_UMCBL`; const params = { symbol: bitgetSymbol, productType: 'USDT-FUTURES', pageSize: limit }; const response = await this.makeRequest('/api/v2/futures/market/history-funding-rate', params); if (response.data.code !== '00000' || !response.data.data) { throw new Error(`Invalid funding rate data structure: ${response.data.msg}`); } const history = response.data.data; return { exchange: 'bitget', symbol, history: history.map(item => ({ rate: parseFloat(item.fundingRate), timestamp: parseInt(item.settleTime, 10) })), real_data: true }; } catch (error) { logger.error('Bitget funding rate error', { symbol, error: error.message }); throw new Error(`Bitget funding rate error for ${symbol}: ${error.message}`); } } async getOpenInterest(symbol = 'BTCUSDT', interval = '1H', limit = 50) { try { const bitgetSymbol = `${symbol}_UMCBL`; // Bitget uses different interval strings (e.g., '1H' vs '1h') const periodMap = { '5min': '5m', '15min': '15m', '30min': '30m', '1h': '1H', '4h': '4H', '1d': '1D' }; const period = periodMap[interval] || interval.toUpperCase(); const params = { symbol: bitgetSymbol, productType: 'USDT-FUTURES', period: period, pageSize: limit }; const response = await this.makeRequest('/api/v2/futures/market/history-open-interest', params); if (response.data.code !== '00000' || !response.data.data) { throw new Error(`Invalid open interest data structure: ${response.data.msg}`); } const history = response.data.data; return { exchange: 'bitget', symbol, interval, history: history.map(item => ({ value: parseFloat(item.openInterest), timestamp: parseInt(item.ts, 10) })).reverse(), // Bitget returns newest first, reverse for chronological order real_data: true }; } catch (error) { logger.error('Bitget open interest error', { symbol, interval, error: error.message }); throw new Error(`Bitget open interest error for ${symbol}: ${error.message}`); } } async getKlines(symbol = 'BTCUSDT', interval = '1h', limit = 100) { try { // Convert symbol to Bitget format const bitgetSymbol = `${symbol}_UMCBL`; // Convert interval to Bitget format const intervalMap = { '1m': '1m', '5m': '5m', '15m': '15m', '30m': '30m', '1h': '1H', '4h': '4H', '1d': '1D' }; const period = intervalMap[interval] || interval.toUpperCase(); const params = { symbol: bitgetSymbol, productType: 'USDT-FUTURES', granularity: period, limit: Math.min(limit, 200) // Bitget has a max limit }; logger.debug('Bitget klines request', { symbol, interval, params }); const response = await this.makeRequest('/api/v2/futures/market/candles', params); if (response.data.code !== '00000' || !response.data.data) { throw new Error(`Invalid klines data structure: ${response.data.msg || 'Unknown error'}`); } const candles = response.data.data; return { exchange: 'bitget', symbol, interval, klines: candles.map(([ts, open, high, low, close, volume, quoteVolume]) => ({ timestamp: parseInt(ts, 10), open: parseFloat(open), high: parseFloat(high), low: parseFloat(low), close: parseFloat(close), volume: parseFloat(volume), quoteVolume: parseFloat(quoteVolume || 0) })).reverse(), // Bitget returns newest first, reverse for chronological order real_data: true }; } catch (error) { logger.error('Bitget klines error', { symbol, interval, error: error.message }); // If it's an HTML response error, provide specific guidance if (error.message.includes('HTML') || error.message.includes('Invalid JSON')) { throw new Error(`Bitget klines API endpoint issue for ${symbol}: ${error.message}. This endpoint may be under maintenance or have changed. Consider using Binance fallback.`); } throw new Error(`Bitget klines error for ${symbol}: ${error.message}`); } } } // Technical Analysis Engine class TechnicalAnalysis { static calculateRSI(prices, period = 14) { if (!Array.isArray(prices) || prices.length < period + 1) { throw new Error(`Insufficient data for RSI calculation: need ${period + 1} prices, got ${prices.length}`); } let gains = 0; let losses = 0; // Calculate initial average gain and loss for (let i = 1; i <= period; i++) { const change = prices[i] - prices[i - 1]; if (change > 0) gains += change; else losses -= change; } let avgGain = gains / period; let avgLoss = losses / period; // Calculate RSI for remaining periods const rsiValues = []; for (let i = period + 1; i < prices.length; i++) { const change = prices[i] - prices[i - 1]; const gain = change > 0 ? change : 0; const loss = change < 0 ? -change : 0; avgGain = (avgGain * (period - 1) + gain) / period; avgLoss = (avgLoss * (period - 1) + loss) / period; const rs = avgGain / avgLoss; const rsi = 100 - (100 / (1 + rs)); rsiValues.push(rsi); } return rsiValues[rsiValues.length - 1]; } static calculateMACD(prices, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) { if (!Array.isArray(prices) || prices.length < slowPeriod) { throw new Error(`Insufficient data for MACD calculation: need ${slowPeriod} prices, got ${prices.length}`); } const fastEMA = this.calculateEMA(prices, fastPeriod); const slowEMA = this.calculateEMA(prices, slowPeriod); if (!fastEMA || !slowEMA) { throw new Error('Failed to calculate EMA values for MACD'); } const macdLine = fastEMA - slowEMA; const macdHistory = []; // Calculate MACD for all periods for (let i = slowPeriod - 1; i < prices.length; i++) { const fastEMAValue = this.calculateEMA(prices.slice(0, i + 1), fastPeriod); const slowEMAValue = this.calculateEMA(prices.slice(0, i + 1), slowPeriod); if (fastEMAValue && slowEMAValue) { macdHistory.push(fastEMAValue - slowEMAValue); } } const signalLine = this.calculateEMA(macdHistory, signalPeriod); const histogram = macdLine - (signalLine || 0); return { macd: macdLine, signal: signalLine, histogram: histogram }; } static calculateEMA(prices, period) { if (!Array.isArray(prices) || prices.length < period) return null; const multiplier = 2 / (period + 1); let ema = prices[0]; for (let i = 1; i < prices.length; i++) { ema = (prices[i] * multiplier) + (ema * (1 - multiplier)); } return ema; } static calculateBollingerBands(prices, period = 20, stdDev = 2) { if (!Array.isArray(prices) || prices.length < period) { throw new Error(`Insufficient data for Bollinger Bands: need ${period} prices, got ${prices.length}`); } const sma = this.calculateSMA(prices, period); const recentPrices = prices.slice(-period); // Calculate standard deviation const variance = recentPrices.reduce((sum, price) => { return sum + Math.pow(price - sma, 2); }, 0) / period; const standardDeviation = Math.sqrt(variance); return { upper: sma + (standardDeviation * stdDev), middle: sma, lower: sma - (standardDeviation * stdDev) }; } static calculateSMA(prices, period) { if (!Array.isArray(prices) || prices.length < period) return null; const sum = prices.slice(-period).reduce((a, b) => a + b, 0); return sum / period; } static calculateStochastic(prices, kPeriod = 14, dPeriod = 3, smoothing = 3) { if (!Array.isArray(prices) || prices.length < kPeriod) { throw new Error(`Insufficient data for Stochastic calculation: need ${kPeriod} prices, got ${prices.length}`); } const highestHigh = (arr) => Math.max(...arr); const lowestLow = (arr) => Math.min(...arr); const kValues = []; for (let i = kPeriod - 1; i < prices.length; i++) { const periodPrices = prices.slice(i - kPeriod + 1, i + 1); const currentClose = prices[i]; const hh = highestHigh(periodPrices); const ll = lowestLow(periodPrices); if (hh === ll) { // Avoid division by zero kValues.push(0); } else { const k = ((currentClose - ll) / (hh - ll)) * 100; kValues.push(k); } } // Simple Moving Average for %D const dValues = []; for (let i = smoothing - 1; i < kValues.length; i++) { const sum = kValues.slice(i - smoothing + 1, i + 1).reduce((a, b) => a + b, 0); dValues.push(sum / smoothing); } return { k: kValues[kValues.length - 1], d: dValues[dValues.length - 1] }; } static calculateADX(highs, lows, closes, period = 14) { if (!Array.isArray(highs) || highs.length < period || !Array.isArray(lows) || lows.length < period || !Array.isArray(closes) || closes.length < period) { throw new Error(`Insufficient data for ADX calculation: need ${period} data points for highs, lows, and closes.`); } const trValues = []; // True Range const plusDM = []; // +DM (Positive Directional Movement) const minusDM = []; // -DM (Negative Directional Movement) for (let i = 1; i < closes.length; i++) { const high = highs[i]; const low = lows[i]; const close = closes[i]; const prevClose = closes[i - 1]; const tr = Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose)); trValues.push(tr); const upMove = high - highs[i - 1]; const downMove = lows[i - 1] - low; if (upMove > downMove && upMove > 0) { plusDM.push(upMove); minusDM.push(0); } else if (downMove > upMove && downMove > 0) { plu