diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index a0e1bd1e..447010b2 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -62,6 +62,7 @@ jobs: - ftx-us - gateio - gemini + - gmocoin - hitbtc - huobi - huobi-futures diff --git a/README.md b/README.md index 64658268..d6987609 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 | - | ✓ | - | - | ✓\* | - | - | +| GMOCoin | 1 | GMOCoin | ✓ | ✓ | - | ✓ | - | - | - | | HitBTC | 2 | HitBTC | ✓ | ✓ | ✓ | - | ✓\* | - | - | | Huobi Global | 1 | Huobi | ✓ | ✓ | ✓ | ✓ | - | - | - | | Huobi Global Futures | 1 | HuobiFutures | ✓ | ✓ | ✓ | ✓ | ✓\* | - | - | diff --git a/__tests__/exchanges/gmocoin-client.spec.js b/__tests__/exchanges/gmocoin-client.spec.js new file mode 100644 index 00000000..a2e97b43 --- /dev/null +++ b/__tests__/exchanges/gmocoin-client.spec.js @@ -0,0 +1,74 @@ +const { testClient } = require("../test-runner"); +const GMOCoinClient = require("../../src/exchanges/gmocoin-client"); + +testClient({ + clientFactory: () => new GMOCoinClient(), + clientName: "GMOCoinClient", + exchangeName: "GMOCoin", + markets: [ + { + id: "BTC", + base: "BTC", + quote: "JPY", + }, + { + id: "ETH", + base: "ETH", + quote: "JPY", + }, + { + id: "BCH", + base: "BCH", + quote: "JPY", + }, + { + id: "LTC", + base: "LTC", + quote: "JPY", + }, + { + id: "XRP", + base: "XRP", + quote: "JPY", + }, + ], + + testConnectEvents: true, + testDisconnectEvents: true, + testReconnectionEvents: true, + testCloseEvents: true, + + hasTickers: true, + hasTrades: true, + hasCandles: false, + hasLevel2Snapshots: true, + hasLevel2Updates: false, + 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: false, + }, + + l2snapshot: { + hasTimestampMs: true, + hasSequenceId: false, + hasCount: false, + }, +}); diff --git a/src/exchanges/gmocoin-client.js b/src/exchanges/gmocoin-client.js new file mode 100644 index 00000000..1355db03 --- /dev/null +++ b/src/exchanges/gmocoin-client.js @@ -0,0 +1,220 @@ +const BasicClient = require("../basic-client"); +const { throttle } = require("../flowcontrol/throttle"); +const Ticker = require("../ticker"); +const Trade = require("../trade"); +const Level2Point = require("../level2-point"); +const Level2Snapshot = require("../level2-snapshot"); +const moment = require("moment"); + +class GMOCoinClient extends BasicClient { + constructor({ + wssPath = "wss://api.coin.z.com/ws/public/v1", + throttleMs = 1000, + watcherMs, + } = {}) { + super(wssPath, "GMOCoin", undefined, watcherMs); + + this.hasTickers = true; + this.hasTrades = true; + this.hasLevel2Snapshots = true; + this._send = throttle(this._send.bind(this), throttleMs); + } + + _send(message) { + this._wss.send(message); + } + _sendPong(id) { + this._send(JSON.stringify({ pong: id })); + } + + _sendSubTicker(remote_id) { + this._send( + JSON.stringify({ + command: "subscribe", + channel: "ticker", + symbol: remote_id, + }) + ); + } + + _sendUnsubTicker(remote_id) { + this._send( + JSON.stringify({ + command: "unsubscribe", + channel: "ticker", + symbol: remote_id, + }) + ); + } + + _sendSubTrades(remote_id) { + this._send( + JSON.stringify({ + command: "subscribe", + channel: "trades", + symbol: remote_id, + // option:'TAKER_ONLY' + }) + ); + } + + _sendUnsubTrades(remote_id) { + this._send( + JSON.stringify({ + command: "unsubscribe", + channel: "trades", + symbol: remote_id, + // option:'TAKER_ONLY' + }) + ); + } + + _sendSubLevel2Snapshots(remote_id) { + this._send( + JSON.stringify({ + command: "subscribe", + channel: "orderbooks", + symbol: remote_id, + }) + ); + } + + _sendUnsubLevel2Snapshots(remote_id) { + this._send( + JSON.stringify({ + command: "unsubscribe", + channel: "orderbooks", + symbol: remote_id, + }) + ); + } + + _onMessage(raw) { + let msg = JSON.parse(raw); + + if (msg.ping) { + this._sendPong(msg.ping); + return; + } + + // tickers + if (msg.channel === "ticker") { + let market = this._tickerSubs.get(msg.symbol); + if (!market) return; + + let ticker = this._constructTicker(msg, market); + this.emit("ticker", ticker, market); + return; + } + + // trade + if (msg.channel === "trades") { + let market = this._tradeSubs.get(msg.symbol); + if (!market) return; + + let trade = this._constructTrade(msg, market); + this.emit("trade", trade, market); + return; + } + + // l2 snapshot + if (msg.channel === "orderbooks") { + let market = this._level2SnapshotSubs.get(msg.symbol); + if (!market) return; + + let snapshot = this._constructLevel2Snapshot(msg, market); + this.emit("l2snapshot", snapshot, market); + return; + } + } + + _onClosing() { + this._sendMessage.cancel(); + super._onClosing(); + } + + /** + * Response example: + * { + * "channel":"ticker", + * "ask": "750760", + * "bid": "750600", + * "high": "762302", + * "last": "756662", + * "low": "704874", + * "symbol": "BTC", + * "timestamp": "2018-03-30T12:34:56.789Z", + * "volume": "194785.8484" + * } + */ + _constructTicker(msg, market) { + let { ask, bid, high, last, low, timestamp, volume } = msg; + return new Ticker({ + exchange: this._name, + base: market.base, + quote: market.quote, + timestamp: moment.utc(timestamp).valueOf(), + last: last, + high: high, + low: low, + volume: volume, + bid, + ask, + }); + } + + /** + * Response example: + * { + * "channel":"trades", + * "price": "750760", + * "side": "BUY", + * "size": "0.1", + * "timestamp": "2018-03-30T12:34:56.789Z", + * "symbol": "BTC" + * } + */ + _constructTrade(datum, market) { + let { price, side, size, timestamp } = datum; + let unix = moment(timestamp).valueOf(); + return new Trade({ + exchange: this._name, + base: market.base, + quote: market.quote, + side: side.toLowerCase(), + unix, + price: price, + amount: size, + }); + } + + /** + * Response example: + * { + * "channel":"orderbooks", + * "asks": [ + * {"price": "455659","size": "0.1"}, + * {"price": "455658","size": "0.2"} + * ], + * "bids": [ + * {"price": "455665","size": "0.1"}, + * {"price": "455655","size": "0.3"} + * ], + * "symbol": "BTC", + * "timestamp": "2018-03-30T12:34:56.789Z" + * } + */ + _constructLevel2Snapshot(msg, market) { + let asks = msg.asks.map(p => new Level2Point(p.price, p.size)); + let bids = msg.bids.map(p => new Level2Point(p.price, p.size)); + return new Level2Snapshot({ + exchange: this._name, + base: market.base, + quote: market.quote, + timestampMs: moment.utc(msg.timestamp).valueOf(), + asks, + bids, + }); + } +} +module.exports = GMOCoinClient; diff --git a/src/index.js b/src/index.js index 0f31505a..48fbebd7 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ const ethfinex = require("./exchanges/ethfinex-client"); const ftx = require("./exchanges/ftx-client"); const gateio = require("./exchanges/gateio-client"); const gemini = require("./exchanges/gemini-client"); +const gmocoin = require("./exchanges/gmocoin-client"); const hitbtc = require("./exchanges/hitbtc-client"); const huobi = require("./exchanges/huobi-client"); const kucoin = require("./exchanges/kucoin-client"); @@ -42,6 +43,7 @@ module.exports = { ftx, gateio, gemini, + gmocoin, hitbtc, hitbtc2: hitbtc, huobi, @@ -79,6 +81,7 @@ module.exports = { FtxUs: require("./exchanges/ftx-us-client"), Gateio: gateio, Gemini: gemini, + GMOCoin: gmocoin, HitBTC: hitbtc, Huobi: huobi, HuobiFutures: require("./exchanges/huobi-futures-client"),