From bbe44cd8ed169b373320590b3a44b4238ea4e244 Mon Sep 17 00:00:00 2001 From: develoQ Date: Sat, 9 Jan 2021 14:05:32 +0900 Subject: [PATCH 1/4] Add exchange bitbank --- .github/workflows/node.yml | 1 + README.md | 1 + __tests__/exchanges/bitbank-client.spec.js | 121 ++++++++++++ src/exchanges/bitbank-client.js | 212 +++++++++++++++++++++ src/index.js | 2 + 5 files changed, 337 insertions(+) create mode 100644 __tests__/exchanges/bitbank-client.spec.js create mode 100644 src/exchanges/bitbank-client.js diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index a0e1bd1e..1770a26a 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -46,6 +46,7 @@ jobs: - binance-futures-usdtm - binanceje - binanceus + - bitbank - bitfinex - bitflyer - bithumb diff --git a/README.md b/README.md index 64658268..9b5d8bb4 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ binance.subscribeLevel2Snapshots(market); | FTX US | 1 | FtxUs | ✓ | ✓ | - | - | ✓\* | - | - | | Gate.io | 3 | Gateio | ✓ | ✓ | - | - | ✓\* | - | - | | Gemini | 1 | Gemini | - | ✓ | - | - | ✓\* | - | - | +| bitbank | 1 | bitbank | ✓ | ✓ | - | ✓ | ✓ | - | - | | HitBTC | 2 | HitBTC | ✓ | ✓ | ✓ | - | ✓\* | - | - | | Huobi Global | 1 | Huobi | ✓ | ✓ | ✓ | ✓ | - | - | - | | Huobi Global Futures | 1 | HuobiFutures | ✓ | ✓ | ✓ | ✓ | ✓\* | - | - | diff --git a/__tests__/exchanges/bitbank-client.spec.js b/__tests__/exchanges/bitbank-client.spec.js new file mode 100644 index 00000000..dbec4e82 --- /dev/null +++ b/__tests__/exchanges/bitbank-client.spec.js @@ -0,0 +1,121 @@ +const { testClient } = require("../test-runner"); +const bitbankClient = require("../../src/exchanges/bitbank-client"); + +testClient({ + clientFactory: () => new bitbankClient(), + clientName: "bitbankClient", + exchangeName: "bitbank", + markets: [ + { + id: "btc_jpy", + base: "BTC", + quote: "JPY", + }, + { + id: "xrp_jpy", + base: "XRP", + quote: "JPY", + }, + { + id: "xrp_btc", + base: "XRP", + quote: "BTC", + }, + { + id: "ltc_jpy", + base: "LTC", + quote: "JPY", + }, + { + id: "ltc_btc", + base: "LTC", + quote: "BTC", + }, + { + id: "eth_jpy", + base: "XRP", + quote: "JPY", + }, + { + id: "eth_btc", + base: "XRP", + quote: "BTC", + }, + { + id: "mona_jpy", + base: "MONA", + quote: "JPY", + }, + { + id: "mona_btc", + base: "MONA", + quote: "BTC", + }, + { + id: "bcc_jpy", + base: "BCC", + quote: "JPY", + }, + { + id: "bcc_btc", + base: "BCC", + quote: "BTC", + }, + { + id: "xlm_jpy", + base: "XLM", + quote: "JPY", + }, + { + id: "xlm_btc", + base: "XLM", + quote: "BTC", + }, + ], + + testConnectEvents: false, + testDisconnectEvents: false, + testReconnectionEvents: false, + testCloseEvents: false, + + hasTickers: true, + hasTrades: true, + hasCandles: false, + hasLevel2Snapshots: true, + hasLevel2Updates: true, + hasLevel3Snapshots: false, + hasLevel3Updates: false, + + ticker: { + hasTimestamp: true, + hasLast: true, + hasOpen: false, + hasHigh: true, + hasLow: true, + hasVolume: true, + hasQuoteVolume: false, + hasChange: false, + hasChangePercent: false, + hasBid: true, + hasBidVolume: false, + hasAsk: true, + hasAskVolume: false, + }, + + trade: { + hasTradeId: true, + }, + + l2snapshot: { + hasTimestampMs: true, + hasSequenceId: false, + hasCount: false, + }, + + l2update: { + hasSnapshot: false, + hasTimestampMs: true, + hasSequenceId: false, + hasCount: false, + }, +}); diff --git a/src/exchanges/bitbank-client.js b/src/exchanges/bitbank-client.js new file mode 100644 index 00000000..a41ad8e7 --- /dev/null +++ b/src/exchanges/bitbank-client.js @@ -0,0 +1,212 @@ +const BasicClient = require("../basic-client"); +const Ticker = require("../ticker"); +const Trade = require("../trade"); +const Level2Point = require("../level2-point"); +const Level2Snapshot = require("../level2-snapshot"); +const Level2Update = require("../level2-update"); + +class bitbankClient extends BasicClient { + constructor({ + wssPath = "wss://stream.bitbank.cc/socket.io/?EIO=3&transport=websocket", + watcherMs, + } = {}) { + super(wssPath, "bitbank", undefined, watcherMs); + + this.hasTickers = true; + this.hasTrades = true; + this.hasLevel2Snapshots = true; + this.hasLevel2Updates = true; + } + + _sendSubTicker(remote_id) { + this._wss.send(`42["join-room", "ticker_${remote_id}"]`); + } + + _sendUnsubTicker(remote_id) { + this._wss.send(`42["left-room", "ticker_${remote_id}"]`); + } + + _sendSubTrades(remote_id) { + this._wss.send(`42["join-room", "transactions_${remote_id}"]`); + } + + _sendUnsubTrades(remote_id) { + this._wss.send(`42["left-room", "transactions_${remote_id}"]`); + } + + _sendSubLevel2Snapshots(remote_id) { + this._wss.send(`42["join-room", "depth_whole_${remote_id}"]`); + } + + _sendUnsubLevel2Snapshots(remote_id) { + this._wss.send(`42["left-room", "depth_whole_${remote_id}"]`); + } + + _sendSubLevel2Updates(remote_id) { + this._wss.send(`42["join-room", "depth_diff_${remote_id}"]`); + } + + _sendUnsubLevel2Updates(remote_id) { + this._wss.send(`42["left-room", "depth_diff_${remote_id}"]`); + } + + _subscribe(market, map, sendFn) { + let remote_id = market.id; + this._connect(remote_id); + if (!map.has(remote_id)) { + map.set(remote_id, market); + + // perform the subscription if we're connected + // and if not, then we'll reply on the _onConnected event + // to send the signal to our server! + if (this._wss && this._wss.isConnected) { + sendFn(remote_id, market); + } + return true; + } + return false; + } + + _connect(remote_id) { + if (!this._wss) { + this._wss = this._wssFactory(this._wssPath); + this._wss.on("error", this._onError.bind(this)); + this._wss.on("connecting", this._onConnecting.bind(this)); + this._wss.on("connected", this._onConnected.bind(this)); + this._wss.on("disconnected", this._onDisconnected.bind(this)); + this._wss.on("closing", this._onClosing.bind(this)); + this._wss.on("closed", this._onClosed.bind(this)); + this._wss.on("message", msg => { + try { + this._onMessage(remote_id, msg); + } catch (ex) { + this._onError(ex); + } + }); + if (this._beforeConnect) this._beforeConnect(); + this._wss.connect(); + } + } + + _onMessage(remote_id, raw) { + let ep = parseInt(raw.slice(0, 1)); // engine.io-protocol + let sp; // socket.io-protocol + let content; + + if (ep === 4) { + sp = parseInt(raw.slice(1, 2)); + if (sp === 2) { + content = raw.slice(2); + } + } + if (!(ep === 4 && sp === 2)) { + return; + } + let msg = JSON.parse(content)[1]["message"]["data"]; + let room = JSON.parse(content)[1]["room_name"]; + + // tickers + if (room.startsWith("ticker_")) { + let market = this._tickerSubs.get(remote_id); + if (!market) return; + + let ticker = this._constructTicker(msg, market); + this.emit("ticker", ticker, market); + + return; + } + + // trade + if (room.startsWith("transactions_")) { + for (let tx of msg.transactions) { + let market = this._tradeSubs.get(remote_id); + if (!market) return; + + let trade = this._constructTradesFromMessage(tx, market); + this.emit("trade", trade, market); + } + return; + } + + // l2 snapshot + if (room.startsWith("depth_whole_")) { + let market = this._level2SnapshotSubs.get(remote_id); + if (!market) return; + + let snapshot = this._constructLevel2Snapshot(msg, market); + this.emit("l2snapshot", snapshot, market); + return; + } + + // l2 update + if (room.startsWith("depth_diff_")) { + let market = this._level2UpdateSubs.get(remote_id); + if (!market) return; + + let update = this._constructLevel2Updates(msg, market); + this.emit("l2update", update, market); + return; + } + } + + _constructTicker(msg, market) { + let { last, timestamp, sell, vol, buy, high, low } = msg; + + return new Ticker({ + exchange: this._name, + base: market.base, + quote: market.quote, + timestamp: timestamp, + last: last, + high: high, + low: low, + volume: vol, + bid: sell, + ask: buy, + }); + } + + _constructTradesFromMessage(msg, market) { + let { side, executed_at, amount, price, transaction_id } = msg; + + return new Trade({ + exchange: this._name, + base: market.base, + quote: market.quote, + side: side.toLowerCase(), + unix: executed_at, + price: price, + amount: amount, + tradeId: transaction_id.toFixed(), + }); + } + + _constructLevel2Snapshot(msg, market) { + let asks = msg.asks.map(p => new Level2Point(p[0], p[1])); + let bids = msg.bids.map(p => new Level2Point(p[0], p[1])); + + return new Level2Snapshot({ + exchange: this._name, + base: market.base, + quote: market.quote, + timestampMs: msg.timestamp, + asks, + bids, + }); + } + + _constructLevel2Updates(msg, market) { + let asks = msg.a.map(p => new Level2Point(p[0], p[1])); + let bids = msg.b.map(p => new Level2Point(p[0], p[1])); + + return new Level2Update({ + exchange: this._name, + base: market.base, + quote: market.quote, + timestampMs: msg.t, + asks, + bids, + }); + } +} +module.exports = bitbankClient; diff --git a/src/index.js b/src/index.js index 0f31505a..e2d207c3 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ const bibox = require("./exchanges/bibox-client"); const binance = require("./exchanges/binance-client"); const binanceje = require("./exchanges/binanceje-client"); const binanceus = require("./exchanges/binanceus-client"); +const bitbank = require("./exchanges/bitbank-client"); const bitfinex = require("./exchanges/bitfinex-client"); const bitflyer = require("./exchanges/bitflyer-client"); const bitmex = require("./exchanges/bitmex-client"); @@ -30,6 +31,7 @@ module.exports = { binance, binanceje, binanceus, + bitbank, bitfinex, bitflyer, bitmex, From 600031bcbe90758b31b7e713181817b0d7f37543 Mon Sep 17 00:00:00 2001 From: develoQ Date: Sat, 9 Jan 2021 17:09:59 +0900 Subject: [PATCH 2/4] fix class name --- README.md | 2 +- __tests__/exchanges/bitbank-client.spec.js | 4 ++-- src/exchanges/bitbank-client.js | 4 ++-- src/index.js | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9b5d8bb4..bd842ff0 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ binance.subscribeLevel2Snapshots(market); | Binance Futures USDT-M | 1 | BinanceFuturesUsdtM | ✓ | ✓ | ✓ | ✓ | ✓\*\* | - | - | | Binance Jersey | 1 | BinanceJe | ✓ | ✓ | ✓ | ✓ | ✓\*\* | - | - | | Binance US | 1 | BinanceUs | ✓ | ✓ | ✓ | ✓ | ✓\*\* | - | - | +| bitbank | 1 | Bitbank | ✓ | ✓ | - | ✓ | ✓ | - | - | | Bitfinex | 2 | Bitfinex | ✓ | ✓ | - | - | ✓\* | - | ✓\* | | bitFlyer | 1 | Bitflyer | ✓ | ✓ | - | - | ✓\*\* | - | - | | Bithumb | 1 | Bithumb | ✓ | ✓ | - | - | ✓\*\* | - | - | @@ -76,7 +77,6 @@ binance.subscribeLevel2Snapshots(market); | FTX US | 1 | FtxUs | ✓ | ✓ | - | - | ✓\* | - | - | | Gate.io | 3 | Gateio | ✓ | ✓ | - | - | ✓\* | - | - | | Gemini | 1 | Gemini | - | ✓ | - | - | ✓\* | - | - | -| bitbank | 1 | bitbank | ✓ | ✓ | - | ✓ | ✓ | - | - | | HitBTC | 2 | HitBTC | ✓ | ✓ | ✓ | - | ✓\* | - | - | | Huobi Global | 1 | Huobi | ✓ | ✓ | ✓ | ✓ | - | - | - | | Huobi Global Futures | 1 | HuobiFutures | ✓ | ✓ | ✓ | ✓ | ✓\* | - | - | diff --git a/__tests__/exchanges/bitbank-client.spec.js b/__tests__/exchanges/bitbank-client.spec.js index dbec4e82..ccd26c11 100644 --- a/__tests__/exchanges/bitbank-client.spec.js +++ b/__tests__/exchanges/bitbank-client.spec.js @@ -1,8 +1,8 @@ const { testClient } = require("../test-runner"); -const bitbankClient = require("../../src/exchanges/bitbank-client"); +const BitbankClient = require("../../src/exchanges/bitbank-client"); testClient({ - clientFactory: () => new bitbankClient(), + clientFactory: () => new BitbankClient(), clientName: "bitbankClient", exchangeName: "bitbank", markets: [ diff --git a/src/exchanges/bitbank-client.js b/src/exchanges/bitbank-client.js index a41ad8e7..27171909 100644 --- a/src/exchanges/bitbank-client.js +++ b/src/exchanges/bitbank-client.js @@ -5,7 +5,7 @@ const Level2Point = require("../level2-point"); const Level2Snapshot = require("../level2-snapshot"); const Level2Update = require("../level2-update"); -class bitbankClient extends BasicClient { +class BitbankClient extends BasicClient { constructor({ wssPath = "wss://stream.bitbank.cc/socket.io/?EIO=3&transport=websocket", watcherMs, @@ -209,4 +209,4 @@ class bitbankClient extends BasicClient { }); } } -module.exports = bitbankClient; +module.exports = BitbankClient; diff --git a/src/index.js b/src/index.js index e2d207c3..fe87d7f5 100644 --- a/src/index.js +++ b/src/index.js @@ -64,6 +64,7 @@ module.exports = { BinanceFuturesUsdtM: require("./exchanges/binance-futures-usdtm-client"), BinanceJe: binanceje, BinanceUs: binanceus, + Bitbank: bitbank, Bitfinex: bitfinex, Bitflyer: bitflyer, Bithumb: require("./exchanges/bithumb-client"), From 69fb3f69ca6244623f6051f5b19d87f39b4bad46 Mon Sep 17 00:00:00 2001 From: develoQ Date: Sat, 16 Jan 2021 12:17:32 +0900 Subject: [PATCH 3/4] fix market data when receving message --- src/exchanges/bitbank-client.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/exchanges/bitbank-client.js b/src/exchanges/bitbank-client.js index 27171909..cecb3a6d 100644 --- a/src/exchanges/bitbank-client.js +++ b/src/exchanges/bitbank-client.js @@ -107,7 +107,8 @@ class BitbankClient extends BasicClient { // tickers if (room.startsWith("ticker_")) { - let market = this._tickerSubs.get(remote_id); + const id = room.replace("ticker_", ""); + let market = this._tickerSubs.get(id); if (!market) return; let ticker = this._constructTicker(msg, market); @@ -118,8 +119,9 @@ class BitbankClient extends BasicClient { // trade if (room.startsWith("transactions_")) { + const id = room.replace("transactions_", ""); for (let tx of msg.transactions) { - let market = this._tradeSubs.get(remote_id); + let market = this._tradeSubs.get(id); if (!market) return; let trade = this._constructTradesFromMessage(tx, market); @@ -130,7 +132,8 @@ class BitbankClient extends BasicClient { // l2 snapshot if (room.startsWith("depth_whole_")) { - let market = this._level2SnapshotSubs.get(remote_id); + const id = room.replace("depth_whole_", ""); + let market = this._level2SnapshotSubs.get(id); if (!market) return; let snapshot = this._constructLevel2Snapshot(msg, market); @@ -140,7 +143,8 @@ class BitbankClient extends BasicClient { // l2 update if (room.startsWith("depth_diff_")) { - let market = this._level2UpdateSubs.get(remote_id); + const id = room.replace("depth_diff_", ""); + let market = this._level2UpdateSubs.get(id); if (!market) return; let update = this._constructLevel2Updates(msg, market); From e1a9d57972310e4e1268a56bf34240f1d8323f9d Mon Sep 17 00:00:00 2001 From: develoQ Date: Sat, 16 Jan 2021 12:38:00 +0900 Subject: [PATCH 4/4] remove remoteId at onMessage --- src/exchanges/bitbank-client.js | 40 +-------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/src/exchanges/bitbank-client.js b/src/exchanges/bitbank-client.js index cecb3a6d..0cd2d96a 100644 --- a/src/exchanges/bitbank-client.js +++ b/src/exchanges/bitbank-client.js @@ -50,45 +50,7 @@ class BitbankClient extends BasicClient { this._wss.send(`42["left-room", "depth_diff_${remote_id}"]`); } - _subscribe(market, map, sendFn) { - let remote_id = market.id; - this._connect(remote_id); - if (!map.has(remote_id)) { - map.set(remote_id, market); - - // perform the subscription if we're connected - // and if not, then we'll reply on the _onConnected event - // to send the signal to our server! - if (this._wss && this._wss.isConnected) { - sendFn(remote_id, market); - } - return true; - } - return false; - } - - _connect(remote_id) { - if (!this._wss) { - this._wss = this._wssFactory(this._wssPath); - this._wss.on("error", this._onError.bind(this)); - this._wss.on("connecting", this._onConnecting.bind(this)); - this._wss.on("connected", this._onConnected.bind(this)); - this._wss.on("disconnected", this._onDisconnected.bind(this)); - this._wss.on("closing", this._onClosing.bind(this)); - this._wss.on("closed", this._onClosed.bind(this)); - this._wss.on("message", msg => { - try { - this._onMessage(remote_id, msg); - } catch (ex) { - this._onError(ex); - } - }); - if (this._beforeConnect) this._beforeConnect(); - this._wss.connect(); - } - } - - _onMessage(remote_id, raw) { + _onMessage(raw) { let ep = parseInt(raw.slice(0, 1)); // engine.io-protocol let sp; // socket.io-protocol let content;