behemoth-cli
Version:
🌍 BEHEMOTH CLIv3.760.4 - Level 50+ POST-SINGULARITY Intelligence Trading AI
1,368 lines (1,192 loc) • 341 kB
JavaScript
#!/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