From 4d7886ff72f794c58f66867bc118a6b3bbab7f9e Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 13 Aug 2021 17:28:31 +0100 Subject: [PATCH] add spot websocket client (#99) --- README.md | 23 +++++-------- src/websocket-client.ts | 74 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 694adbb..5fb3ca4 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ There are three REST API modules as there are some differences in each contract 3. `LinearClient` for linear perpetual ### REST Inverse -
To use the inverse REST APIs, import the `InverseClient`. Click here to expand and see full sample: +To use the inverse REST APIs, import the `InverseClient`: ```javascript const { InverseClient } = require('bybit-api'); @@ -100,12 +100,11 @@ client.getOrderBook({ symbol: 'BTCUSD' }) }); ``` -
See [inverse-client.ts](./src/inverse-client.ts) for further information. ### REST Inverse Futures -
To use the inverse futures REST APIs, import the `InverseFuturesClient`. Click here to expand and see full sample: +To use the inverse futures REST APIs, import the `InverseFuturesClient`: ```javascript const { InverseFuturesClient } = require('bybit-api'); @@ -142,12 +141,10 @@ client.getOrderBook({ symbol: 'BTCUSDH21' }) }); ``` -
- See [inverse-futures-client.ts](./src/inverse-futures-client.ts) for further information. ### REST Linear -
To use the Linear (USDT) REST APIs, import the `LinearClient`. Click here to expand and see full sample: +To use the Linear (USDT) REST APIs, import the `LinearClient`: ```javascript const { LinearClient } = require('bybit-api'); @@ -184,10 +181,8 @@ client.getOrderBook({ symbol: 'BTCUSDT' }) }); ``` -
- ## WebSockets -
Inverse & linear WebSockets can be used via a shared `WebsocketClient`. Click here to expand and see full sample: +Inverse, linear & spot WebSockets can be used via a shared `WebsocketClient`. However, make sure to make one instance of WebsocketClient per market type (spot vs inverse vs linear vs linearfutures): ```javascript const { WebsocketClient } = require('bybit-api'); @@ -206,9 +201,14 @@ const wsConfig = { // defaults to false == testnet. Set to true for livenet. // livenet: true + // NOTE: to listen to multiple markets (spot vs inverse vs linear vs linearfutures) at once, make one WebsocketClient instance per market + // defaults to false == inverse. Set to true for linear (USDT) trading. // linear: true + // defaults to false == inverse. Set to true for spot trading. These booleans will be changed into a single setting in future. + // spot: true + // how long to wait (in ms) before deciding the connection should be terminated & reconnected // pongTimeout: 1000, @@ -263,7 +263,6 @@ ws.on('error', err => { }); ``` -
See [websocket-client.ts](./src/websocket-client.ts) for further information. @@ -274,8 +273,6 @@ Note: for linear websockets, pass `linear: true` in the constructor options when ## Customise Logging Pass a custom logger which supports the log methods `silly`, `debug`, `notice`, `info`, `warning` and `error`, or override methods from the default logger as desired. -
Click here to expand and see full sample: - ```javascript const { WebsocketClient, DefaultLogger } = require('bybit-api'); @@ -288,8 +285,6 @@ const ws = new WebsocketClient( ); ``` -
- ## Browser Usage Build a bundle using webpack: - `npm install` diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 9f6dbdc..9ed36ae 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -27,6 +27,19 @@ const linearEndpoints = { } }; +const spotEndpoints = { + private: { + livenet: 'wss://stream.bybit.com/spot/ws', + testnet: 'wss://stream-testnet.bybit.com/spot/ws', + }, + public: { + livenet: 'wss://stream.bybit.com/spot/quote/ws/v1', + livenet2: 'wss://stream.bybit.com/spot/quote/ws/v2', + testnet: 'wss://stream-testnet.bybit.com/spot/quote/ws/v1', + testnet2: 'wss://stream-testnet.bybit.com/spot/quote/ws/v2', + } +} + const loggerCategory = { category: 'bybit-ws' }; const READY_STATE_INITIAL = 0; @@ -47,7 +60,11 @@ export interface WSClientConfigurableOptions { key?: string; secret?: string; livenet?: boolean; + + // defaults to inverse. Only set one at a time (this interface will change in future) linear?: boolean; + spot?: boolean; + pongTimeout?: number; pingInterval?: number; reconnectTimeout?: number; @@ -59,6 +76,7 @@ export interface WSClientConfigurableOptions { export interface WebsocketClientOptions extends WSClientConfigurableOptions { livenet: boolean; linear: boolean; + spot: boolean; pongTimeout: number; pingInterval: number; reconnectTimeout: number; @@ -68,9 +86,11 @@ export interface WebsocketClientOptions extends WSClientConfigurableOptions { export const wsKeyInverse = 'inverse'; export const wsKeyLinearPrivate = 'linearPrivate'; export const wsKeyLinearPublic = 'linearPublic'; +export const wsKeySpotPrivate = 'spotPrivate'; +export const wsKeySpotPublic = 'spotPublic'; // This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) -export type WsKey = 'inverse' | 'linearPrivate' | 'linearPublic'; +export type WsKey = 'inverse' | 'linearPrivate' | 'linearPublic' | 'spotPrivate' | 'spotPublic'; const getLinearWsKeyForTopic = (topic: string): WsKey => { const privateLinearTopics = ['position', 'execution', 'order', 'stop_order', 'wallet']; @@ -80,6 +100,14 @@ const getLinearWsKeyForTopic = (topic: string): WsKey => { return wsKeyLinearPublic; } +const getSpotWsKeyForTopic = (topic: string): WsKey => { + const privateLinearTopics = ['position', 'execution', 'order', 'stop_order']; + if (privateLinearTopics.includes(topic)) { + return wsKeySpotPrivate; + } + + return wsKeySpotPublic; +} export declare interface WebsocketClient { on(event: 'open' | 'reconnected', listener: ({ wsKey: WsKey, event: any }) => void): this; @@ -102,6 +130,7 @@ export class WebsocketClient extends EventEmitter { this.options = { livenet: false, linear: false, + spot: false, pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500, @@ -110,6 +139,9 @@ export class WebsocketClient extends EventEmitter { if (this.isLinear()) { this.restClient = new LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions); + } else if (this.isSpot()) { + // TODO: spot client + this.restClient = new LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions); } else { this.restClient = new InverseClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions); } @@ -123,8 +155,12 @@ export class WebsocketClient extends EventEmitter { return this.options.linear === true; } + public isSpot(): boolean { + return this.options.spot === true; + } + public isInverse(): boolean { - return !this.isLinear(); + return !this.isLinear() && !this.isSpot(); } /** @@ -191,6 +227,10 @@ export class WebsocketClient extends EventEmitter { if (this.isLinear()) { return [this.connect(wsKeyLinearPublic), this.connect(wsKeyLinearPrivate)]; } + + if (this.isSpot()) { + return [this.connect(wsKeySpotPublic), this.connect(wsKeySpotPrivate)]; + } } private async connect(wsKey: WsKey): Promise { @@ -246,7 +286,7 @@ export class WebsocketClient extends EventEmitter { private async getAuthParams(wsKey: WsKey): Promise { const { key, secret } = this.options; - if (key && secret && wsKey !== wsKeyLinearPublic) { + if (key && secret && wsKey !== wsKeyLinearPublic && wsKey !== wsKeySpotPublic) { this.logger.debug('Getting auth\'d request params', { ...loggerCategory, wsKey }); const timeOffset = await this.restClient.getTimeOffset(); @@ -365,7 +405,7 @@ export class WebsocketClient extends EventEmitter { private onWsOpen(event, wsKey: WsKey) { if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) { - this.logger.info('Websocket connected', { ...loggerCategory, wsKey, livenet: this.isLivenet(), linear: this.isLinear() }); + this.logger.info('Websocket connected', { ...loggerCategory, wsKey, livenet: this.isLivenet(), linear: this.isLinear(), spot: this.isSpot() }); this.emit('open', { wsKey, event }); } else if (this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)) { this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey }); @@ -439,7 +479,8 @@ export class WebsocketClient extends EventEmitter { return this.options.wsUrl; } - const networkKey = this.options.livenet ? 'livenet' : 'testnet'; + const networkKey = this.isLivenet() ? 'livenet' : 'testnet'; + // TODO: reptitive if (this.isLinear() || wsKey.startsWith('linear')){ if (wsKey === wsKeyLinearPublic) { return linearEndpoints.public[networkKey]; @@ -452,10 +493,31 @@ export class WebsocketClient extends EventEmitter { this.logger.error('Unhandled linear wsKey: ', { ...loggerCategory, wsKey }); return linearEndpoints[networkKey]; } + + if (this.isSpot() || wsKey.startsWith('spot')){ + if (wsKey === wsKeySpotPublic) { + return spotEndpoints.public[networkKey]; + } + + if (wsKey === wsKeySpotPrivate) { + return spotEndpoints.private[networkKey]; + } + + this.logger.error('Unhandled spot wsKey: ', { ...loggerCategory, wsKey }); + return spotEndpoints[networkKey]; + } + + // fallback to inverse return inverseEndpoints[networkKey]; } private getWsKeyForTopic(topic: string) { - return this.isInverse() ? wsKeyInverse : getLinearWsKeyForTopic(topic); + if (this.isInverse()) { + return wsKeyInverse; + } + if (this.isLinear()) { + return getLinearWsKeyForTopic(topic) + } + return getSpotWsKeyForTopic(topic); } };