From bf34611e2826f610ac8f044760aa64c9749eb424 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:27:51 +0000 Subject: [PATCH 01/14] orderbook update --- js/zb.js | 64 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/js/zb.js b/js/zb.js index 632c7e5930c9..950ecdd7aac8 100644 --- a/js/zb.js +++ b/js/zb.js @@ -19,7 +19,10 @@ module.exports = class zb extends ccxt.zb { }, 'urls': { 'api': { - 'ws': 'wss://api.zb.work/websocket', + 'ws': { + 'spot': 'wss://api.zb.work/websocket', + 'future': 'wss://fapi.zb.com/ws/public/v1', + }, }, }, 'options': { @@ -30,28 +33,21 @@ module.exports = class zb extends ccxt.zb { }); } - async watchPublic (name, symbol, method, params = {}) { - await this.loadMarkets (); - const market = this.market (symbol); - const messageHash = market['baseId'] + market['quoteId'] + '_' + name; - const url = this.urls['api']['ws']; - const request = { - 'event': 'addChannel', - 'channel': messageHash, - }; - const message = this.extend (request, params); + async watchPublic (url, messageHash, symbol, method, request, params = {}) { const subscription = { - 'name': name, + // 'name': name, 'symbol': symbol, - 'marketId': market['id'], 'messageHash': messageHash, 'method': method, }; + const message = this.extend (request, params); return await this.watch (url, messageHash, message, messageHash, subscription); } async watchTicker (symbol, params = {}) { - return await this.watchPublic ('ticker', symbol, this.handleTicker, params); + const market = this.market (symbol); + const messageHash = market['baseId'] + market['quoteId'] + '_' + 'ticker'; + return await this.watchPublic (market['type'], messageHash, symbol, this.handleTicker, params); } handleTicker (client, message, subscription) { @@ -132,23 +128,29 @@ module.exports = class zb extends ccxt.zb { } await this.loadMarkets (); const market = this.market (symbol); - const name = 'quick_depth'; - const messageHash = market['baseId'] + market['quoteId'] + '_' + name; - const url = this.urls['api']['ws'] + '/' + market['baseId']; - const request = { - 'event': 'addChannel', - 'channel': messageHash, - 'length': limit, - }; - const message = this.extend (request, params); - const subscription = { - 'name': name, - 'symbol': symbol, - 'marketId': market['id'], - 'messageHash': messageHash, - 'method': this.handleOrderBook, - }; - const orderbook = await this.watch (url, messageHash, message, messageHash, subscription); + const type = market['type']; + let request = undefined; + let messageHash = market['baseId'] + market['quoteId']; + let url = this.urls['api']['ws'][type]; + if (market['type'] === 'spot') { + url += '/' + market['baseId']; + const name = 'quick_depth'; + messageHash += '_' + name; + request = { + 'event': 'addChannel', + 'channel': messageHash, + 'length': limit, + }; + } else { + const name = 'Depth'; + messageHash += '.' + name; + request = { + 'event': 'addChannel', + 'channel': messageHash, + 'size': limit, + }; + } + const orderbook = await this.watchPublic (url, messageHash, this.handleOrderBook, request, params); return orderbook.limit (limit); } From 35543bcdc372f6c7f1353803c21f9b88b217404a Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Tue, 15 Mar 2022 17:23:13 +0000 Subject: [PATCH 02/14] orderbook update --- js/zb.js | 141 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 125 insertions(+), 16 deletions(-) diff --git a/js/zb.js b/js/zb.js index 950ecdd7aac8..f31cd18690c1 100644 --- a/js/zb.js +++ b/js/zb.js @@ -21,7 +21,7 @@ module.exports = class zb extends ccxt.zb { 'api': { 'ws': { 'spot': 'wss://api.zb.work/websocket', - 'future': 'wss://fapi.zb.com/ws/public/v1', + 'contract': 'wss://fapi.zb.com/ws/public/v1', }, }, }, @@ -128,14 +128,14 @@ module.exports = class zb extends ccxt.zb { } await this.loadMarkets (); const market = this.market (symbol); - const type = market['type']; + const type = market['spot'] ? 'spot' : 'contract'; let request = undefined; - let messageHash = market['baseId'] + market['quoteId']; + let messageHash = undefined; let url = this.urls['api']['ws'][type]; if (market['type'] === 'spot') { url += '/' + market['baseId']; const name = 'quick_depth'; - messageHash += '_' + name; + messageHash = market['baseId'] + market['quoteId'] + '_' + name; request = { 'event': 'addChannel', 'channel': messageHash, @@ -143,18 +143,26 @@ module.exports = class zb extends ccxt.zb { }; } else { const name = 'Depth'; - messageHash += '.' + name; + messageHash = market['id'] + '.' + name; request = { - 'event': 'addChannel', + 'action': 'subscribe', 'channel': messageHash, 'size': limit, }; } - const orderbook = await this.watchPublic (url, messageHash, this.handleOrderBook, request, params); + const subscription = { + 'symbol': symbol, + 'messageHash': messageHash, + 'limit': limit, + 'method': this.handleOrderBook, + }; + const message = this.extend (request, params); + const orderbook = await this.watch (url, messageHash, message, messageHash, subscription); return orderbook.limit (limit); } handleOrderBook (client, message, subscription) { + // spot snapshot // // { // lastTime: 1624524640066, @@ -185,19 +193,91 @@ module.exports = class zb extends ccxt.zb { // showMarket: 'btcusdt' // } // + // contract snapshot + // { + // channel: 'BTC_USDT.Depth', + // type: 'Whole', + // data: { + // asks: [ [Array], [Array], [Array], [Array], [Array] ], + // bids: [ [Array], [Array], [Array], [Array], [Array] ], + // time: '1647359998198' + // } + // } + // + // contract deltas + // { + // channel: 'BTC_USDT.Depth', + // data: { + // bids: [ [Array], [Array], [Array], [Array] ], + // asks: [ [Array], [Array], [Array] ], + // time: '1647360038079' + // } + // } + // + const type = this.safeString2 (message, 'type', 'dataType'); const channel = this.safeString (message, 'channel'); - const limit = this.safeInteger (subscription, 'limit'); const symbol = this.safeString (subscription, 'symbol'); let orderbook = this.safeValue (this.orderbooks, symbol); - if (orderbook === undefined) { - orderbook = this.orderBook ({}, limit); - this.orderbooks[symbol] = orderbook; + if (type !== undefined) { + // handle orderbook snapshot + const timestamp = this.safeInteger2 (message, 'lastTime', 'time'); + const asksKey = (type === 'whole') ? 'asks' : 'listUp'; + const bidsKey = (type === 'whole') ? 'bids' : 'listDown'; + const snapshot = this.parseOrderBook (message, symbol, timestamp, asksKey, bidsKey); + if (!(symbol in this.orderbooks)) { + const defaultLimit = this.safeInteger (this.options, 'watchOrderBookLimit', 1000); + const limit = this.safeInteger (subscription, 'limit', defaultLimit); + orderbook = this.orderBook (snapshot, limit); + this.orderbooks[symbol] = orderbook; + } else { + orderbook = this.orderbooks[symbol]; + orderbook.reset (snapshot); + } + orderbook['symbol'] = symbol; + if (type === 'whole') { + // unroll the accumulated deltas for contract markets + const messages = orderbook.cache; + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + this.handleOrderBookMessage (client, message, orderbook); + } + } + client.resolve (orderbook, channel); + } else { + // handle deltas for contract markets + const nonce = this.safeInteger (orderbook, 'nonce'); + if (nonce === undefined) { + orderbook.cache.push (message); + } else { + this.handleOrderBookMessage (client, message, orderbook); + client.resolve (orderbook, channel); + } + } + } + + handleOrderBookMessage (client, message, orderbook) { + // + // { + // channel: 'BTC_USDT.Depth', + // data: { + // bids: [ [Array], [Array], [Array], [Array] ], + // asks: [ [Array], [Array], [Array] ], + // time: '1647360038079' + // } + // } + // + const data = this.safeValue (message, 'data', {}); + const seqNum = this.safeInteger (data, 'time'); + if (seqNum > orderbook['nonce']) { + const asks = this.safeValue (data, 'asks', []); + const bids = this.safeValue (data, 'bids', []); + this.handleDeltas (orderbook['asks'], asks); + this.handleDeltas (orderbook['bids'], bids); + orderbook['nonce'] = seqNum; + orderbook['timestamp'] = seqNum; + orderbook['datetime'] = this.iso8601 (seqNum); } - const timestamp = this.safeInteger (message, 'lastTime'); - const parsed = this.parseOrderBook (message, symbol, timestamp, 'listDown', 'listUp'); - orderbook.reset (parsed); - orderbook['symbol'] = symbol; - client.resolve (orderbook, channel); + return orderbook; } handleMessage (client, message) { @@ -238,6 +318,26 @@ module.exports = class zb extends ccxt.zb { // channel: 'btcusdt_trades' // } // + // contract snapshot + // { + // channel: 'BTC_USDT.Depth', + // type: 'Whole', + // data: { + // asks: [ [Array], [Array], [Array], [Array], [Array] ], + // bids: [ [Array], [Array], [Array], [Array], [Array] ], + // time: '1647359998198' + // } + // } + // contract deltas update + // { + // channel: 'BTC_USDT.Depth', + // data: { + // bids: [ [Array], [Array], [Array], [Array] ], + // asks: [ [Array], [Array], [Array] ], + // time: '1647360038079' + // } + // } + // const dataType = this.safeString (message, 'dataType'); if (dataType !== undefined) { const channel = this.safeString (message, 'channel'); @@ -250,5 +350,14 @@ module.exports = class zb extends ccxt.zb { } return message; } + const type = this.safeString (message, 'type'); + if (type === 'Whole') { + this.handleOrderBookSnapshot (client, message); + return; + } + const channel = this.safeString (message, 'channel'); + if (channel.indexOf ('Depth') !== -1) { + this.handleOrderBookMessage (client, message); + } } }; From 0d55050fa61378baf2518b8a320cd96e26d8253a Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Tue, 15 Mar 2022 18:16:38 +0000 Subject: [PATCH 03/14] add watchOHLCV --- js/zb.js | 134 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 26 deletions(-) diff --git a/js/zb.js b/js/zb.js index f31cd18690c1..7a367adb062a 100644 --- a/js/zb.js +++ b/js/zb.js @@ -3,8 +3,9 @@ // --------------------------------------------------------------------------- const ccxt = require ('ccxt'); -const { ExchangeError } = require ('ccxt/js/base/errors'); -const { ArrayCache } = require ('./base/Cache'); +const { ExchangeError, NotSupported } = require ('ccxt/js/base/errors'); +const time = require ('ccxt/js/base/functions/time'); +const { ArrayCache, ArrayCacheByTimestamp } = require ('./base/Cache'); // --------------------------------------------------------------------------- @@ -81,6 +82,82 @@ module.exports = class zb extends ccxt.zb { return message; } + async watchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) { + await this.loadMarkets (); + const market = this.market (symbol); + if (market['spot']) { + throw NotSupported (this.id + ' watchOHLCV() supports contract markets only'); + } + if (limit === undefined) { + limit = 20; + } + const interval = this.timeframes[timeframe]; + const messageHash = market['id'] + '.KLine' + '_' + interval; + const url = this.urls['api']['ws']['contract']; + const request = { + 'action': 'subscribe', + 'channel': messageHash, + 'size': limit, + }; + const subscription = { + 'symbol': symbol, + 'messageHash': messageHash, + 'limit': limit, + 'timeframe': timeframe, + 'method': this.handleOHLCV, + }; + const ohlcv = await this.watch (url, messageHash, this.extend (request, params), messageHash, subscription); + if (this.newUpdates) { + limit = ohlcv.getLimit (symbol, limit); + } + return this.filterBySinceLimit (ohlcv, since, limit, 0, true); + } + + handleOHLCV (client, message) { + // + // snapshot update + // { + // channel: 'BTC_USDT.KLine_1m', + // type: 'Whole', + // data: [ + // [ 48543.77, 48543.77, 48542.82, 48542.82, 0.43, 1640227260 ], + // [ 48542.81, 48542.81, 48529.89, 48529.89, 1.202, 1640227320 ], + // [ 48529.95, 48529.99, 48529.85, 48529.9, 4.226, 1640227380 ], + // [ 48529.96, 48529.99, 48525.11, 48525.11, 8.858, 1640227440 ], + // [ 48525.05, 48525.05, 48464.17, 48476.63, 32.772, 1640227500 ], + // [ 48475.62, 48485.65, 48475.12, 48479.36, 20.04, 1640227560 ], + // ] + // } + // partial update + // { + // channel: 'BTC_USDT.KLine_1m', + // data: [ + // [ 39095.45, 45339.48, 36923.58, 39204.94, 1215304.988, 1645920000 ] + // ] + // } + // + const data = this.safeValue (message, 'data', []); + const channel = this.safeString (message, 'channel'); + const subscription = this.safeValue (client.subscriptions, channel); + const symbol = this.safeString (subscription, 'symbol'); + const market = this.market (symbol); + const timeframe = this.safeString (subscription, 'symbol'); + for (let i = 0; i < data.length; i++) { + const candle = data[i]; + const parsed = this.parseOHLCV (candle, market); + this.ohlcvs[symbol] = this.safeValue (this.ohlcvs, symbol, {}); + let stored = this.safeValue (this.ohlcvs[symbol], timeframe); + if (stored === undefined) { + const limit = this.safeInteger (this.options, 'OHLCVLimit', 1000); + stored = new ArrayCacheByTimestamp (limit); + this.ohlcvs[symbol][timeframe] = stored; + } + stored.append (parsed); + client.resolve (stored, channel); + } + return message; + } + async watchTrades (symbol, since = undefined, limit = undefined, params = {}) { const trades = await this.watchPublic ('trades', symbol, this.handleTrades, params); if (this.newUpdates) { @@ -220,10 +297,12 @@ module.exports = class zb extends ccxt.zb { let orderbook = this.safeValue (this.orderbooks, symbol); if (type !== undefined) { // handle orderbook snapshot - const timestamp = this.safeInteger2 (message, 'lastTime', 'time'); - const asksKey = (type === 'whole') ? 'asks' : 'listUp'; - const bidsKey = (type === 'whole') ? 'bids' : 'listDown'; - const snapshot = this.parseOrderBook (message, symbol, timestamp, asksKey, bidsKey); + const isContractSnapshot = (type === 'Whole'); + const data = isContractSnapshot ? this.safeValue (message, 'data') : message; + const timestamp = this.safeInteger2 (data, 'lastTime', 'time'); + const asksKey = isContractSnapshot ? 'asks' : 'listUp'; + const bidsKey = isContractSnapshot ? 'bids' : 'listDown'; + const snapshot = this.parseOrderBook (data, symbol, timestamp, asksKey, bidsKey); if (!(symbol in this.orderbooks)) { const defaultLimit = this.safeInteger (this.options, 'watchOrderBookLimit', 1000); const limit = this.safeInteger (subscription, 'limit', defaultLimit); @@ -234,7 +313,8 @@ module.exports = class zb extends ccxt.zb { orderbook.reset (snapshot); } orderbook['symbol'] = symbol; - if (type === 'whole') { + orderbook['nonce'] = timestamp; + if (isContractSnapshot) { // unroll the accumulated deltas for contract markets const messages = orderbook.cache; for (let i = 0; i < messages.length; i++) { @@ -280,6 +360,18 @@ module.exports = class zb extends ccxt.zb { return orderbook; } + handleDelta (bookside, delta) { + const price = this.safeFloat (delta, 0); + const amount = this.safeFloat (delta, 1); + bookside.store (price, amount); + } + + handleDeltas (bookside, deltas) { + for (let i = 0; i < deltas.length; i++) { + this.handleDelta (bookside, deltas[i]); + } + } + handleMessage (client, message) { // // @@ -338,26 +430,16 @@ module.exports = class zb extends ccxt.zb { // } // } // - const dataType = this.safeString (message, 'dataType'); - if (dataType !== undefined) { - const channel = this.safeString (message, 'channel'); - const subscription = this.safeValue (client.subscriptions, channel); - if (subscription !== undefined) { - const method = this.safeValue (subscription, 'method'); - if (method !== undefined) { - return method.call (this, client, message, subscription); - } - } - return message; - } - const type = this.safeString (message, 'type'); - if (type === 'Whole') { - this.handleOrderBookSnapshot (client, message); - return; - } + // const dataType = this.safeString2 (message, 'dataType', 'type'); + // if (dataType !== undefined) { const channel = this.safeString (message, 'channel'); - if (channel.indexOf ('Depth') !== -1) { - this.handleOrderBookMessage (client, message); + const subscription = this.safeValue (client.subscriptions, channel); + if (subscription !== undefined) { + const method = this.safeValue (subscription, 'method'); + if (method !== undefined) { + return method.call (this, client, message, subscription); + } } + return message; } }; From 43f65458ccd6a2a63d7bd04f5c8592902c960304 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Wed, 16 Mar 2022 14:11:46 +0000 Subject: [PATCH 04/14] orderbook update --- js/zb.js | 100 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 36 deletions(-) diff --git a/js/zb.js b/js/zb.js index 7a367adb062a..abbda7b0f8ec 100644 --- a/js/zb.js +++ b/js/zb.js @@ -4,7 +4,6 @@ const ccxt = require ('ccxt'); const { ExchangeError, NotSupported } = require ('ccxt/js/base/errors'); -const time = require ('ccxt/js/base/functions/time'); const { ArrayCache, ArrayCacheByTimestamp } = require ('./base/Cache'); // --------------------------------------------------------------------------- @@ -17,6 +16,7 @@ module.exports = class zb extends ccxt.zb { 'watchOrderBook': true, 'watchTicker': true, 'watchTrades': true, + 'watchOHLCV': true, }, 'urls': { 'api': { @@ -34,24 +34,47 @@ module.exports = class zb extends ccxt.zb { }); } - async watchPublic (url, messageHash, symbol, method, request, params = {}) { + async watchPublic (messageHash, symbol, method, params = {}) { + await this.loadMarkets (); + const market = this.market (symbol); + const type = market['spot'] ? 'spot' : 'contract'; + const url = this.urls['api']['ws'][type]; + let request = undefined; + if (type === 'spot') { + request = { + 'event': 'addChannel', + 'channel': messageHash, + }; + } else { + request = { + 'action': 'subscribe', + 'channel': messageHash, + }; + } + const message = this.extend (request, params); const subscription = { - // 'name': name, 'symbol': symbol, 'messageHash': messageHash, 'method': method, }; - const message = this.extend (request, params); return await this.watch (url, messageHash, message, messageHash, subscription); } async watchTicker (symbol, params = {}) { const market = this.market (symbol); - const messageHash = market['baseId'] + market['quoteId'] + '_' + 'ticker'; - return await this.watchPublic (market['type'], messageHash, symbol, this.handleTicker, params); + let messageHash = undefined; + const type = market['type']; + if (type === 'spot') { + messageHash = market['baseId'] + market['quoteId'] + '_' + 'ticker'; + } else { + messageHash = market['id'] + '.' + 'Ticker'; + } + return await this.watchPublic (messageHash, symbol, this.handleTicker, params); } handleTicker (client, message, subscription) { + // + // spot ticker // // { // date: '1624398991255', @@ -70,6 +93,21 @@ module.exports = class zb extends ccxt.zb { // channel: 'btcusdt_ticker' // } // + // contract ticker + // { + // channel: 'BTC_USDT.Ticker', + // data: [ + // 38568.36, + // 39958.75, + // 38100, + // 39211.78, + // 61695.496, + // 1.67, + // 1647369457, + // 285916.615048 + // ] + // } + // const symbol = this.safeString (subscription, 'symbol'); const channel = this.safeString (message, 'channel'); const market = this.market (symbol); @@ -86,10 +124,10 @@ module.exports = class zb extends ccxt.zb { await this.loadMarkets (); const market = this.market (symbol); if (market['spot']) { - throw NotSupported (this.id + ' watchOHLCV() supports contract markets only'); + throw new NotSupported (this.id + ' watchOHLCV() supports contract markets only'); } - if (limit === undefined) { - limit = 20; + if ((limit === undefined) || (limit > 1440)) { + limit = 100; } const interval = this.timeframes[timeframe]; const messageHash = market['id'] + '.KLine' + '_' + interval; @@ -291,6 +329,14 @@ module.exports = class zb extends ccxt.zb { // } // } // + // For contract markets zb will: + // 1: send snapshot + // 2: send deltas + // 3: repeat + // So we have a guarentee that deltas + // are always updated and arrive after + // the snapshot + // const type = this.safeString2 (message, 'type', 'dataType'); const channel = this.safeString (message, 'channel'); const symbol = this.safeString (subscription, 'symbol'); @@ -313,25 +359,10 @@ module.exports = class zb extends ccxt.zb { orderbook.reset (snapshot); } orderbook['symbol'] = symbol; - orderbook['nonce'] = timestamp; - if (isContractSnapshot) { - // unroll the accumulated deltas for contract markets - const messages = orderbook.cache; - for (let i = 0; i < messages.length; i++) { - const message = messages[i]; - this.handleOrderBookMessage (client, message, orderbook); - } - } client.resolve (orderbook, channel); } else { - // handle deltas for contract markets - const nonce = this.safeInteger (orderbook, 'nonce'); - if (nonce === undefined) { - orderbook.cache.push (message); - } else { - this.handleOrderBookMessage (client, message, orderbook); - client.resolve (orderbook, channel); - } + this.handleOrderBookMessage (client, message, orderbook); + client.resolve (orderbook, channel); } } @@ -347,16 +378,13 @@ module.exports = class zb extends ccxt.zb { // } // const data = this.safeValue (message, 'data', {}); - const seqNum = this.safeInteger (data, 'time'); - if (seqNum > orderbook['nonce']) { - const asks = this.safeValue (data, 'asks', []); - const bids = this.safeValue (data, 'bids', []); - this.handleDeltas (orderbook['asks'], asks); - this.handleDeltas (orderbook['bids'], bids); - orderbook['nonce'] = seqNum; - orderbook['timestamp'] = seqNum; - orderbook['datetime'] = this.iso8601 (seqNum); - } + const timestamp = this.safeInteger (data, 'time'); + const asks = this.safeValue (data, 'asks', []); + const bids = this.safeValue (data, 'bids', []); + this.handleDeltas (orderbook['asks'], asks); + this.handleDeltas (orderbook['bids'], bids); + orderbook['timestamp'] = timestamp; + orderbook['datetime'] = this.iso8601 (timestamp); return orderbook; } From e7eb2bf6caa6550a0b505e860de4bd0ab3a7ac29 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Wed, 16 Mar 2022 14:38:13 +0000 Subject: [PATCH 05/14] parseWsTicker and error message --- js/zb.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/js/zb.js b/js/zb.js index abbda7b0f8ec..41235849b178 100644 --- a/js/zb.js +++ b/js/zb.js @@ -3,7 +3,7 @@ // --------------------------------------------------------------------------- const ccxt = require ('ccxt'); -const { ExchangeError, NotSupported } = require ('ccxt/js/base/errors'); +const { ExchangeError, NotSupported, AuthenticationError } = require ('ccxt/js/base/errors'); const { ArrayCache, ArrayCacheByTimestamp } = require ('./base/Cache'); // --------------------------------------------------------------------------- @@ -72,6 +72,48 @@ module.exports = class zb extends ccxt.zb { return await this.watchPublic (messageHash, symbol, this.handleTicker, params); } + parseWsTicker (ticker, market = undefined) { + // + // contract ticker + // { + // data: [ + // 38568.36, // open + // 39958.75, // high + // 38100, // low + // 39211.78, // last + // 61695.496, // volume 24h + // 1.67, // change + // 1647369457, // time + // 285916.615048 + // ] + // } + // + const timestamp = this.safeInteger (ticker, 6); + const last = this.safeString (ticker, 3); + return this.safeTicker ({ + 'symbol': this.safeSymbol (undefined, market), + 'timestamp': timestamp, + 'datetime': undefined, + 'high': this.safeString (ticker, 1), + 'low': this.safeString (ticker, 2), + 'bid': undefined, + 'bidVolume': undefined, + 'ask': undefined, + 'askVolume': undefined, + 'vwap': undefined, + 'open': this.safeString (ticker, 0), + 'close': last, + 'last': last, + 'previousClose': undefined, + 'change': undefined, + 'percentage': this.safeString (ticker, 5), + 'average': undefined, + 'baseVolume': this.safeString (ticker, 4), + 'quoteVolume': undefined, + 'info': ticker, + }, market, false); + } + handleTicker (client, message, subscription) { // // spot ticker @@ -111,9 +153,15 @@ module.exports = class zb extends ccxt.zb { const symbol = this.safeString (subscription, 'symbol'); const channel = this.safeString (message, 'channel'); const market = this.market (symbol); - const data = this.safeValue (message, 'ticker'); - data['date'] = this.safeValue (message, 'date'); - const ticker = this.parseTicker (data, market); + let data = this.safeValue (message, 'ticker'); + let ticker = undefined; + if (data === undefined) { + data = this.safeValue (message, 'data', []); + ticker = this.parseWsTicker (data, market); + } else { + data['date'] = this.safeValue (message, 'date'); + ticker = this.parseTicker (data, market); + } ticker['symbol'] = symbol; this.tickers[symbol] = ticker; client.resolve (ticker, channel); @@ -235,8 +283,8 @@ module.exports = class zb extends ccxt.zb { async watchOrderBook (symbol, limit = undefined, params = {}) { if (limit !== undefined) { - if ((limit !== 5) && (limit !== 10) && (limit !== 20)) { - throw new ExchangeError (this.id + ' watchOrderBook limit argument must be undefined, 5, 10 or 20'); + if ((limit !== 5) && (limit !== 10)) { + throw new ExchangeError (this.id + ' watchOrderBook limit argument must be undefined, 5, or 10'); } } else { limit = 5; // default @@ -332,7 +380,7 @@ module.exports = class zb extends ccxt.zb { // For contract markets zb will: // 1: send snapshot // 2: send deltas - // 3: repeat + // 3: repeat 1-2 // So we have a guarentee that deltas // are always updated and arrive after // the snapshot @@ -459,7 +507,6 @@ module.exports = class zb extends ccxt.zb { // } // // const dataType = this.safeString2 (message, 'dataType', 'type'); - // if (dataType !== undefined) { const channel = this.safeString (message, 'channel'); const subscription = this.safeValue (client.subscriptions, channel); if (subscription !== undefined) { @@ -470,4 +517,32 @@ module.exports = class zb extends ccxt.zb { } return message; } + + handleErrorMessage (client, message) { + // + // { errorCode: 10020, errorMsg: "action param can't be empty" } + // { errorCode: 10015, errorMsg: '无效的签名(1002)' } + // + const errorCode = this.safeString (message, 'errorCode'); + try { + if (errorCode !== undefined) { + const feedback = this.id + ' ' + this.json (message); + this.throwExactlyMatchedException (this.exceptions['exact'], errorCode, feedback); + const messageString = this.safeValue (message, 'message'); + if (messageString !== undefined) { + this.throwBroadlyMatchedException (this.exceptions['broad'], messageString, feedback); + } + } + } catch (e) { + if (e instanceof AuthenticationError) { + client.reject (e, 'authenticated'); + const method = 'login'; + if (method in client.subscriptions) { + delete client.subscriptions[method]; + } + return false; + } + } + return message; + } }; From 4c246121260d3f845ea9404b04964b5ff68a6d26 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Wed, 16 Mar 2022 15:22:26 +0000 Subject: [PATCH 06/14] refactor and handleTrades --- js/zb.js | 146 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 44 deletions(-) diff --git a/js/zb.js b/js/zb.js index 41235849b178..31ae18e20a7c 100644 --- a/js/zb.js +++ b/js/zb.js @@ -34,22 +34,28 @@ module.exports = class zb extends ccxt.zb { }); } - async watchPublic (messageHash, symbol, method, params = {}) { + async watchPublic (url, messageHash, symbol, method, limit = undefined, params = {}) { await this.loadMarkets (); const market = this.market (symbol); const type = market['spot'] ? 'spot' : 'contract'; - const url = this.urls['api']['ws'][type]; let request = undefined; + const isLimitSet = limit !== undefined; if (type === 'spot') { request = { 'event': 'addChannel', 'channel': messageHash, }; + if (isLimitSet) { + request['length'] = limit; + } } else { request = { 'action': 'subscribe', 'channel': messageHash, }; + if (isLimitSet) { + request['size'] = limit; + } } const message = this.extend (request, params); const subscription = { @@ -57,6 +63,9 @@ module.exports = class zb extends ccxt.zb { 'messageHash': messageHash, 'method': method, }; + if (isLimitSet) { + subscription['limit'] = limit; + } return await this.watch (url, messageHash, message, messageHash, subscription); } @@ -180,19 +189,7 @@ module.exports = class zb extends ccxt.zb { const interval = this.timeframes[timeframe]; const messageHash = market['id'] + '.KLine' + '_' + interval; const url = this.urls['api']['ws']['contract']; - const request = { - 'action': 'subscribe', - 'channel': messageHash, - 'size': limit, - }; - const subscription = { - 'symbol': symbol, - 'messageHash': messageHash, - 'limit': limit, - 'timeframe': timeframe, - 'method': this.handleOHLCV, - }; - const ohlcv = await this.watch (url, messageHash, this.extend (request, params), messageHash, subscription); + const ohlcv = await this.watchPublic (url, messageHash, symbol, this.handleOHLCV, limit, params); if (this.newUpdates) { limit = ohlcv.getLimit (symbol, limit); } @@ -223,11 +220,14 @@ module.exports = class zb extends ccxt.zb { // } // const data = this.safeValue (message, 'data', []); - const channel = this.safeString (message, 'channel'); + const channel = this.safeString (message, 'channel', ''); + const parts = channel.split ('_'); + const partsLength = parts.length; + const interval = this.safeString (parts, partsLength - 1); + const timeframe = this.findTimeframe (interval); const subscription = this.safeValue (client.subscriptions, channel); const symbol = this.safeString (subscription, 'symbol'); const market = this.market (symbol); - const timeframe = this.safeString (subscription, 'symbol'); for (let i = 0; i < data.length; i++) { const candle = data[i]; const parsed = this.parseOHLCV (candle, market); @@ -245,7 +245,16 @@ module.exports = class zb extends ccxt.zb { } async watchTrades (symbol, since = undefined, limit = undefined, params = {}) { - const trades = await this.watchPublic ('trades', symbol, this.handleTrades, params); + const market = this.market (symbol); + let messageHash = undefined; + const type = market['spot'] ? 'spot' : 'contract'; + if (type === 'spot') { + messageHash = market['baseId'] + market['quoteId'] + '_' + 'ticker'; + } else { + messageHash = market['id'] + '.' + 'Trade'; + } + const url = this.urls['api']['ws'][type]; + const trades = await await this.watchPublic (url, messageHash, symbol, this.handleTrades, limit, params); if (this.newUpdates) { limit = trades.getLimit (symbol, limit); } @@ -253,6 +262,32 @@ module.exports = class zb extends ccxt.zb { } handleTrades (client, message, subscription) { + // contract trades + // { + // "channel":"BTC_USDT.Trade", + // "type":"Whole", + // "data":[ + // [ + // 40768.07, + // 0.01, + // 1, + // 1647442757 + // ], + // [ + // 40792.22, + // 0.334, + // -1, + // 1647442765 + // ], + // [ + // 40789.77, + // 0.14, + // 1, + // 1647442766 + // ] + // ] + // } + // spot trades // // { // data: [ @@ -268,7 +303,19 @@ module.exports = class zb extends ccxt.zb { const symbol = this.safeString (subscription, 'symbol'); const market = this.market (symbol); const data = this.safeValue (message, 'data'); - const trades = this.parseTrades (data, market); + const type = this.safeString (message, 'type'); + let trades = []; + if (type === 'Whole') { + // contract trades + for (let i = 0; i < data.length; i++) { + const trade = data[i]; + const parsed = this.parseWsTrade (trade, market); + trades.push (parsed); + } + } else { + // spot trades + trades = this.parseTrades (data, market); + } let array = this.safeValue (this.trades, symbol); if (array === undefined) { const limit = this.safeInteger (this.options, 'tradesLimit', 1000); @@ -292,38 +339,50 @@ module.exports = class zb extends ccxt.zb { await this.loadMarkets (); const market = this.market (symbol); const type = market['spot'] ? 'spot' : 'contract'; - let request = undefined; let messageHash = undefined; let url = this.urls['api']['ws'][type]; - if (market['type'] === 'spot') { + if (type === 'spot') { url += '/' + market['baseId']; - const name = 'quick_depth'; - messageHash = market['baseId'] + market['quoteId'] + '_' + name; - request = { - 'event': 'addChannel', - 'channel': messageHash, - 'length': limit, - }; + messageHash = market['baseId'] + market['quoteId'] + '_' + 'quick_depth'; } else { - const name = 'Depth'; - messageHash = market['id'] + '.' + name; - request = { - 'action': 'subscribe', - 'channel': messageHash, - 'size': limit, - }; + messageHash = market['id'] + '.' + 'Depth'; } - const subscription = { - 'symbol': symbol, - 'messageHash': messageHash, - 'limit': limit, - 'method': this.handleOrderBook, - }; - const message = this.extend (request, params); - const orderbook = await this.watch (url, messageHash, message, messageHash, subscription); + const orderbook = await this.watchPublic (url, messageHash, symbol, this.handleOrderBook, limit, params); return orderbook.limit (limit); } + parseWsTrade (trade, market = undefined) { + // + // [ + // 40768.07, // price + // 0.01, // quantity + // 1, // buy or -1 sell + // 1647442757 // time + // ], + // + const timestamp = this.safeTimestamp (trade, 3); + const price = this.safeString (trade, 0); + const amount = this.safeString (trade, 1); + market = this.safeMarket (undefined, market); + const sideNumber = this.safeInteger (trade, 2); + const side = (sideNumber === 1) ? 'buy' : 'sell'; + return this.safeTrade ({ + 'id': undefined, + 'timestamp': timestamp, + 'datetime': this.iso8601 (timestamp), + 'symbol': market['symbol'], + 'order': undefined, + 'type': 'limit', + 'side': side, + 'takerOrMaker': undefined, + 'price': price, + 'amount': amount, + 'cost': undefined, + 'fee': undefined, + 'info': trade, + }, market); + } + handleOrderBook (client, message, subscription) { // spot snapshot // @@ -506,7 +565,6 @@ module.exports = class zb extends ccxt.zb { // } // } // - // const dataType = this.safeString2 (message, 'dataType', 'type'); const channel = this.safeString (message, 'channel'); const subscription = this.safeValue (client.subscriptions, channel); if (subscription !== undefined) { From 6afdf47278c8688d01c50a6ed2c5639733749f6b Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Wed, 16 Mar 2022 15:45:05 +0000 Subject: [PATCH 07/14] last fixes --- js/zb.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/js/zb.js b/js/zb.js index 31ae18e20a7c..03c43811a837 100644 --- a/js/zb.js +++ b/js/zb.js @@ -70,15 +70,17 @@ module.exports = class zb extends ccxt.zb { } async watchTicker (symbol, params = {}) { + await this.loadMarkets (); const market = this.market (symbol); let messageHash = undefined; - const type = market['type']; + const type = market['spot'] ? 'spot' : 'contract'; if (type === 'spot') { messageHash = market['baseId'] + market['quoteId'] + '_' + 'ticker'; } else { messageHash = market['id'] + '.' + 'Ticker'; } - return await this.watchPublic (messageHash, symbol, this.handleTicker, params); + const url = this.urls['api']['ws'][type]; + return await this.watchPublic (url, messageHash, symbol, this.handleTicker, undefined, params); } parseWsTicker (ticker, market = undefined) { @@ -196,7 +198,7 @@ module.exports = class zb extends ccxt.zb { return this.filterBySinceLimit (ohlcv, since, limit, 0, true); } - handleOHLCV (client, message) { + handleOHLCV (client, message, subscription) { // // snapshot update // { @@ -225,7 +227,6 @@ module.exports = class zb extends ccxt.zb { const partsLength = parts.length; const interval = this.safeString (parts, partsLength - 1); const timeframe = this.findTimeframe (interval); - const subscription = this.safeValue (client.subscriptions, channel); const symbol = this.safeString (subscription, 'symbol'); const market = this.market (symbol); for (let i = 0; i < data.length; i++) { @@ -245,16 +246,17 @@ module.exports = class zb extends ccxt.zb { } async watchTrades (symbol, since = undefined, limit = undefined, params = {}) { + await this.loadMarkets (); const market = this.market (symbol); let messageHash = undefined; const type = market['spot'] ? 'spot' : 'contract'; if (type === 'spot') { - messageHash = market['baseId'] + market['quoteId'] + '_' + 'ticker'; + messageHash = market['baseId'] + market['quoteId'] + '_' + 'trades'; } else { messageHash = market['id'] + '.' + 'Trade'; } const url = this.urls['api']['ws'][type]; - const trades = await await this.watchPublic (url, messageHash, symbol, this.handleTrades, limit, params); + const trades = await this.watchPublic (url, messageHash, symbol, this.handleTrades, limit, params); if (this.newUpdates) { limit = trades.getLimit (symbol, limit); } @@ -372,7 +374,7 @@ module.exports = class zb extends ccxt.zb { 'datetime': this.iso8601 (timestamp), 'symbol': market['symbol'], 'order': undefined, - 'type': 'limit', + 'type': undefined, 'side': side, 'takerOrMaker': undefined, 'price': price, @@ -546,6 +548,7 @@ module.exports = class zb extends ccxt.zb { // } // // contract snapshot + // // { // channel: 'BTC_USDT.Depth', // type: 'Whole', @@ -555,6 +558,7 @@ module.exports = class zb extends ccxt.zb { // time: '1647359998198' // } // } + // // contract deltas update // { // channel: 'BTC_USDT.Depth', From 652791f8c74fb47fd6d06bdc3c98515e3f2ba6b4 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Thu, 17 Mar 2022 09:13:24 +0000 Subject: [PATCH 08/14] orderbook fix --- js/zb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/zb.js b/js/zb.js index 03c43811a837..a53cd599bfd5 100644 --- a/js/zb.js +++ b/js/zb.js @@ -457,7 +457,7 @@ module.exports = class zb extends ccxt.zb { const timestamp = this.safeInteger2 (data, 'lastTime', 'time'); const asksKey = isContractSnapshot ? 'asks' : 'listUp'; const bidsKey = isContractSnapshot ? 'bids' : 'listDown'; - const snapshot = this.parseOrderBook (data, symbol, timestamp, asksKey, bidsKey); + const snapshot = this.parseOrderBook (data, symbol, timestamp, bidsKey, asksKey); if (!(symbol in this.orderbooks)) { const defaultLimit = this.safeInteger (this.options, 'watchOrderBookLimit', 1000); const limit = this.safeInteger (subscription, 'limit', defaultLimit); From 3d051aa287bcb7d73b29fa2f1f1bf76187f6dfb7 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Thu, 17 Mar 2022 10:34:39 +0000 Subject: [PATCH 09/14] fix transpiling --- js/zb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/zb.js b/js/zb.js index a53cd599bfd5..e7812784ca0c 100644 --- a/js/zb.js +++ b/js/zb.js @@ -306,7 +306,7 @@ module.exports = class zb extends ccxt.zb { const market = this.market (symbol); const data = this.safeValue (message, 'data'); const type = this.safeString (message, 'type'); - let trades = []; + let trades = undefined; if (type === 'Whole') { // contract trades for (let i = 0; i < data.length; i++) { From b2ee000ba87509d9463fe4b7906fe09dab0262ea Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Mon, 20 Jun 2022 15:32:43 +0100 Subject: [PATCH 10/14] ignore ohlcv --- js/test/Exchange/test.watchOHLCV.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/js/test/Exchange/test.watchOHLCV.js b/js/test/Exchange/test.watchOHLCV.js index b906ed9cca8e..857cf8d52026 100644 --- a/js/test/Exchange/test.watchOHLCV.js +++ b/js/test/Exchange/test.watchOHLCV.js @@ -21,6 +21,7 @@ module.exports = async (exchange, symbol) => { 'dsx', 'idex2', // rinkeby testnet, trades too rare 'bitvavo', + 'zb' ] if (skippedExchanges.includes (exchange.id)) { @@ -57,8 +58,10 @@ module.exports = async (exchange, symbol) => { if (i > 0) { const previous = response[i - 1] if (current[0] && previous[0]) { - assert (current[0] >= previous[0], - 'OHLCV timestamp ordering is wrong at candle ' + i.toString () + ' ' + current[0].toString () + ' < ' + previous[0].toString ()) + assert ( + current[0] >= previous[0], + 'OHLCV timestamp ordering is wrong at candle ' + i.toString () + ' ' + current[0].toString () + ' < ' + previous[0].toString () + ) } } } From a779d33a7ce80ad2fdffa673eca82129284f6bab Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Tue, 21 Jun 2022 15:18:03 +0100 Subject: [PATCH 11/14] ignore zb --- python/ccxtpro/test/exchange/test_watch_ohlcv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ccxtpro/test/exchange/test_watch_ohlcv.py b/python/ccxtpro/test/exchange/test_watch_ohlcv.py index 375d91924f3d..837efb774087 100644 --- a/python/ccxtpro/test/exchange/test_watch_ohlcv.py +++ b/python/ccxtpro/test/exchange/test_watch_ohlcv.py @@ -11,6 +11,7 @@ async def test_watch_ohlcv(exchange, symbol): skipped_exchanges = [ 'dsx', 'idex2', # rinkeby testnet, trades too rare + 'zb' ] if exchange.id in skipped_exchanges: From 82f22f658b06663ea9e9b1a8e91db7d359db44c9 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:21:23 +0000 Subject: [PATCH 12/14] add --- js/{ => pro}/zb.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename js/{ => pro}/zb.js (100%) diff --git a/js/zb.js b/js/pro/zb.js similarity index 100% rename from js/zb.js rename to js/pro/zb.js From 8d1dcca8b761c8ae82a05bb39237f0e187211b06 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:27:47 +0000 Subject: [PATCH 13/14] pro implementation --- js/pro/zb.js | 504 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 414 insertions(+), 90 deletions(-) diff --git a/js/pro/zb.js b/js/pro/zb.js index 8b5adf172214..a5379bd29225 100644 --- a/js/pro/zb.js +++ b/js/pro/zb.js @@ -3,7 +3,7 @@ // --------------------------------------------------------------------------- const zbRest = require ('../zb.js'); -const { ExchangeError } = require ('../base/errors'); +const { ExchangeError, AuthenticationError } = require ('../base/errors'); const { ArrayCache } = require ('./base/Cache'); // --------------------------------------------------------------------------- @@ -16,6 +16,7 @@ module.exports = class zb extends zbRest { 'watchOrderBook': true, 'watchTicker': true, 'watchTrades': true, + 'watchOHLCV': true, }, 'urls': { 'api': { @@ -30,40 +31,100 @@ module.exports = class zb extends zbRest { }); } - async watchPublic (name, symbol, method, params = {}) { + async watchPublic (url, messageHash, symbol, method, limit = undefined, params = {}) { await this.loadMarkets (); const market = this.market (symbol); - symbol = market['symbol']; - const messageHash = market['baseId'] + market['quoteId'] + '_' + name; - const url = this.implodeHostname (this.urls['api']['ws']); - const request = { - 'event': 'addChannel', - 'channel': messageHash, - }; + const type = market['spot'] ? 'spot' : 'contract'; + let request = undefined; + const isLimitSet = limit !== undefined; + if (type === 'spot') { + request = { + 'event': 'addChannel', + 'channel': messageHash, + }; + if (isLimitSet) { + request['length'] = limit; + } + } else { + request = { + 'action': 'subscribe', + 'channel': messageHash, + }; + if (isLimitSet) { + request['size'] = limit; + } + } const message = this.extend (request, params); const subscription = { - 'name': name, 'symbol': symbol, - 'marketId': market['id'], 'messageHash': messageHash, 'method': method, }; + if (isLimitSet) { + subscription['limit'] = limit; + } return await this.watch (url, messageHash, message, messageHash, subscription); } async watchTicker (symbol, params = {}) { - /** - * @method - * @name zb#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 zb api endpoint - * @returns {object} a [ticker structure]{@link https://docs.ccxt.com/en/latest/manual.html#ticker-structure} - */ - return await this.watchPublic ('ticker', symbol, this.handleTicker, params); + await this.loadMarkets (); + const market = this.market (symbol); + let messageHash = undefined; + const type = market['spot'] ? 'spot' : 'contract'; + if (type === 'spot') { + messageHash = market['baseId'] + market['quoteId'] + '_' + 'ticker'; + } else { + messageHash = market['id'] + '.' + 'Ticker'; + } + const url = this.implodeHostname (this.urls['api']['ws'][type]); + return await this.watchPublic (url, messageHash, symbol, this.handleTicker, undefined, params); + } + + parseWsTicker (ticker, market = undefined) { + // + // contract ticker + // { + // data: [ + // 38568.36, // open + // 39958.75, // high + // 38100, // low + // 39211.78, // last + // 61695.496, // volume 24h + // 1.67, // change + // 1647369457, // time + // 285916.615048 + // ] + // } + // + const timestamp = this.safeInteger (ticker, 6); + const last = this.safeString (ticker, 3); + return this.safeTicker ({ + 'symbol': this.safeSymbol (undefined, market), + 'timestamp': timestamp, + 'datetime': undefined, + 'high': this.safeString (ticker, 1), + 'low': this.safeString (ticker, 2), + 'bid': undefined, + 'bidVolume': undefined, + 'ask': undefined, + 'askVolume': undefined, + 'vwap': undefined, + 'open': this.safeString (ticker, 0), + 'close': last, + 'last': last, + 'previousClose': undefined, + 'change': undefined, + 'percentage': this.safeString (ticker, 5), + 'average': undefined, + 'baseVolume': this.safeString (ticker, 4), + 'quoteVolume': undefined, + 'info': ticker, + }, market, false); } handleTicker (client, message, subscription) { + // + // spot ticker // // { // date: '1624398991255', @@ -82,32 +143,117 @@ module.exports = class zb extends zbRest { // channel: 'btcusdt_ticker' // } // + // contract ticker + // { + // channel: 'BTC_USDT.Ticker', + // data: [ + // 38568.36, + // 39958.75, + // 38100, + // 39211.78, + // 61695.496, + // 1.67, + // 1647369457, + // 285916.615048 + // ] + // } + // const symbol = this.safeString (subscription, 'symbol'); const channel = this.safeString (message, 'channel'); const market = this.market (symbol); - const data = this.safeValue (message, 'ticker'); - data['date'] = this.safeValue (message, 'date'); - const ticker = this.parseTicker (data, market); + let data = this.safeValue (message, 'ticker'); + let ticker = undefined; + if (data === undefined) { + data = this.safeValue (message, 'data', []); + ticker = this.parseWsTicker (data, market); + } else { + data['date'] = this.safeValue (message, 'date'); + ticker = this.parseTicker (data, market); + } ticker['symbol'] = symbol; this.tickers[symbol] = ticker; client.resolve (ticker, channel); return message; } + async watchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) { + await this.loadMarkets (); + const market = this.market (symbol); + if (market['spot']) { + throw new NotSupported (this.id + ' watchOHLCV() supports contract markets only'); + } + if ((limit === undefined) || (limit > 1440)) { + limit = 100; + } + const interval = this.timeframes[timeframe]; + const messageHash = market['id'] + '.KLine' + '_' + interval; + const url = this.implodeHostname (this.urls['api']['ws']['contract']); + const ohlcv = await this.watchPublic (url, messageHash, symbol, this.handleOHLCV, limit, params); + if (this.newUpdates) { + limit = ohlcv.getLimit (symbol, limit); + } + return this.filterBySinceLimit (ohlcv, since, limit, 0, true); + } + + handleOHLCV (client, message, subscription) { + // + // snapshot update + // { + // channel: 'BTC_USDT.KLine_1m', + // type: 'Whole', + // data: [ + // [ 48543.77, 48543.77, 48542.82, 48542.82, 0.43, 1640227260 ], + // [ 48542.81, 48542.81, 48529.89, 48529.89, 1.202, 1640227320 ], + // [ 48529.95, 48529.99, 48529.85, 48529.9, 4.226, 1640227380 ], + // [ 48529.96, 48529.99, 48525.11, 48525.11, 8.858, 1640227440 ], + // [ 48525.05, 48525.05, 48464.17, 48476.63, 32.772, 1640227500 ], + // [ 48475.62, 48485.65, 48475.12, 48479.36, 20.04, 1640227560 ], + // ] + // } + // partial update + // { + // channel: 'BTC_USDT.KLine_1m', + // data: [ + // [ 39095.45, 45339.48, 36923.58, 39204.94, 1215304.988, 1645920000 ] + // ] + // } + // + const data = this.safeValue (message, 'data', []); + const channel = this.safeString (message, 'channel', ''); + const parts = channel.split ('_'); + const partsLength = parts.length; + const interval = this.safeString (parts, partsLength - 1); + const timeframe = this.findTimeframe (interval); + const symbol = this.safeString (subscription, 'symbol'); + const market = this.market (symbol); + for (let i = 0; i < data.length; i++) { + const candle = data[i]; + const parsed = this.parseOHLCV (candle, market); + this.ohlcvs[symbol] = this.safeValue (this.ohlcvs, symbol, {}); + let stored = this.safeValue (this.ohlcvs[symbol], timeframe); + if (stored === undefined) { + const limit = this.safeInteger (this.options, 'OHLCVLimit', 1000); + stored = new ArrayCacheByTimestamp (limit); + this.ohlcvs[symbol][timeframe] = stored; + } + stored.append (parsed); + client.resolve (stored, channel); + } + return message; + } + async watchTrades (symbol, since = undefined, limit = undefined, params = {}) { - /** - * @method - * @name zb#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 zb 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.watchPublic ('trades', symbol, this.handleTrades, params); + const market = this.market (symbol); + let messageHash = undefined; + const type = market['spot'] ? 'spot' : 'contract'; + if (type === 'spot') { + messageHash = market['baseId'] + market['quoteId'] + '_' + 'trades'; + } else { + messageHash = market['id'] + '.' + 'Trade'; + } + const url = this.implodeHostname (this.urls['api']['ws'][type]); + const trades = await this.watchPublic (url, messageHash, symbol, this.handleTrades, limit, params); if (this.newUpdates) { limit = trades.getLimit (symbol, limit); } @@ -115,6 +261,32 @@ module.exports = class zb extends zbRest { } handleTrades (client, message, subscription) { + // contract trades + // { + // "channel":"BTC_USDT.Trade", + // "type":"Whole", + // "data":[ + // [ + // 40768.07, + // 0.01, + // 1, + // 1647442757 + // ], + // [ + // 40792.22, + // 0.334, + // -1, + // 1647442765 + // ], + // [ + // 40789.77, + // 0.14, + // 1, + // 1647442766 + // ] + // ] + // } + // spot trades // // { // data: [ @@ -130,60 +302,88 @@ module.exports = class zb extends zbRest { const symbol = this.safeString (subscription, 'symbol'); const market = this.market (symbol); const data = this.safeValue (message, 'data'); - const trades = this.parseTrades (data, market); - let tradesArray = this.safeValue (this.trades, symbol); - if (tradesArray === undefined) { + const type = this.safeString (message, 'type'); + let trades = undefined; + if (type === 'Whole') { + // contract trades + for (let i = 0; i < data.length; i++) { + const trade = data[i]; + const parsed = this.parseWsTrade (trade, market); + trades.push (parsed); + } + } else { + // spot trades + trades = this.parseTrades (data, market); + } + let array = this.safeValue (this.trades, symbol); + if (array === undefined) { const limit = this.safeInteger (this.options, 'tradesLimit', 1000); - tradesArray = new ArrayCache (limit); + array = new ArrayCache (limit); } for (let i = 0; i < trades.length; i++) { - tradesArray.append (trades[i]); + array.append (trades[i]); } - this.trades[symbol] = tradesArray; - client.resolve (tradesArray, channel); + this.trades[symbol] = array; + client.resolve (array, channel); } async watchOrderBook (symbol, limit = undefined, params = {}) { - /** - * @method - * @name zb#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 zb api endpoint - * @returns {object} A dictionary of [order book structures]{@link https://docs.ccxt.com/en/latest/manual.html#order-book-structure} indexed by market symbols - */ if (limit !== undefined) { - if ((limit !== 5) && (limit !== 10) && (limit !== 20)) { - throw new ExchangeError (this.id + ' watchOrderBook limit argument must be undefined, 5, 10 or 20'); + if ((limit !== 5) && (limit !== 10)) { + throw new ExchangeError (this.id + ' watchOrderBook limit argument must be undefined, 5, or 10'); } } else { limit = 5; // default } await this.loadMarkets (); const market = this.market (symbol); - symbol = market['symbol']; - const name = 'quick_depth'; - const messageHash = market['baseId'] + market['quoteId'] + '_' + name; - const url = this.implodeHostname (this.urls['api']['ws']) + '/' + market['baseId']; - const request = { - 'event': 'addChannel', - 'channel': messageHash, - 'length': limit, - }; - const message = this.extend (request, params); - const subscription = { - 'name': name, - 'symbol': symbol, - 'marketId': market['id'], - 'messageHash': messageHash, - 'method': this.handleOrderBook, - }; - const orderbook = await this.watch (url, messageHash, message, messageHash, subscription); - return orderbook.limit (); + const type = market['spot'] ? 'spot' : 'contract'; + let messageHash = undefined; + let url = this.implodeHostname (this.urls['api']['ws'][type]); + if (type === 'spot') { + url += '/' + market['baseId']; + messageHash = market['baseId'] + market['quoteId'] + '_' + 'quick_depth'; + } else { + messageHash = market['id'] + '.' + 'Depth'; + } + const orderbook = await this.watchPublic (url, messageHash, symbol, this.handleOrderBook, limit, params); + return orderbook.limit (limit); + } + + parseWsTrade (trade, market = undefined) { + // + // [ + // 40768.07, // price + // 0.01, // quantity + // 1, // buy or -1 sell + // 1647442757 // time + // ], + // + const timestamp = this.safeTimestamp (trade, 3); + const price = this.safeString (trade, 0); + const amount = this.safeString (trade, 1); + market = this.safeMarket (undefined, market); + const sideNumber = this.safeInteger (trade, 2); + const side = (sideNumber === 1) ? 'buy' : 'sell'; + return this.safeTrade ({ + 'id': undefined, + 'timestamp': timestamp, + 'datetime': this.iso8601 (timestamp), + 'symbol': market['symbol'], + 'order': undefined, + 'type': undefined, + 'side': side, + 'takerOrMaker': undefined, + 'price': price, + 'amount': amount, + 'cost': undefined, + 'fee': undefined, + 'info': trade, + }, market); } handleOrderBook (client, message, subscription) { + // spot snapshot // // { // lastTime: 1624524640066, @@ -214,19 +414,96 @@ module.exports = class zb extends zbRest { // showMarket: 'btcusdt' // } // + // contract snapshot + // { + // channel: 'BTC_USDT.Depth', + // type: 'Whole', + // data: { + // asks: [ [Array], [Array], [Array], [Array], [Array] ], + // bids: [ [Array], [Array], [Array], [Array], [Array] ], + // time: '1647359998198' + // } + // } + // + // contract deltas + // { + // channel: 'BTC_USDT.Depth', + // data: { + // bids: [ [Array], [Array], [Array], [Array] ], + // asks: [ [Array], [Array], [Array] ], + // time: '1647360038079' + // } + // } + // + // For contract markets zb will: + // 1: send snapshot + // 2: send deltas + // 3: repeat 1-2 + // So we have a guarentee that deltas + // are always updated and arrive after + // the snapshot + // + const type = this.safeString2 (message, 'type', 'dataType'); const channel = this.safeString (message, 'channel'); - const limit = this.safeInteger (subscription, 'limit'); const symbol = this.safeString (subscription, 'symbol'); let orderbook = this.safeValue (this.orderbooks, symbol); - if (orderbook === undefined) { - orderbook = this.orderBook ({}, limit); - this.orderbooks[symbol] = orderbook; + if (type !== undefined) { + // handle orderbook snapshot + const isContractSnapshot = (type === 'Whole'); + const data = isContractSnapshot ? this.safeValue (message, 'data') : message; + const timestamp = this.safeInteger2 (data, 'lastTime', 'time'); + const asksKey = isContractSnapshot ? 'asks' : 'listUp'; + const bidsKey = isContractSnapshot ? 'bids' : 'listDown'; + const snapshot = this.parseOrderBook (data, symbol, timestamp, bidsKey, asksKey); + if (!(symbol in this.orderbooks)) { + const defaultLimit = this.safeInteger (this.options, 'watchOrderBookLimit', 1000); + const limit = this.safeInteger (subscription, 'limit', defaultLimit); + orderbook = this.orderBook (snapshot, limit); + this.orderbooks[symbol] = orderbook; + } else { + orderbook = this.orderbooks[symbol]; + orderbook.reset (snapshot); + } + orderbook['symbol'] = symbol; + client.resolve (orderbook, channel); + } else { + this.handleOrderBookMessage (client, message, orderbook); + client.resolve (orderbook, channel); + } + } + + handleOrderBookMessage (client, message, orderbook) { + // + // { + // channel: 'BTC_USDT.Depth', + // data: { + // bids: [ [Array], [Array], [Array], [Array] ], + // asks: [ [Array], [Array], [Array] ], + // time: '1647360038079' + // } + // } + // + const data = this.safeValue (message, 'data', {}); + const timestamp = this.safeInteger (data, 'time'); + const asks = this.safeValue (data, 'asks', []); + const bids = this.safeValue (data, 'bids', []); + this.handleDeltas (orderbook['asks'], asks); + this.handleDeltas (orderbook['bids'], bids); + orderbook['timestamp'] = timestamp; + orderbook['datetime'] = this.iso8601 (timestamp); + return orderbook; + } + + handleDelta (bookside, delta) { + const price = this.safeFloat (delta, 0); + const amount = this.safeFloat (delta, 1); + bookside.store (price, amount); + } + + handleDeltas (bookside, deltas) { + for (let i = 0; i < deltas.length; i++) { + this.handleDelta (bookside, deltas[i]); } - const timestamp = this.safeInteger (message, 'lastTime'); - const parsed = this.parseOrderBook (message, symbol, timestamp, 'listDown', 'listUp'); - orderbook.reset (parsed); - orderbook['symbol'] = symbol; - client.resolve (orderbook, channel); } handleMessage (client, message) { @@ -267,17 +544,64 @@ module.exports = class zb extends zbRest { // channel: 'btcusdt_trades' // } // - const dataType = this.safeString (message, 'dataType'); - if (dataType !== undefined) { - const channel = this.safeString (message, 'channel'); - const subscription = this.safeValue (client.subscriptions, channel); - if (subscription !== undefined) { - const method = this.safeValue (subscription, 'method'); - if (method !== undefined) { - return method.call (this, client, message, subscription); + // contract snapshot + // + // { + // channel: 'BTC_USDT.Depth', + // type: 'Whole', + // data: { + // asks: [ [Array], [Array], [Array], [Array], [Array] ], + // bids: [ [Array], [Array], [Array], [Array], [Array] ], + // time: '1647359998198' + // } + // } + // + // contract deltas update + // { + // channel: 'BTC_USDT.Depth', + // data: { + // bids: [ [Array], [Array], [Array], [Array] ], + // asks: [ [Array], [Array], [Array] ], + // time: '1647360038079' + // } + // } + // + const channel = this.safeString (message, 'channel'); + const subscription = this.safeValue (client.subscriptions, channel); + if (subscription !== undefined) { + const method = this.safeValue (subscription, 'method'); + if (method !== undefined) { + return method.call (this, client, message, subscription); + } + } + return message; + } + + handleErrorMessage (client, message) { + // + // { errorCode: 10020, errorMsg: "action param can't be empty" } + // { errorCode: 10015, errorMsg: '无效的签名(1002)' } + // + const errorCode = this.safeString (message, 'errorCode'); + try { + if (errorCode !== undefined) { + const feedback = this.id + ' ' + this.json (message); + this.throwExactlyMatchedException (this.exceptions['exact'], errorCode, feedback); + const messageString = this.safeValue (message, 'message'); + if (messageString !== undefined) { + this.throwBroadlyMatchedException (this.exceptions['broad'], messageString, feedback); } } - return message; + } catch (e) { + if (e instanceof AuthenticationError) { + client.reject (e, 'authenticated'); + const method = 'login'; + if (method in client.subscriptions) { + delete client.subscriptions[method]; + } + return false; + } } + return message; } }; From e1b77ba7ccf345b3a01d0d6ee52d5577549a755d Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:38:51 +0000 Subject: [PATCH 14/14] small fix --- js/pro/zb.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/js/pro/zb.js b/js/pro/zb.js index a5379bd29225..9e14f4d780a2 100644 --- a/js/pro/zb.js +++ b/js/pro/zb.js @@ -3,8 +3,8 @@ // --------------------------------------------------------------------------- const zbRest = require ('../zb.js'); -const { ExchangeError, AuthenticationError } = require ('../base/errors'); -const { ArrayCache } = require ('./base/Cache'); +const { ExchangeError, AuthenticationError, NotSupported } = require ('../base/errors'); +const { ArrayCache, ArrayCacheByTimestamp } = require ('./base/Cache'); // --------------------------------------------------------------------------- @@ -20,7 +20,10 @@ module.exports = class zb extends zbRest { }, 'urls': { 'api': { - 'ws': 'wss://api.{hostname}/websocket', + 'ws': { + 'spot': 'wss://api.{hostname}/websocket', + 'contract': 'wss://fapi.{hostname}/ws/public/v1', + }, }, }, 'options': { @@ -303,7 +306,7 @@ module.exports = class zb extends zbRest { const market = this.market (symbol); const data = this.safeValue (message, 'data'); const type = this.safeString (message, 'type'); - let trades = undefined; + let trades = []; if (type === 'Whole') { // contract trades for (let i = 0; i < data.length; i++) { @@ -347,7 +350,7 @@ module.exports = class zb extends zbRest { messageHash = market['id'] + '.' + 'Depth'; } const orderbook = await this.watchPublic (url, messageHash, symbol, this.handleOrderBook, limit, params); - return orderbook.limit (limit); + return orderbook.limit (); } parseWsTrade (trade, market = undefined) {