rest-client.tsā¢24.7 kB
/**
* Bitget REST API Client
* Handles all REST API communications with Bitget exchange
*/
import crypto from 'crypto';
import fetch from 'node-fetch';
import {
BitgetConfig,
APIResponse,
Ticker,
OrderBook,
Candle,
Order,
Balance,
Position,
OrderParams,
BitgetError,
BitgetAPIError,
BitgetNetworkError,
BitgetRateLimitError,
BitgetAuthenticationError,
RetryConfig
} from '../types/bitget.js';
import { logger } from '../utils/logger.js';
import { retryManager, RetryManager } from '../utils/retry.js';
import { priceCache, tickerCache, orderbookCache, candlesCache, balanceCache, positionsCache } from '../utils/cache.js';
export class BitgetRestClient {
private config: BitgetConfig;
private rateLimitRequests: number = 0;
private rateLimitWindow: number = Date.now();
private retryManager: RetryManager;
constructor(config: BitgetConfig, retryConfig?: Partial<RetryConfig>) {
this.config = config;
this.retryManager = new RetryManager(retryConfig);
logger.info('BitgetRestClient initialized', {
sandbox: config.sandbox,
baseUrl: config.baseUrl
});
}
/**
* Validate API credentials by making a test request
*/
async validateCredentials(): Promise<boolean> {
try {
if (!this.config.apiKey || !this.config.secretKey || !this.config.passphrase) {
throw new BitgetAuthenticationError('Missing API credentials');
}
// Test with a simple account info request
await this.request('GET', '/api/v2/spot/account/assets', {}, true);
logger.info('API credentials validated successfully');
return true;
} catch (error: any) {
logger.error('API credentials validation failed', {
error: error.message,
errorType: error.constructor.name
});
return false;
}
}
/**
* Helper to determine if symbol is for futures (contains _UMCBL)
*/
private isFuturesSymbol(symbol: string): boolean {
return symbol.includes('_UMCBL') || symbol.includes('_');
}
/**
* Format interval for Bitget Futures API
* Futures API accepts: [1m,3m,5m,15m,30m,1H,4H,6H,12H,1D,1W,1M,6Hutc,12Hutc,1Dutc,3Dutc,1Wutc,1Mutc]
*/
private formatIntervalForFuturesAPI(interval: string): string {
const lower = interval.toLowerCase();
// Minutes: keep short format (1m, 5m, 15m, 30m)
if (lower.match(/^\d+m$/)) {
return lower;
}
// Hours: convert to uppercase H (1H, 4H, 6H, 12H)
if (lower.includes('h')) {
return lower.replace('h', 'H');
}
// Days/Weeks/Months: uppercase (1D, 1W, 1M)
if (lower.includes('d') || lower.includes('w')) {
return lower.toUpperCase();
}
// Default: return as is for special cases like UTC variants
return interval;
}
/**
* Format interval for Bitget Spot API
* Spot API accepts: [1min,3min,5min,15min,30min,1h,4h,6h,12h,1day,1week,1M,6Hutc,12Hutc,1Dutc,3Dutc,1Wutc,1Mutc]
*/
private formatIntervalForSpotAPI(interval: string): string {
const lower = interval.toLowerCase();
// Minutes: convert to full format (1min, 5min, 15min, 30min)
if (lower.match(/^\d+m$/)) {
return lower.replace('m', 'min');
}
// Hours: keep lowercase (1h, 4h, 6h, 12h)
if (lower.includes('h') && !lower.includes('utc')) {
return lower;
}
// Days: convert to full format (1day)
if (lower.match(/^\d+d$/)) {
return lower.replace('d', 'day');
}
// Weeks: convert to full format (1week)
if (lower.match(/^\d+w$/)) {
return lower.replace('w', 'week');
}
// Months and UTC variants: return as is
return interval;
}
/**
* Generate authentication signature for private endpoints
*/
private generateSignature(timestamp: string, method: string, requestPath: string, body: string = ''): string {
const message = timestamp + method.toUpperCase() + requestPath + body;
return crypto.createHmac('sha256', this.config.secretKey).update(message).digest('base64');
}
/**
* Rate limiting check
*/
private checkRateLimit(): void {
const now = Date.now();
if (now - this.rateLimitWindow > 1000) {
this.rateLimitWindow = now;
this.rateLimitRequests = 0;
}
if (this.rateLimitRequests >= 10) {
logger.warn('Rate limit exceeded', {
requests: this.rateLimitRequests,
window: this.rateLimitWindow
});
throw new BitgetRateLimitError('Rate limit exceeded: 10 requests per second');
}
this.rateLimitRequests++;
}
/**
* Make authenticated request to Bitget API
*/
private async request<T>(
method: 'GET' | 'POST' | 'DELETE',
endpoint: string,
params: Record<string, any> = {},
isPrivate: boolean = false
): Promise<APIResponse<T>> {
const requestId = Math.random().toString(36).substring(7);
const context = `${method} ${endpoint}`;
return this.retryManager.execute(async () => {
this.checkRateLimit();
const timestamp = Date.now().toString();
let url = `${this.config.baseUrl}${endpoint}`;
let body = '';
// Build query string for GET requests
let queryString = '';
if (method === 'GET' && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, value.toString());
}
});
queryString = searchParams.toString();
url += `?${queryString}`;
}
// Handle body for POST requests
if (method === 'POST' && Object.keys(params).length > 0) {
body = JSON.stringify(params);
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// Add authentication headers for private endpoints
if (isPrivate) {
// For GET requests, include query params in signature path
const signaturePath = method === 'GET' && queryString
? `${endpoint}?${queryString}`
: endpoint;
const signature = this.generateSignature(timestamp, method, signaturePath, body);
headers['ACCESS-KEY'] = this.config.apiKey;
headers['ACCESS-SIGN'] = signature;
headers['ACCESS-TIMESTAMP'] = timestamp;
headers['ACCESS-PASSPHRASE'] = this.config.passphrase;
// Add demo trading header if in sandbox mode
if (this.config.sandbox) {
headers['paptrading'] = '1';
}
}
try {
logger.debug('Making API request', {
requestId,
method,
url,
isPrivate,
bodyLength: body.length
});
const response = await fetch(url, {
method,
headers,
body: method === 'POST' ? body : undefined,
});
if (!response.ok) {
throw new BitgetNetworkError(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json() as APIResponse<T>;
logger.debug('Received API response', {
requestId,
status: response.status,
code: data.code
});
if (data.code !== '00000') {
const errorCode = data.code;
const errorMessage = data.msg || 'Unknown API error';
// Classify errors
if (errorCode === '40009') {
throw new BitgetAuthenticationError(`Authentication failed: ${errorMessage}`);
} else if (errorCode === '40014') {
throw new BitgetRateLimitError(`Rate limit exceeded: ${errorMessage}`);
} else {
throw new BitgetAPIError(errorCode, errorMessage, requestId, endpoint);
}
}
return data;
} catch (error: any) {
logger.error('API request failed', {
requestId,
method,
url,
error: error.message,
errorType: error.constructor.name
});
// Re-throw custom errors as-is
if (error instanceof BitgetAPIError ||
error instanceof BitgetNetworkError ||
error instanceof BitgetRateLimitError ||
error instanceof BitgetAuthenticationError) {
throw error;
}
// Wrap other errors as network errors
throw new BitgetNetworkError(`Network error: ${error.message}`, error);
}
}, context);
}
// ========== PUBLIC MARKET DATA METHODS ==========
/**
* Get current price for a symbol
*/
async getPrice(symbol: string): Promise<string> {
const cacheKey = `price:${symbol}`;
// Try cache first
const cachedPrice = priceCache.get(cacheKey);
if (cachedPrice) {
return cachedPrice;
}
let price: string = '';
if (this.isFuturesSymbol(symbol)) {
// Futures ticker
const futuresSymbol = symbol.includes('_UMCBL') ? symbol : `${symbol}_UMCBL`;
const response = await this.request<any>('GET', '/api/mix/v1/market/ticker', { symbol: futuresSymbol });
if (response.data?.last) {
price = response.data.last;
} else {
throw new Error(`Price not found for symbol: ${symbol}`);
}
} else {
// Spot ticker - use v1 public API
const response = await this.request<any>('GET', '/api/spot/v1/market/tickers', {});
if (response.data && Array.isArray(response.data)) {
const ticker = response.data.find((t: any) => t.symbol === symbol);
if (ticker) {
price = ticker.close;
} else {
throw new Error(`Price not found for symbol: ${symbol}`);
}
} else {
throw new Error(`Price not found for symbol: ${symbol}`);
}
}
// Cache the result
priceCache.set(cacheKey, price);
return price;
}
/**
* Get full ticker information
*/
async getTicker(symbol: string): Promise<Ticker> {
const cacheKey = `ticker:${symbol}`;
// Try cache first
const cachedTicker = tickerCache.get(cacheKey);
if (cachedTicker) {
return cachedTicker;
}
let ticker: Ticker = {
symbol: '',
last: '',
bid: '',
ask: '',
high24h: '',
low24h: '',
volume24h: '',
change24h: '',
changePercent24h: '',
timestamp: 0
};
if (this.isFuturesSymbol(symbol)) {
// Futures ticker
const futuresSymbol = symbol.includes('_UMCBL') ? symbol : `${symbol}_UMCBL`;
const response = await this.request<any>('GET', '/api/mix/v1/market/ticker', { symbol: futuresSymbol });
if (response.data) {
const tickerData = response.data;
ticker = {
symbol: tickerData.symbol,
last: tickerData.last,
bid: tickerData.bestBid,
ask: tickerData.bestAsk,
high24h: tickerData.high24h,
low24h: tickerData.low24h,
volume24h: tickerData.baseVolume,
change24h: ((parseFloat(tickerData.last) - parseFloat(tickerData.openUtc)) / parseFloat(tickerData.openUtc) * 100).toFixed(2),
changePercent24h: tickerData.priceChangePercent,
timestamp: parseInt(tickerData.timestamp) || Date.now()
};
} else {
throw new Error(`Ticker not found for symbol: ${symbol}`);
}
} else {
// Spot ticker - use v1 public API
const response = await this.request<any>('GET', '/api/spot/v1/market/tickers', {});
if (response.data && Array.isArray(response.data)) {
const tickerData = response.data.find((t: any) => t.symbol === symbol);
if (tickerData) {
ticker = {
symbol: tickerData.symbol,
last: tickerData.close,
bid: tickerData.buyOne,
ask: tickerData.sellOne,
high24h: tickerData.high24h,
low24h: tickerData.low24h,
volume24h: tickerData.baseVol,
change24h: tickerData.change,
changePercent24h: tickerData.changePercent,
timestamp: parseInt(tickerData.ts) || Date.now()
};
} else {
throw new Error(`Ticker not found for symbol: ${symbol}`);
}
} else {
throw new Error(`Ticker not found for symbol: ${symbol}`);
}
}
// Cache the result
tickerCache.set(cacheKey, ticker);
return ticker;
}
/**
* Get order book
*/
async getOrderBook(symbol: string, depth: number = 20): Promise<OrderBook> {
const cacheKey = `orderbook:${symbol}:${depth}`;
// Try cache first
const cachedOrderBook = orderbookCache.get(cacheKey);
if (cachedOrderBook) {
return cachedOrderBook;
}
let orderBook: OrderBook = {
symbol: '',
bids: [],
asks: [],
timestamp: 0
};
if (this.isFuturesSymbol(symbol)) {
// Futures orderbook
const futuresSymbol = symbol.includes('_UMCBL') ? symbol : `${symbol}_UMCBL`;
const response = await this.request<any>('GET', '/api/mix/v1/market/depth', {
symbol: futuresSymbol,
limit: depth.toString()
});
orderBook = {
symbol: futuresSymbol,
bids: response.data?.bids || [],
asks: response.data?.asks || [],
timestamp: response.data?.timestamp || Date.now()
};
} else {
// Spot orderbook
const response = await this.request<any>('GET', '/api/v2/spot/market/orderbook', {
symbol,
type: 'step0',
limit: depth.toString()
});
orderBook = {
symbol,
bids: response.data?.bids || [],
asks: response.data?.asks || [],
timestamp: response.data?.ts || Date.now()
};
}
// Cache the result
orderbookCache.set(cacheKey, orderBook);
return orderBook;
}
/**
* Get historical candles/klines
*/
async getCandles(symbol: string, interval: string, limit: number = 100): Promise<Candle[]> {
if (this.isFuturesSymbol(symbol)) {
// Futures candles - use v2 API with productType
// Remove _UMCBL suffix if present for v2 API
const cleanSymbol = symbol.replace('_UMCBL', '');
const response = await this.request<string[][]>('GET', '/api/v2/mix/market/candles', {
productType: 'USDT-FUTURES',
symbol: cleanSymbol,
granularity: this.formatIntervalForFuturesAPI(interval), // Format interval for futures API
limit: limit.toString()
});
if (!response.data || response.data.length === 0) {
return [];
}
return response.data.map(candle => ({
symbol: symbol, // Keep original symbol format
timestamp: parseInt(candle[0]),
open: candle[1],
high: candle[2],
low: candle[3],
close: candle[4],
volume: candle[5]
}));
} else {
// Spot candles
const response = await this.request<string[][]>('GET', '/api/v2/spot/market/candles', {
symbol,
granularity: this.formatIntervalForSpotAPI(interval), // Format interval for spot API
limit: limit.toString()
});
if (!response.data || response.data.length === 0) {
return [];
}
return response.data.map(candle => ({
symbol,
timestamp: parseInt(candle[0]),
open: candle[1],
high: candle[2],
low: candle[3],
close: candle[4],
volume: candle[5]
}));
}
}
// ========== PRIVATE TRADING METHODS ==========
/**
* Get account balance
*/
async getBalance(asset?: string): Promise<Balance[]> {
const response = await this.request<any>('GET', '/api/v2/spot/account/assets', {}, true);
const balances = response.data.map((item: any) => ({
asset: item.coin,
free: item.available,
locked: item.frozen,
total: (parseFloat(item.available) + parseFloat(item.frozen)).toString()
}));
if (asset) {
return balances.filter((balance: Balance) => balance.asset === asset);
}
return balances;
}
/**
* Place a new order (automatically detects spot vs futures)
*/
async placeOrder(params: OrderParams): Promise<Order> {
if (this.isFuturesSymbol(params.symbol)) {
return this.placeFuturesOrder(params);
} else {
return this.placeSpotOrder(params);
}
}
/**
* Place a spot order
*/
private async placeSpotOrder(params: OrderParams): Promise<Order> {
const orderData: any = {
symbol: params.symbol,
side: params.side,
orderType: params.type,
size: params.quantity, // v2 API uses 'size' instead of 'quantity'
};
if (params.type === 'limit' && params.price) {
orderData.price = params.price;
}
if (params.timeInForce) {
orderData.force = params.timeInForce; // v2 API uses 'force' instead of 'timeInForceValue'
} else if (params.type === 'limit') {
orderData.force = 'GTC'; // Default to GTC for limit orders
}
if (params.clientOrderId) {
orderData.clientOid = params.clientOrderId;
}
const response = await this.request<any>('POST', '/api/v2/spot/trade/place-order', orderData, true);
return {
orderId: response.data.orderId,
clientOrderId: response.data.clientOid,
symbol: params.symbol,
side: params.side,
type: params.type,
quantity: params.quantity,
price: params.price,
status: 'open',
filled: '0',
remaining: params.quantity,
timestamp: Date.now(),
updateTime: Date.now()
};
}
/**
* Place a futures order
*/
private async placeFuturesOrder(params: OrderParams): Promise<Order> {
// For v1 mix API, keep the _UMCBL suffix
const futuresSymbol = params.symbol.includes('_UMCBL') ? params.symbol : `${params.symbol}_UMCBL`;
// Use v2 API format for futures orders
const orderData: any = {
symbol: futuresSymbol.replace('_UMCBL', ''), // v2 API might not need suffix
productType: 'USDT-FUTURES',
marginCoin: 'USDT',
marginMode: 'crossed',
side: params.side,
orderType: params.type,
size: params.quantity, // For futures, this is in contracts
};
if (params.type === 'limit' && params.price) {
orderData.price = params.price;
}
if (params.timeInForce) {
orderData.timeInForceValue = params.timeInForce; // v2 API uses timeInForceValue
} else if (params.type === 'limit') {
orderData.timeInForceValue = 'GTC'; // v2 API uses 'GTC'
}
if (params.clientOrderId) {
orderData.clientOid = params.clientOrderId;
}
if (params.reduceOnly) {
orderData.reduceOnly = params.reduceOnly;
}
console.error('Placing futures order with data:', JSON.stringify(orderData, null, 2));
// Try v2 API endpoint
const response = await this.request<any>('POST', '/api/v2/mix/order/place-order', orderData, true);
return {
orderId: response.data.orderId,
clientOrderId: response.data.clientOid,
symbol: params.symbol, // Return original symbol with suffix
side: params.side,
type: params.type,
quantity: params.quantity,
price: params.price,
status: 'open',
filled: '0',
remaining: params.quantity,
timestamp: Date.now(),
updateTime: Date.now()
};
}
/**
* Cancel an order (automatically detects spot vs futures)
*/
async cancelOrder(orderId: string, symbol: string): Promise<boolean> {
if (this.isFuturesSymbol(symbol)) {
return this.cancelFuturesOrder(orderId, symbol);
} else {
return this.cancelSpotOrder(orderId, symbol);
}
}
/**
* Cancel a spot order
*/
private async cancelSpotOrder(orderId: string, symbol: string): Promise<boolean> {
const response = await this.request<any>('POST', '/api/v2/spot/trade/cancel-order', {
orderId,
symbol
}, true);
return response.code === '00000';
}
/**
* Cancel a futures order
*/
private async cancelFuturesOrder(orderId: string, symbol: string): Promise<boolean> {
// Remove _UMCBL suffix for v1 API
const cleanSymbol = symbol.replace('_UMCBL', '');
const response = await this.request<any>('POST', '/api/v2/mix/order/cancel-order', {
orderId,
symbol: cleanSymbol,
productType: 'USDT-FUTURES',
marginCoin: 'USDT'
}, true);
return response.code === '00000';
}
/**
* Get open orders (supports both spot and futures)
*/
async getOrders(symbol?: string, status?: string): Promise<Order[]> {
if (symbol && this.isFuturesSymbol(symbol)) {
return this.getFuturesOrders(symbol, status);
} else {
return this.getSpotOrders(symbol, status);
}
}
/**
* Get spot orders
*/
private async getSpotOrders(symbol?: string, status?: string): Promise<Order[]> {
const params: any = {};
if (symbol) params.symbol = symbol;
const response = await this.request<any[]>('GET', '/api/v2/spot/trade/unfilled-orders', params, true);
return response.data.map(order => ({
orderId: order.orderId,
clientOrderId: order.clientOid,
symbol: order.symbol,
side: order.side,
type: order.orderType,
quantity: order.quantity,
price: order.price,
status: order.status,
filled: order.fillQuantity,
remaining: (parseFloat(order.quantity) - parseFloat(order.fillQuantity)).toString(),
timestamp: parseInt(order.cTime),
updateTime: parseInt(order.uTime)
}));
}
/**
* Get futures orders
*/
private async getFuturesOrders(symbol?: string, status?: string): Promise<Order[]> {
const params: any = {};
if (symbol) {
// Remove _UMCBL suffix for v1 API
params.symbol = symbol.replace('_UMCBL', '');
}
const response = await this.request<any[]>('GET', '/api/mix/v1/order/current', params, true);
return response.data.map(order => ({
orderId: order.orderId,
clientOrderId: order.clientOid,
symbol: symbol || `${order.symbol}_UMCBL`, // Add suffix back for consistency
side: order.side,
type: order.orderType,
quantity: order.size,
price: order.price,
status: order.state,
filled: order.fillSize,
remaining: (parseFloat(order.size) - parseFloat(order.fillSize || '0')).toString(),
timestamp: parseInt(order.cTime),
updateTime: parseInt(order.uTime)
}));
}
// ========== FUTURES METHODS ==========
/**
* Get futures positions
*/
async getFuturesPositions(symbol?: string): Promise<Position[]> {
const params: any = { productType: 'USDT-FUTURES' };
if (symbol) {
// Add _UMCBL suffix for futures if not present
params.symbol = symbol.includes('_') ? symbol : `${symbol}_UMCBL`;
}
const response = await this.request<any>('GET', '/api/v2/mix/position/all-position', params, true);
const positions = response.data || [];
return positions.map((position: any) => ({
symbol: position.symbol,
side: position.holdSide || (parseFloat(position.size || '0') > 0 ? 'long' : 'short'),
size: Math.abs(parseFloat(position.size || position.total || '0')).toString(),
entryPrice: position.averageOpenPrice || position.openPriceAvg,
markPrice: position.markPrice,
pnl: position.unrealizedPL || position.achievedProfits,
pnlPercent: position.unrealizedPLR || '0',
margin: position.margin || position.marginSize,
leverage: position.leverage,
timestamp: parseInt(position.cTime || Date.now().toString())
}));
}
/**
* Set leverage for futures trading
*/
async setLeverage(symbol: string, leverage: number): Promise<boolean> {
// Remove _UMCBL suffix for v2 API (like in candles)
const cleanSymbol = symbol.replace('_UMCBL', '');
const response = await this.request<any>('POST', '/api/v2/mix/account/set-leverage', {
symbol: cleanSymbol,
productType: 'USDT-FUTURES',
marginCoin: 'USDT', // Required parameter!
leverage: leverage.toString(),
holdSide: 'long'
}, true);
return response.code === '00000';
}
/**
* Get margin information
*/
async getMarginInfo(symbol?: string): Promise<any> {
const params: any = { productType: 'USDT-FUTURES' };
if (symbol) {
// Add _UMCBL suffix for futures if not present
params.symbol = symbol.includes('_') ? symbol : `${symbol}_UMCBL`;
}
const response = await this.request<any>('GET', '/api/v2/mix/account/accounts', params, true);
return response.data;
}
}