@jmparsons/ccxt
Version:
A JavaScript / Python / PHP cryptocurrency trading library with support for 100+ exchanges
608 lines (581 loc) • 23.9 kB
JavaScript
'use strict';
// ---------------------------------------------------------------------------
const Exchange = require ('./base/Exchange');
const { ExchangeError, DDoSProtection, OrderNotFound, AuthenticationError } = require ('./base/errors');
// ---------------------------------------------------------------------------
module.exports = class bitmex extends Exchange {
describe () {
return this.deepExtend (super.describe (), {
'id': 'bitmex',
'name': 'BitMEX',
'countries': 'SC', // Seychelles
'version': 'v1',
'userAgent': undefined,
'rateLimit': 2000,
'has': {
'CORS': false,
'fetchOHLCV': true,
'withdraw': true,
'editOrder': true,
'fetchOrder': true,
'fetchOrders': true,
'fetchOpenOrders': true,
'fetchClosedOrders': true,
},
'timeframes': {
'1m': '1m',
'5m': '5m',
'1h': '1h',
'1d': '1d',
},
'urls': {
'test': 'https://testnet.bitmex.com',
'logo': 'https://user-images.githubusercontent.com/1294454/27766319-f653c6e6-5ed4-11e7-933d-f0bc3699ae8f.jpg',
'api': 'https://www.bitmex.com',
'www': 'https://www.bitmex.com',
'doc': [
'https://www.bitmex.com/app/apiOverview',
'https://github.com/BitMEX/api-connectors/tree/master/official-http',
],
'fees': 'https://www.bitmex.com/app/fees',
},
'api': {
'public': {
'get': [
'announcement',
'announcement/urgent',
'funding',
'instrument',
'instrument/active',
'instrument/activeAndIndices',
'instrument/activeIntervals',
'instrument/compositeIndex',
'instrument/indices',
'insurance',
'leaderboard',
'liquidation',
'orderBook',
'orderBook/L2',
'quote',
'quote/bucketed',
'schema',
'schema/websocketHelp',
'settlement',
'stats',
'stats/history',
'trade',
'trade/bucketed',
],
},
'private': {
'get': [
'apiKey',
'chat',
'chat/channels',
'chat/connected',
'execution',
'execution/tradeHistory',
'notification',
'order',
'position',
'user',
'user/affiliateStatus',
'user/checkReferralCode',
'user/commission',
'user/depositAddress',
'user/margin',
'user/minWithdrawalFee',
'user/wallet',
'user/walletHistory',
'user/walletSummary',
],
'post': [
'apiKey',
'apiKey/disable',
'apiKey/enable',
'chat',
'order',
'order/bulk',
'order/cancelAllAfter',
'order/closePosition',
'position/isolate',
'position/leverage',
'position/riskLimit',
'position/transferMargin',
'user/cancelWithdrawal',
'user/confirmEmail',
'user/confirmEnableTFA',
'user/confirmWithdrawal',
'user/disableTFA',
'user/logout',
'user/logoutAll',
'user/preferences',
'user/requestEnableTFA',
'user/requestWithdrawal',
],
'put': [
'order',
'order/bulk',
'user',
],
'delete': [
'apiKey',
'order',
'order/all',
],
},
},
});
}
async fetchMarkets () {
let markets = await this.publicGetInstrumentActiveAndIndices ();
let result = [];
for (let p = 0; p < markets.length; p++) {
let market = markets[p];
let active = (market['state'] !== 'Unlisted');
let id = market['symbol'];
let base = market['underlying'];
let quote = market['quoteCurrency'];
let type = undefined;
let future = false;
let prediction = false;
let basequote = base + quote;
base = this.commonCurrencyCode (base);
quote = this.commonCurrencyCode (quote);
let swap = (id === basequote);
let symbol = id;
if (swap) {
type = 'swap';
symbol = base + '/' + quote;
} else if (id.indexOf ('B_') >= 0) {
prediction = true;
type = 'prediction';
} else {
future = true;
type = 'future';
}
let precision = {
'amount': undefined,
'price': undefined,
};
if (market['lotSize'])
precision['amount'] = this.precisionFromString (this.truncate_to_string (market['lotSize'], 16));
if (market['tickSize'])
precision['price'] = this.precisionFromString (this.truncate_to_string (market['tickSize'], 16));
result.push ({
'id': id,
'symbol': symbol,
'base': base,
'quote': quote,
'active': active,
'precision': precision,
'limits': {
'amount': {
'min': market['lotSize'],
'max': market['maxOrderQty'],
},
'price': {
'min': market['tickSize'],
'max': market['maxPrice'],
},
},
'taker': market['takerFee'],
'maker': market['makerFee'],
'type': type,
'spot': false,
'swap': swap,
'future': future,
'prediction': prediction,
'info': market,
});
}
return result;
}
async fetchBalance (params = {}) {
await this.loadMarkets ();
let response = await this.privateGetUserMargin ({ 'currency': 'all' });
let result = { 'info': response };
for (let b = 0; b < response.length; b++) {
let balance = response[b];
let currency = balance['currency'].toUpperCase ();
currency = this.commonCurrencyCode (currency);
let account = {
'free': balance['availableMargin'],
'used': 0.0,
'total': balance['marginBalance'],
};
if (currency === 'BTC') {
account['free'] = account['free'] * 0.00000001;
account['total'] = account['total'] * 0.00000001;
}
account['used'] = account['total'] - account['free'];
result[currency] = account;
}
return this.parseBalance (result);
}
async fetchOrderBook (symbol, limit = undefined, params = {}) {
await this.loadMarkets ();
let market = this.market (symbol);
let request = {
'symbol': market['id'],
};
if (typeof limit !== 'undefined')
request['depth'] = limit;
let orderbook = await this.publicGetOrderBookL2 (this.extend (request, params));
let result = {
'bids': [],
'asks': [],
'timestamp': undefined,
'datetime': undefined,
'nonce': undefined,
};
for (let o = 0; o < orderbook.length; o++) {
let order = orderbook[o];
let side = (order['side'] === 'Sell') ? 'asks' : 'bids';
let amount = order['size'];
let price = order['price'];
result[side].push ([ price, amount ]);
}
result['bids'] = this.sortBy (result['bids'], 0, true);
result['asks'] = this.sortBy (result['asks'], 0);
return result;
}
async fetchOrder (id, symbol = undefined, params = {}) {
let filter = { 'filter': { 'orderID': id }};
let result = await this.fetchOrders (symbol, undefined, undefined, this.deepExtend (filter, params));
let numResults = result.length;
if (numResults === 1)
return result[0];
throw new OrderNotFound (this.id + ': The order ' + id + ' not found.');
}
async fetchOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
await this.loadMarkets ();
let market = undefined;
let request = {};
if (typeof symbol !== 'undefined') {
market = this.market (symbol);
request['symbol'] = market['id'];
}
if (typeof since !== 'undefined')
request['startTime'] = this.iso8601 (since);
if (typeof limit !== 'undefined')
request['count'] = limit;
request = this.deepExtend (request, params);
// why the hassle? urlencode in python is kinda broken for nested dicts.
// E.g. self.urlencode({"filter": {"open": True}}) will return "filter={'open':+True}"
// Bitmex doesn't like that. Hence resorting to this hack.
if ('filter' in request)
request['filter'] = this.json (request['filter']);
let response = await this.privateGetOrder (request);
return this.parseOrders (response, market, since, limit);
}
async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
let filter_params = { 'filter': { 'open': true }};
return await this.fetchOrders (symbol, since, limit, this.deepExtend (filter_params, params));
}
async fetchClosedOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
// Bitmex barfs if you set 'open': false in the filter...
let orders = await this.fetchOrders (symbol, since, limit, params);
return this.filterBy (orders, 'status', 'closed');
}
async fetchTicker (symbol, params = {}) {
await this.loadMarkets ();
let market = this.market (symbol);
if (!market['active'])
throw new ExchangeError (this.id + ': symbol ' + symbol + ' is delisted');
let request = this.extend ({
'symbol': market['id'],
'binSize': '1d',
'partial': true,
'count': 1,
'reverse': true,
}, params);
let quotes = await this.publicGetQuoteBucketed (request);
let quotesLength = quotes.length;
let quote = quotes[quotesLength - 1];
let tickers = await this.publicGetTradeBucketed (request);
let ticker = tickers[0];
let timestamp = this.milliseconds ();
let open = this.safeFloat (ticker, 'open');
let close = this.safeFloat (ticker, 'close');
let change = close - open;
return {
'symbol': symbol,
'timestamp': timestamp,
'datetime': this.iso8601 (timestamp),
'high': this.safeFloat (ticker, 'high'),
'low': this.safeFloat (ticker, 'low'),
'bid': this.safeFloat (quote, 'bidPrice'),
'bidVolume': undefined,
'ask': this.safeFloat (quote, 'askPrice'),
'askVolume': undefined,
'vwap': this.safeFloat (ticker, 'vwap'),
'open': open,
'close': close,
'last': close,
'previousClose': undefined,
'change': change,
'percentage': change / open * 100,
'average': this.sum (open, close) / 2,
'baseVolume': this.safeFloat (ticker, 'homeNotional'),
'quoteVolume': this.safeFloat (ticker, 'foreignNotional'),
'info': ticker,
};
}
parseOHLCV (ohlcv, market = undefined, timeframe = '1m', since = undefined, limit = undefined) {
let timestamp = this.parse8601 (ohlcv['timestamp']) - this.parseTimeframe (timeframe) * 1000;
return [
timestamp,
ohlcv['open'],
ohlcv['high'],
ohlcv['low'],
ohlcv['close'],
ohlcv['volume'],
];
}
async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
await this.loadMarkets ();
// send JSON key/value pairs, such as {"key": "value"}
// filter by individual fields and do advanced queries on timestamps
// let filter = { 'key': 'value' };
// send a bare series (e.g. XBU) to nearest expiring contract in that series
// you can also send a timeframe, e.g. XBU:monthly
// timeframes: daily, weekly, monthly, quarterly, and biquarterly
let market = this.market (symbol);
let request = {
'symbol': market['id'],
'binSize': this.timeframes[timeframe],
'partial': true, // true == include yet-incomplete current bins
// 'filter': filter, // filter by individual fields and do advanced queries
// 'columns': [], // will return all columns if omitted
// 'start': 0, // starting point for results (wtf?)
// 'reverse': false, // true == newest first
// 'endTime': '', // ending date filter for results
};
if (typeof limit !== 'undefined')
request['count'] = limit; // default 100, max 500
// if since is not set, they will return candles starting from 2017-01-01
if (typeof since !== 'undefined') {
let ymdhms = this.ymdhms (since);
let ymdhm = ymdhms.slice (0, 16);
request['startTime'] = ymdhm; // starting date filter for results
}
let response = await this.publicGetTradeBucketed (this.extend (request, params));
return this.parseOHLCVs (response, market, timeframe, since, limit);
}
parseTrade (trade, market = undefined) {
let timestamp = this.parse8601 (trade['timestamp']);
let symbol = undefined;
if (!market) {
if ('symbol' in trade)
market = this.markets_by_id[trade['symbol']];
}
if (market)
symbol = market['symbol'];
return {
'id': trade['trdMatchID'],
'info': trade,
'timestamp': timestamp,
'datetime': this.iso8601 (timestamp),
'symbol': symbol,
'order': undefined,
'type': undefined,
'side': trade['side'].toLowerCase (),
'price': trade['price'],
'amount': trade['size'],
};
}
parseOrderStatus (status) {
let statuses = {
'new': 'open',
'partiallyfilled': 'open',
'filled': 'closed',
'canceled': 'canceled',
'rejected': 'rejected',
'expired': 'expired',
};
return this.safeString (statuses, status.toLowerCase ());
}
parseOrder (order, market = undefined) {
let status = this.safeValue (order, 'ordStatus');
if (typeof status !== 'undefined')
status = this.parseOrderStatus (status);
let symbol = undefined;
if (market) {
symbol = market['symbol'];
} else {
let id = order['symbol'];
if (id in this.markets_by_id) {
market = this.markets_by_id[id];
symbol = market['symbol'];
}
}
let datetime_value = undefined;
let timestamp = undefined;
let iso8601 = undefined;
if ('timestamp' in order)
datetime_value = order['timestamp'];
else if ('transactTime' in order)
datetime_value = order['transactTime'];
if (typeof datetime_value !== 'undefined') {
timestamp = this.parse8601 (datetime_value);
iso8601 = this.iso8601 (timestamp);
}
let price = this.safeFloat (order, 'price');
let amount = this.safeFloat (order, 'orderQty');
let filled = this.safeFloat (order, 'cumQty', 0.0);
let remaining = Math.max (amount - filled, 0.0);
let cost = undefined;
if (typeof price !== 'undefined')
if (typeof filled !== 'undefined')
cost = price * filled;
let result = {
'info': order,
'id': order['orderID'].toString (),
'timestamp': timestamp,
'datetime': iso8601,
'lastTradeTimestamp': undefined,
'symbol': symbol,
'type': order['ordType'].toLowerCase (),
'side': order['side'].toLowerCase (),
'price': price,
'amount': amount,
'cost': cost,
'filled': filled,
'remaining': remaining,
'status': status,
'fee': undefined,
};
return result;
}
async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
await this.loadMarkets ();
let market = this.market (symbol);
let request = {
'symbol': market['id'],
};
if (typeof since !== 'undefined')
request['startTime'] = this.iso8601 (since);
if (typeof limit !== 'undefined')
request['count'] = limit;
let response = await this.publicGetTrade (this.extend (request, params));
return this.parseTrades (response, market);
}
async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
await this.loadMarkets ();
let request = {
'symbol': this.marketId (symbol),
'side': this.capitalize (side),
'orderQty': amount,
'ordType': this.capitalize (type),
};
if (typeof price !== 'undefined')
request['price'] = price;
let response = await this.privatePostOrder (this.extend (request, params));
let order = this.parseOrder (response);
let id = order['id'];
this.orders[id] = order;
return this.extend ({ 'info': response }, order);
}
async editOrder (id, symbol, type, side, amount = undefined, price = undefined, params = {}) {
await this.loadMarkets ();
let request = {
'orderID': id,
};
if (typeof amount !== 'undefined')
request['orderQty'] = amount;
if (typeof price !== 'undefined')
request['price'] = price;
let response = await this.privatePutOrder (this.extend (request, params));
let order = this.parseOrder (response);
this.orders[order['id']] = order;
return this.extend ({ 'info': response }, order);
}
async cancelOrder (id, symbol = undefined, params = {}) {
await this.loadMarkets ();
let response = await this.privateDeleteOrder (this.extend ({ 'orderID': id }, params));
let order = response[0];
let error = this.safeString (order, 'error');
if (typeof error !== 'undefined')
if (error.indexOf ('Unable to cancel order due to existing state') >= 0)
throw new OrderNotFound (this.id + ' cancelOrder() failed: ' + error);
order = this.parseOrder (order);
this.orders[order['id']] = order;
return this.extend ({ 'info': response }, order);
}
isFiat (currency) {
if (currency === 'EUR')
return true;
if (currency === 'PLN')
return true;
return false;
}
async withdraw (currency, amount, address, tag = undefined, params = {}) {
this.checkAddress (address);
await this.loadMarkets ();
if (currency !== 'BTC')
throw new ExchangeError (this.id + ' supoprts BTC withdrawals only, other currencies coming soon...');
let request = {
'currency': 'XBt', // temporarily
'amount': amount,
'address': address,
// 'otpToken': '123456', // requires if two-factor auth (OTP) is enabled
// 'fee': 0.001, // bitcoin network fee
};
let response = await this.privatePostUserRequestWithdrawal (this.extend (request, params));
return {
'info': response,
'id': response['transactID'],
};
}
handleErrors (code, reason, url, method, headers, body) {
if (code === 429)
throw new DDoSProtection (this.id + ' ' + body);
if (code >= 400) {
if (body) {
if (body[0] === '{') {
let response = JSON.parse (body);
if ('error' in response) {
if ('message' in response['error']) {
let message = this.safeValue (response['error'], 'message');
if (typeof message !== 'undefined') {
if (message === 'Invalid API Key.')
throw new AuthenticationError (this.id + ' ' + this.json (response));
}
// stub code, need proper handling
throw new ExchangeError (this.id + ' ' + this.json (response));
}
}
}
}
}
}
nonce () {
return this.milliseconds ();
}
sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
let query = '/api' + '/' + this.version + '/' + path;
if (method !== 'PUT')
if (Object.keys (params).length)
query += '?' + this.urlencode (params);
let url = this.urls['api'] + query;
if (api === 'private') {
this.checkRequiredCredentials ();
let nonce = this.nonce ().toString ();
let auth = method + query + nonce;
if (method === 'POST' || method === 'PUT') {
if (Object.keys (params).length) {
body = this.json (params);
auth += body;
}
}
headers = {
'Content-Type': 'application/json',
'api-nonce': nonce,
'api-key': this.apiKey,
'api-signature': this.hmac (this.encode (auth), this.encode (this.secret)),
};
}
return { 'url': url, 'method': method, 'body': body, 'headers': headers };
}
};