@proton/ccxt
Version:
A JavaScript / TypeScript / Python / C# / PHP cryptocurrency trading library with support for 130+ exchanges
678 lines (674 loc) • 26.7 kB
JavaScript
'use strict';
var bitfinex$1 = require('../bitfinex.js');
var errors = require('../base/errors.js');
var Cache = require('../base/ws/Cache.js');
var Precise = require('../base/Precise.js');
var sha512 = require('../static_dependencies/noble-hashes/sha512.js');
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
class bitfinex extends bitfinex$1 {
describe() {
return this.deepExtend(super.describe(), {
'has': {
'ws': true,
'watchTicker': true,
'watchTickers': false,
'watchOrderBook': true,
'watchTrades': true,
'watchBalance': false,
'watchOHLCV': false, // missing on the exchange side in v1
},
'urls': {
'api': {
'ws': {
'public': 'wss://api-pub.bitfinex.com/ws/1',
'private': 'wss://api.bitfinex.com/ws/1',
},
},
},
'options': {
'watchOrderBook': {
'prec': 'P0',
'freq': 'F0',
},
'ordersLimit': 1000,
},
});
}
async subscribe(channel, symbol, params = {}) {
await this.loadMarkets();
const market = this.market(symbol);
const marketId = market['id'];
const url = this.urls['api']['ws']['public'];
const messageHash = channel + ':' + marketId;
// const channel = 'trades';
const request = {
'event': 'subscribe',
'channel': channel,
'symbol': marketId,
'messageHash': messageHash,
};
return await this.watch(url, messageHash, this.deepExtend(request, params), messageHash);
}
async watchTrades(symbol, since = undefined, limit = undefined, params = {}) {
/**
* @method
* @name bitfinex#watchTrades
* @description get the list of most recent trades for a particular symbol
* @param {string} symbol unified symbol of the market to fetch trades for
* @param {int|undefined} since timestamp in ms of the earliest trade to fetch
* @param {int|undefined} limit the maximum amount of trades to fetch
* @param {object} params extra parameters specific to the bitfinex api endpoint
* @returns {[object]} a list of [trade structures]{@link https://docs.ccxt.com/en/latest/manual.html?#public-trades}
*/
await this.loadMarkets();
symbol = this.symbol(symbol);
const trades = await this.subscribe('trades', symbol, params);
if (this.newUpdates) {
limit = trades.getLimit(symbol, limit);
}
return this.filterBySinceLimit(trades, since, limit, 'timestamp', true);
}
async watchTicker(symbol, params = {}) {
/**
* @method
* @name bitfinex#watchTicker
* @description watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
* @param {string} symbol unified symbol of the market to fetch the ticker for
* @param {object} params extra parameters specific to the bitfinex api endpoint
* @returns {object} a [ticker structure]{@link https://docs.ccxt.com/#/?id=ticker-structure}
*/
return await this.subscribe('ticker', symbol, params);
}
handleTrades(client, message, subscription) {
//
// initial snapshot
//
// [
// 2,
// [
// [ null, 1580565020, 9374.9, 0.005 ],
// [ null, 1580565004, 9374.9, 0.005 ],
// [ null, 1580565003, 9374.9, 0.005 ],
// ]
// ]
//
// when a trade does not have an id yet
//
// // channel id, update type, seq, time, price, amount
// [ 2, 'te', '28462857-BTCUSD', 1580565041, 9374.9, 0.005 ],
//
// when a trade already has an id
//
// // channel id, update type, seq, trade id, time, price, amount
// [ 2, 'tu', '28462857-BTCUSD', 413357662, 1580565041, 9374.9, 0.005 ]
//
const channel = this.safeValue(subscription, 'channel');
const marketId = this.safeString(subscription, 'pair');
const messageHash = channel + ':' + marketId;
const tradesLimit = this.safeInteger(this.options, 'tradesLimit', 1000);
const market = this.safeMarket(marketId);
const symbol = market['symbol'];
const data = this.safeValue(message, 1);
let stored = this.safeValue(this.trades, symbol);
if (stored === undefined) {
stored = new Cache.ArrayCache(tradesLimit);
this.trades[symbol] = stored;
}
if (Array.isArray(data)) {
const trades = this.parseTrades(data, market);
for (let i = 0; i < trades.length; i++) {
stored.append(trades[i]);
}
}
else {
const second = this.safeString(message, 1);
if (second !== 'tu') {
return;
}
const trade = this.parseTrade(message, market);
stored.append(trade);
}
client.resolve(stored, messageHash);
return message;
}
parseTrade(trade, market = undefined) {
//
// snapshot trade
//
// // null, time, price, amount
// [ null, 1580565020, 9374.9, 0.005 ],
//
// when a trade does not have an id yet
//
// // channel id, update type, seq, time, price, amount
// [ 2, 'te', '28462857-BTCUSD', 1580565041, 9374.9, 0.005 ],
//
// when a trade already has an id
//
// // channel id, update type, seq, trade id, time, price, amount
// [ 2, 'tu', '28462857-BTCUSD', 413357662, 1580565041, 9374.9, 0.005 ]
//
if (!Array.isArray(trade)) {
return super.parseTrade(trade, market);
}
const tradeLength = trade.length;
const event = this.safeString(trade, 1);
let id = undefined;
if (event === 'tu') {
id = this.safeString(trade, tradeLength - 4);
}
const timestamp = this.safeTimestamp(trade, tradeLength - 3);
const price = this.safeFloat(trade, tradeLength - 2);
let amount = this.safeFloat(trade, tradeLength - 1);
let side = undefined;
if (amount !== undefined) {
side = (amount > 0) ? 'buy' : 'sell';
amount = Math.abs(amount);
}
let cost = undefined;
if ((price !== undefined) && (amount !== undefined)) {
cost = price * amount;
}
const seq = this.safeString(trade, 2);
const parts = seq.split('-');
let marketId = this.safeString(parts, 1);
if (marketId !== undefined) {
marketId = marketId.replace('t', '');
}
const symbol = this.safeSymbol(marketId, market);
const takerOrMaker = undefined;
const orderId = undefined;
return {
'info': trade,
'timestamp': timestamp,
'datetime': this.iso8601(timestamp),
'symbol': symbol,
'id': id,
'order': orderId,
'type': undefined,
'takerOrMaker': takerOrMaker,
'side': side,
'price': price,
'amount': amount,
'cost': cost,
'fee': undefined,
};
}
handleTicker(client, message, subscription) {
//
// [
// 2, // 0 CHANNEL_ID integer Channel ID
// 236.62, // 1 BID float Price of last highest bid
// 9.0029, // 2 BID_SIZE float Size of the last highest bid
// 236.88, // 3 ASK float Price of last lowest ask
// 7.1138, // 4 ASK_SIZE float Size of the last lowest ask
// -1.02, // 5 DAILY_CHANGE float Amount that the last price has changed since yesterday
// 0, // 6 DAILY_CHANGE_PERC float Amount that the price has changed expressed in percentage terms
// 236.52, // 7 LAST_PRICE float Price of the last trade.
// 5191.36754297, // 8 VOLUME float Daily volume
// 250.01, // 9 HIGH float Daily high
// 220.05, // 10 LOW float Daily low
// ]
//
const timestamp = this.milliseconds();
const marketId = this.safeString(subscription, 'pair');
const symbol = this.safeSymbol(marketId);
const channel = 'ticker';
const messageHash = channel + ':' + marketId;
const last = this.safeString(message, 7);
const change = this.safeString(message, 5);
let open = undefined;
if ((last !== undefined) && (change !== undefined)) {
open = Precise["default"].stringSub(last, change);
}
const result = {
'symbol': symbol,
'timestamp': timestamp,
'datetime': this.iso8601(timestamp),
'high': this.safeFloat(message, 9),
'low': this.safeFloat(message, 10),
'bid': this.safeFloat(message, 1),
'bidVolume': undefined,
'ask': this.safeFloat(message, 3),
'askVolume': undefined,
'vwap': undefined,
'open': this.parseNumber(open),
'close': this.parseNumber(last),
'last': this.parseNumber(last),
'previousClose': undefined,
'change': this.parseNumber(change),
'percentage': this.safeFloat(message, 6),
'average': undefined,
'baseVolume': this.safeFloat(message, 8),
'quoteVolume': undefined,
'info': message,
};
this.tickers[symbol] = result;
client.resolve(result, messageHash);
}
async watchOrderBook(symbol, limit = undefined, params = {}) {
/**
* @method
* @name bitfinex#watchOrderBook
* @description watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
* @param {string} symbol unified symbol of the market to fetch the order book for
* @param {int|undefined} limit the maximum amount of order book entries to return
* @param {object} params extra parameters specific to the bitfinex api endpoint
* @returns {object} A dictionary of [order book structures]{@link https://docs.ccxt.com/#/?id=order-book-structure} indexed by market symbols
*/
if (limit !== undefined) {
if ((limit !== 25) && (limit !== 100)) {
throw new errors.ExchangeError(this.id + ' watchOrderBook limit argument must be undefined, 25 or 100');
}
}
const options = this.safeValue(this.options, 'watchOrderBook', {});
const prec = this.safeString(options, 'prec', 'P0');
const freq = this.safeString(options, 'freq', 'F0');
const request = {
// 'event': 'subscribe', // added in subscribe()
// 'channel': channel, // added in subscribe()
// 'symbol': marketId, // added in subscribe()
'prec': prec,
'freq': freq,
'len': limit, // string, number of price points, '25', '100', default = '25'
};
const orderbook = await this.subscribe('book', symbol, this.deepExtend(request, params));
return orderbook.limit();
}
handleOrderBook(client, message, subscription) {
//
// first message (snapshot)
//
// [
// 18691, // channel id
// [
// [ 7364.8, 10, 4.354802 ], // price, count, size > 0 = bid
// [ 7364.7, 1, 0.00288831 ],
// [ 7364.3, 12, 0.048 ],
// [ 7364.9, 3, -0.42028976 ], // price, count, size < 0 = ask
// [ 7365, 1, -0.25 ],
// [ 7365.5, 1, -0.00371937 ],
// ]
// ]
//
// subsequent updates
//
// [
// 30, // channel id
// 9339.9, // price
// 0, // count
// -1, // size > 0 = bid, size < 0 = ask
// ]
//
const marketId = this.safeString(subscription, 'pair');
const symbol = this.safeSymbol(marketId);
const channel = 'book';
const messageHash = channel + ':' + marketId;
const prec = this.safeString(subscription, 'prec', 'P0');
const isRaw = (prec === 'R0');
// if it is an initial snapshot
if (Array.isArray(message[1])) {
const limit = this.safeInteger(subscription, 'len');
if (isRaw) {
// raw order books
this.orderbooks[symbol] = this.indexedOrderBook({}, limit);
}
else {
// P0, P1, P2, P3, P4
this.orderbooks[symbol] = this.countedOrderBook({}, limit);
}
const orderbook = this.orderbooks[symbol];
if (isRaw) {
const deltas = message[1];
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i];
const id = this.safeString(delta, 0);
const price = this.safeFloat(delta, 1);
const size = (delta[2] < 0) ? -delta[2] : delta[2];
const side = (delta[2] < 0) ? 'asks' : 'bids';
const bookside = orderbook[side];
bookside.store(price, size, id);
}
}
else {
const deltas = message[1];
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i];
const size = (delta[2] < 0) ? -delta[2] : delta[2];
const side = (delta[2] < 0) ? 'asks' : 'bids';
const bookside = orderbook[side];
bookside.store(delta[0], size, delta[1]);
}
}
client.resolve(orderbook, messageHash);
}
else {
const orderbook = this.orderbooks[symbol];
if (isRaw) {
const id = this.safeString(message, 1);
const price = this.safeFloat(message, 2);
const size = (message[3] < 0) ? -message[3] : message[3];
const side = (message[3] < 0) ? 'asks' : 'bids';
const bookside = orderbook[side];
// price = 0 means that you have to remove the order from your book
const amount = (price > 0) ? size : 0;
bookside.store(price, amount, id);
}
else {
const size = (message[3] < 0) ? -message[3] : message[3];
const side = (message[3] < 0) ? 'asks' : 'bids';
const bookside = orderbook[side];
bookside.store(message[1], size, message[2]);
}
client.resolve(orderbook, messageHash);
}
}
handleHeartbeat(client, message) {
//
// every second (approx) if no other updates are sent
//
// { "event": "heartbeat" }
//
const event = this.safeString(message, 'event');
client.resolve(message, event);
}
handleSystemStatus(client, message) {
//
// todo: answer the question whether handleSystemStatus should be renamed
// and unified as handleStatus for any usage pattern that
// involves system status and maintenance updates
//
// {
// event: 'info',
// version: 2,
// serverId: 'e293377e-7bb7-427e-b28c-5db045b2c1d1',
// platform: { status: 1 }, // 1 for operative, 0 for maintenance
// }
//
return message;
}
handleSubscriptionStatus(client, message) {
//
// {
// event: 'subscribed',
// channel: 'book',
// chanId: 67473,
// symbol: 'tBTCUSD',
// prec: 'P0',
// freq: 'F0',
// len: '25',
// pair: 'BTCUSD'
// }
//
const channelId = this.safeString(message, 'chanId');
client.subscriptions[channelId] = message;
return message;
}
async authenticate(params = {}) {
const url = this.urls['api']['ws']['private'];
const client = this.client(url);
const future = client.future('authenticated');
const method = 'auth';
const authenticated = this.safeValue(client.subscriptions, method);
if (authenticated === undefined) {
const nonce = this.milliseconds();
const payload = 'AUTH' + nonce.toString();
const signature = this.hmac(this.encode(payload), this.encode(this.secret), sha512.sha384, 'hex');
const request = {
'apiKey': this.apiKey,
'authSig': signature,
'authNonce': nonce,
'authPayload': payload,
'event': method,
'filter': [
'trading',
'wallet',
],
};
this.spawn(this.watch, url, method, request, 1);
}
return await future;
}
handleAuthenticationMessage(client, message) {
const status = this.safeString(message, 'status');
if (status === 'OK') {
// we resolve the future here permanently so authentication only happens once
const future = this.safeValue(client.futures, 'authenticated');
future.resolve(true);
}
else {
const error = new errors.AuthenticationError(this.json(message));
client.reject(error, 'authenticated');
// allows further authentication attempts
const method = this.safeString(message, 'event');
if (method in client.subscriptions) {
delete client.subscriptions[method];
}
}
}
async watchOrder(id, symbol = undefined, params = {}) {
await this.loadMarkets();
const url = this.urls['api']['ws']['private'];
await this.authenticate();
return await this.watch(url, id, undefined, 1);
}
async watchOrders(symbol = undefined, since = undefined, limit = undefined, params = {}) {
/**
* @method
* @name bitfinex#watchOrders
* @description watches information on multiple orders made by the user
* @param {string|undefined} symbol unified market symbol of the market orders were made in
* @param {int|undefined} since the earliest time in ms to fetch orders for
* @param {int|undefined} limit the maximum number of orde structures to retrieve
* @param {object} params extra parameters specific to the bitfinex api endpoint
* @returns {[object]} a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure}
*/
await this.loadMarkets();
await this.authenticate();
if (symbol !== undefined) {
symbol = this.symbol(symbol);
}
const url = this.urls['api']['ws']['private'];
const orders = await this.watch(url, 'os', undefined, 1);
if (this.newUpdates) {
limit = orders.getLimit(symbol, limit);
}
return this.filterBySymbolSinceLimit(orders, symbol, since, limit);
}
handleOrders(client, message, subscription) {
//
// order snapshot
//
// [
// 0,
// 'os',
// [
// [
// 45287766631,
// 'ETHUST',
// -0.07,
// -0.07,
// 'EXCHANGE LIMIT',
// 'ACTIVE',
// 210,
// 0,
// '2020-05-16T13:17:46Z',
// 0,
// 0,
// 0
// ]
// ]
// ]
//
// order cancel
//
// [
// 0,
// 'oc',
// [
// 45287766631,
// 'ETHUST',
// -0.07,
// -0.07,
// 'EXCHANGE LIMIT',
// 'CANCELED',
// 210,
// 0,
// '2020-05-16T13:17:46Z',
// 0,
// 0,
// 0,
// ]
// ]
//
const data = this.safeValue(message, 2, []);
const messageType = this.safeString(message, 1);
if (messageType === 'os') {
for (let i = 0; i < data.length; i++) {
const value = data[i];
this.handleOrder(client, value);
}
}
else {
this.handleOrder(client, data);
}
if (this.orders !== undefined) {
client.resolve(this.orders, 'os');
}
}
parseWsOrderStatus(status) {
const statuses = {
'ACTIVE': 'open',
'CANCELED': 'canceled',
};
return this.safeString(statuses, status, status);
}
handleOrder(client, order) {
// [ 45287766631,
// 'ETHUST',
// -0.07,
// -0.07,
// 'EXCHANGE LIMIT',
// 'CANCELED',
// 210,
// 0,
// '2020-05-16T13:17:46Z',
// 0,
// 0,
// 0 ]
const id = this.safeString(order, 0);
const marketId = this.safeString(order, 1);
const symbol = this.safeSymbol(marketId);
let amount = this.safeString(order, 2);
let remaining = this.safeString(order, 3);
let side = 'buy';
if (Precise["default"].stringLt(amount, '0')) {
amount = Precise["default"].stringAbs(amount);
remaining = Precise["default"].stringAbs(remaining);
side = 'sell';
}
let type = this.safeString(order, 4);
if (type.indexOf('LIMIT') > -1) {
type = 'limit';
}
else if (type.indexOf('MARKET') > -1) {
type = 'market';
}
const status = this.parseWsOrderStatus(this.safeString(order, 5));
const price = this.safeString(order, 6);
const rawDatetime = this.safeString(order, 8);
const timestamp = this.parse8601(rawDatetime);
const parsed = this.safeOrder({
'info': order,
'id': id,
'clientOrderId': undefined,
'timestamp': timestamp,
'datetime': this.iso8601(timestamp),
'lastTradeTimestamp': undefined,
'symbol': symbol,
'type': type,
'side': side,
'price': price,
'stopPrice': undefined,
'triggerPrice': undefined,
'average': undefined,
'amount': amount,
'remaining': remaining,
'filled': undefined,
'status': status,
'fee': undefined,
'cost': undefined,
'trades': undefined,
});
if (this.orders === undefined) {
const limit = this.safeInteger(this.options, 'ordersLimit', 1000);
this.orders = new Cache.ArrayCacheBySymbolById(limit);
}
const orders = this.orders;
orders.append(parsed);
client.resolve(parsed, id);
return parsed;
}
handleMessage(client, message) {
if (Array.isArray(message)) {
const channelId = this.safeString(message, 0);
//
// [
// 1231,
// 'hb',
// ]
//
if (message[1] === 'hb') {
return message; // skip heartbeats within subscription channels for now
}
const subscription = this.safeValue(client.subscriptions, channelId, {});
const channel = this.safeString(subscription, 'channel');
const name = this.safeString(message, 1);
const methods = {
'book': this.handleOrderBook,
// 'ohlc': this.handleOHLCV,
'ticker': this.handleTicker,
'trades': this.handleTrades,
'os': this.handleOrders,
'on': this.handleOrders,
'oc': this.handleOrders,
};
const method = this.safeValue2(methods, channel, name);
if (method === undefined) {
return message;
}
else {
return method.call(this, client, message, subscription);
}
}
else {
// todo add bitfinex handleErrorMessage
//
// {
// event: 'info',
// version: 2,
// serverId: 'e293377e-7bb7-427e-b28c-5db045b2c1d1',
// platform: { status: 1 }, // 1 for operative, 0 for maintenance
// }
//
const event = this.safeString(message, 'event');
if (event !== undefined) {
const methods = {
'info': this.handleSystemStatus,
// 'book': 'handleOrderBook',
'subscribed': this.handleSubscriptionStatus,
'auth': this.handleAuthenticationMessage,
};
const method = this.safeValue(methods, event);
if (method === undefined) {
return message;
}
else {
return method.call(this, client, message);
}
}
}
}
}
module.exports = bitfinex;