From 0a1cc4ed2bca1527d217e2517b648b9f2f357529 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 24 Feb 2023 15:59:05 +0000 Subject: [PATCH] v3.5.1: feat() add support for V5 public & private websockets --- package-lock.json | 4 +- package.json | 2 +- src/types/shared.ts | 4 +- src/types/websockets.ts | 3 +- src/util/websocket-util.ts | 222 +++++++++++++++++++++++++++++++--- src/websocket-client.ts | 236 +++++++++++++++++++++++-------------- 6 files changed, 358 insertions(+), 113 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3eaa06..feb57b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bybit-api", - "version": "3.5.0-beta.0", + "version": "3.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bybit-api", - "version": "3.5.0-beta.0", + "version": "3.5.1", "license": "MIT", "dependencies": { "axios": "^0.21.0", diff --git a/package.json b/package.json index e5089fb..687f567 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bybit-api", - "version": "3.5.0", + "version": "3.5.1", "description": "Complete & robust Node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & strong end to end tests.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/types/shared.ts b/src/types/shared.ts index 6396f12..3e13c34 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -1,6 +1,7 @@ import { ContractClient } from '../contract-client'; import { InverseClient } from '../inverse-client'; import { LinearClient } from '../linear-client'; +import { RestClientV5 } from '../rest-client-v5'; import { SpotClient } from '../spot-client'; import { SpotClientV3 } from '../spot-client-v3'; import { UnifiedMarginClient } from '../unified-margin-client'; @@ -15,7 +16,8 @@ export type RESTClient = | USDCOptionClient | USDCPerpetualClient | UnifiedMarginClient - | ContractClient; + | ContractClient + | RestClientV5; export type numberInString = string; diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 17c1b8a..7bfc63c 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -11,7 +11,8 @@ export type APIMarket = | 'unifiedPerp' | 'unifiedOption' | 'contractUSDT' - | 'contractInverse'; + | 'contractInverse' + | 'v5'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 339bbad..3d135c5 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -1,4 +1,4 @@ -import { APIMarket, WsKey } from '../types'; +import { APIMarket, CategoryV5, WsKey } from '../types'; interface NetworkMapV3 { livenet: string; @@ -9,10 +9,28 @@ interface NetworkMapV3 { type PublicPrivateNetwork = 'public' | 'private'; +/** + * The following WS keys are logical. + * + * They're not directly used as a market. They usually have one private endpoint but many public ones, + * so they need a bit of extra handling for seamless messaging between endpoints. + * + * For the unified keys, the "split" happens using the symbol. Symbols suffixed with USDT are obviously USDT topics. + * For the v5 endpoints, the subscribe/unsubscribe call must specify the category the subscription should route to. + */ +type PublicOnlyWsKeys = + | 'unifiedPerpUSDT' + | 'unifiedPerpUSDC' + | 'v5SpotPublic' + | 'v5LinearPublic' + | 'v5InversePublic' + | 'v5OptionPublic'; + export const WS_BASE_URL_MAP: Record< - APIMarket | 'unifiedPerpUSDT' | 'unifiedPerpUSDC', + APIMarket, Record -> = { +> & + Record> = { inverse: { public: { livenet: 'wss://stream.bybit.com/realtime', @@ -106,20 +124,12 @@ export const WS_BASE_URL_MAP: Record< livenet: 'wss://stream.bybit.com/contract/usdt/public/v3', testnet: 'wss://stream-testnet.bybit.com/contract/usdt/public/v3', }, - private: { - livenet: 'useUnifiedEndpoint', - testnet: 'useUnifiedEndpoint', - }, }, unifiedPerpUSDC: { public: { livenet: 'wss://stream.bybit.com/contract/usdc/public/v3', testnet: 'wss://stream-testnet.bybit.com/contract/usdc/public/v3', }, - private: { - livenet: 'useUnifiedEndpoint', - testnet: 'useUnifiedEndpoint', - }, }, contractUSDT: { public: { @@ -141,6 +151,40 @@ export const WS_BASE_URL_MAP: Record< testnet: 'wss://stream-testnet.bybit.com/contract/private/v3', }, }, + v5: { + public: { + livenet: 'public topics are routed internally via the public wskeys', + testnet: 'public topics are routed internally via the public wskeys', + }, + private: { + livenet: 'wss://stream.bybit.com/v5/private', + testnet: 'wss://stream-testnet.bybit.com/v5/private', + }, + }, + v5SpotPublic: { + public: { + livenet: 'wss://stream.bybit.com/v5/public/spot', + testnet: 'wss://stream-testnet.bybit.com/v5/public/spot', + }, + }, + v5LinearPublic: { + public: { + livenet: 'wss://stream.bybit.com/v5/public/linear', + testnet: 'wss://stream-testnet.bybit.com/v5/public/linear', + }, + }, + v5InversePublic: { + public: { + livenet: 'wss://stream.bybit.com/v5/public/inverse', + testnet: 'wss://stream-testnet.bybit.com/v5/public/inverse', + }, + }, + v5OptionPublic: { + public: { + livenet: 'wss://stream.bybit.com/v5/public/option', + testnet: 'wss://stream-testnet.bybit.com/v5/public/option', + }, + }, }; export const WS_KEY_MAP = { @@ -163,6 +207,11 @@ export const WS_KEY_MAP = { contractUSDTPrivate: 'contractUSDTPrivate', contractInversePublic: 'contractInversePublic', contractInversePrivate: 'contractInversePrivate', + v5SpotPublic: 'v5SpotPublic', + v5LinearPublic: 'v5LinearPublic', + v5InversePublic: 'v5InversePublic', + v5OptionPublic: 'v5OptionPublic', + v5Private: 'v5Private', } as const; export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [ @@ -172,6 +221,7 @@ export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [ WS_KEY_MAP.unifiedPrivate, WS_KEY_MAP.contractUSDTPrivate, WS_KEY_MAP.contractInversePrivate, + WS_KEY_MAP.v5Private, ]; export const PUBLIC_WS_KEYS = [ @@ -185,15 +235,15 @@ export const PUBLIC_WS_KEYS = [ WS_KEY_MAP.unifiedPerpUSDCPublic, WS_KEY_MAP.contractUSDTPublic, WS_KEY_MAP.contractInversePublic, + WS_KEY_MAP.v5SpotPublic, + WS_KEY_MAP.v5LinearPublic, + WS_KEY_MAP.v5InversePublic, + WS_KEY_MAP.v5OptionPublic, ] as string[]; /** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */ const PRIVATE_TOPICS = [ - 'position', - 'execution', - 'order', 'stop_order', - 'wallet', 'outboundAccountInfo', 'executionReport', 'ticketInfo', @@ -226,12 +276,23 @@ const PRIVATE_TOPICS = [ 'user.execution.contractAccount', 'user.order.contractAccount', 'user.wallet.contractAccount', + // v5 + 'position', + 'execution', + 'order', + 'wallet', + 'greeks', ]; +export function isPrivateWsTopic(topic: string): boolean { + return PRIVATE_TOPICS.includes(topic); +} + export function getWsKeyForTopic( market: APIMarket, topic: string, - isPrivate?: boolean + isPrivate?: boolean, + category?: CategoryV5 ): WsKey { const isPrivateTopic = isPrivate === true || PRIVATE_TOPICS.includes(topic); switch (market) { @@ -297,12 +358,138 @@ export function getWsKeyForTopic( ? WS_KEY_MAP.contractUSDTPrivate : WS_KEY_MAP.contractUSDTPublic; } + case 'v5': { + if (isPrivateTopic) { + return WS_KEY_MAP.v5Private; + } + + switch (category) { + case 'spot': { + return WS_KEY_MAP.v5SpotPublic; + } + case 'linear': { + return WS_KEY_MAP.v5LinearPublic; + } + case 'inverse': { + return WS_KEY_MAP.v5InversePublic; + } + case 'option': { + return WS_KEY_MAP.v5OptionPublic; + } + case undefined: { + throw new Error('Category cannot be undefined'); + } + default: { + throw neverGuard( + category, + 'getWsKeyForTopic(v5): Unhandled v5 category' + ); + } + } + // TODO: simple way to manage many public api groups in one api market? + return isPrivateTopic ? WS_KEY_MAP.v5Private : WS_KEY_MAP.v5Private; + } default: { throw neverGuard(market, 'getWsKeyForTopic(): Unhandled market'); } } } +export function getWsUrl( + wsKey: WsKey, + wsUrl: string | undefined, + isTestnet: boolean +): string { + if (wsUrl) { + return wsUrl; + } + + const networkKey = isTestnet ? 'testnet' : 'livenet'; + + switch (wsKey) { + case WS_KEY_MAP.linearPublic: { + return WS_BASE_URL_MAP.linear.public[networkKey]; + } + case WS_KEY_MAP.linearPrivate: { + return WS_BASE_URL_MAP.linear.private[networkKey]; + } + case WS_KEY_MAP.spotPublic: { + return WS_BASE_URL_MAP.spot.public[networkKey]; + } + case WS_KEY_MAP.spotPrivate: { + return WS_BASE_URL_MAP.spot.private[networkKey]; + } + case WS_KEY_MAP.spotV3Public: { + return WS_BASE_URL_MAP.spotv3.public[networkKey]; + } + case WS_KEY_MAP.spotV3Private: { + return WS_BASE_URL_MAP.spotv3.private[networkKey]; + } + case WS_KEY_MAP.inverse: { + // private and public are on the same WS connection + return WS_BASE_URL_MAP.inverse.public[networkKey]; + } + case WS_KEY_MAP.usdcOptionPublic: { + return WS_BASE_URL_MAP.usdcOption.public[networkKey]; + } + case WS_KEY_MAP.usdcOptionPrivate: { + return WS_BASE_URL_MAP.usdcOption.private[networkKey]; + } + case WS_KEY_MAP.usdcPerpPublic: { + return WS_BASE_URL_MAP.usdcPerp.public[networkKey]; + } + case WS_KEY_MAP.usdcPerpPrivate: { + return WS_BASE_URL_MAP.usdcPerp.private[networkKey]; + } + case WS_KEY_MAP.unifiedOptionPublic: { + return WS_BASE_URL_MAP.unifiedOption.public[networkKey]; + } + case WS_KEY_MAP.unifiedPerpUSDTPublic: { + return WS_BASE_URL_MAP.unifiedPerpUSDT.public[networkKey]; + } + case WS_KEY_MAP.unifiedPerpUSDCPublic: { + return WS_BASE_URL_MAP.unifiedPerpUSDC.public[networkKey]; + } + case WS_KEY_MAP.unifiedPrivate: { + return WS_BASE_URL_MAP.unifiedPerp.private[networkKey]; + } + case WS_KEY_MAP.contractInversePrivate: { + return WS_BASE_URL_MAP.contractInverse.private[networkKey]; + } + case WS_KEY_MAP.contractInversePublic: { + return WS_BASE_URL_MAP.contractInverse.public[networkKey]; + } + case WS_KEY_MAP.contractUSDTPrivate: { + return WS_BASE_URL_MAP.contractUSDT.private[networkKey]; + } + case WS_KEY_MAP.contractUSDTPublic: { + return WS_BASE_URL_MAP.contractUSDT.public[networkKey]; + } + case WS_KEY_MAP.v5Private: { + return WS_BASE_URL_MAP.v5.private[networkKey]; + } + case WS_KEY_MAP.v5SpotPublic: { + return WS_BASE_URL_MAP.v5SpotPublic.public[networkKey]; + } + case WS_KEY_MAP.v5LinearPublic: { + return WS_BASE_URL_MAP.v5LinearPublic.public[networkKey]; + } + case WS_KEY_MAP.v5InversePublic: { + return WS_BASE_URL_MAP.v5InversePublic.public[networkKey]; + } + case WS_KEY_MAP.v5OptionPublic: { + return WS_BASE_URL_MAP.v5OptionPublic.public[networkKey]; + } + default: { + this.logger.error('getWsUrl(): Unhandled wsKey: ', { + category: 'bybit-ws', + wsKey, + }); + throw neverGuard(wsKey, 'getWsUrl(): Unhandled wsKey'); + } + } +} + export function getMaxTopicsPerSubscribeEvent( market: APIMarket ): number | null { @@ -315,7 +502,8 @@ export function getMaxTopicsPerSubscribeEvent( case 'unifiedPerp': case 'spot': case 'contractInverse': - case 'contractUSDT': { + case 'contractUSDT': + case 'v5': { return null; } case 'spotv3': { diff --git a/src/websocket-client.ts b/src/websocket-client.ts index e1a1180..348b671 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -17,6 +17,7 @@ import WsStore from './util/WsStore'; import { APIMarket, + CategoryV5, KlineInterval, RESTClient, WSClientConfigurableOptions, @@ -34,10 +35,13 @@ import { WsConnectionStateEnum, getMaxTopicsPerSubscribeEvent, getWsKeyForTopic, + getWsUrl, + isPrivateWsTopic, isWsPong, neverGuard, serializeParams, } from './util'; +import { RestClientV5 } from './rest-client-v5'; const loggerCategory = { category: 'bybit-ws' }; @@ -119,13 +123,82 @@ export class WebsocketClient extends EventEmitter { this.on('error', () => {}); } + /** Get the WsStore that tracks websockets & topics */ + public getWsStore(): WsStore { + return this.wsStore; + } + + public isTestnet(): boolean { + return this.options.testnet === true; + } + /** - * Subscribe to topics & track/persist them. They will be automatically resubscribed to if the connection drops/reconnects. - * @param wsTopics topic or list of topics + * Subscribe to V5 topics & track/persist them. + * @param wsTopics - topic or list of topics + * @param category - the API category this topic is for (e.g. "linear"). The value is only important when connecting to public topics and will be ignored for private topics. + * @param isPrivateTopic - optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet) + */ + public subscribeV5( + wsTopics: WsTopic[] | WsTopic[], + category: CategoryV5, + isPrivateTopic?: boolean + ) { + const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + + topics.forEach((topic) => { + const wsKey = getWsKeyForTopic( + this.options.market, + topic, + isPrivateTopic, + category + ); + + // Persist topic for reconnects + this.wsStore.addTopic(wsKey, topic); + + // if connected, send subscription request + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { + return this.requestSubscribeTopics(wsKey, [topic]); + } + + // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect + if ( + !this.wsStore.isConnectionState( + wsKey, + WsConnectionStateEnum.CONNECTING + ) && + !this.wsStore.isConnectionState( + wsKey, + WsConnectionStateEnum.RECONNECTING + ) + ) { + return this.connect(wsKey); + } + }); + } + + /** + * Subscribe to V1-V3 topics & track/persist them. + * + * Note: for public V5 topics use the `subscribeV5()` method. + * + * Topics will be automatically resubscribed to if the connection resets/drops/reconnects. + * @param wsTopics - topic or list of topics * @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet) */ public subscribe(wsTopics: WsTopic[] | WsTopic, isPrivateTopic?: boolean) { const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + if (this.options.market === 'v5') { + topics.forEach((topic) => { + if (!isPrivateWsTopic(topic)) { + throw new Error( + 'For public "v5" websocket topics, use the subscribeV5() method & provide the category parameter' + ); + } + }); + } topics.forEach((topic) => { const wsKey = getWsKeyForTopic( @@ -161,12 +234,57 @@ export class WebsocketClient extends EventEmitter { } /** - * Unsubscribe from topics & remove them from memory. They won't be re-subscribed to if the connection reconnects. + * Unsubscribe from V5 topics & remove them from memory. They won't be re-subscribed to if the connection reconnects. + * @param wsTopics - topic or list of topics + * @param category - the API category this topic is for (e.g. "linear"). The value is only important when connecting to public topics and will be ignored for private topics. + * @param isPrivateTopic - optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet) + */ + public unsubscribeV5( + wsTopics: WsTopic[] | WsTopic[], + category: CategoryV5, + isPrivateTopic?: boolean + ) { + const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + topics.forEach((topic) => { + const wsKey = getWsKeyForTopic( + this.options.market, + topic, + isPrivateTopic, + category + ); + + // Remove topic from persistence for reconnects + this.wsStore.deleteTopic(wsKey, topic); + + // unsubscribe request only necessary if active connection exists + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { + this.requestUnsubscribeTopics(wsKey, [topic]); + } + }); + } + + /** + * Unsubscribe from V1-V3 topics & remove them from memory. They won't be re-subscribed to if the connection reconnects. + * + * Note: For public V5 topics, use `unsubscribeV5()` instead! + * * @param wsTopics topic or list of topics * @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet) */ public unsubscribe(wsTopics: WsTopic[] | WsTopic, isPrivateTopic?: boolean) { const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + if (this.options.market === 'v5') { + topics.forEach((topic) => { + if (!isPrivateWsTopic(topic)) { + throw new Error( + 'For public "v5" websocket topics, use the unsubscribeV5() method & provide the category parameter' + ); + } + }); + } + topics.forEach((topic) => { const wsKey = getWsKeyForTopic( this.options.market, @@ -251,6 +369,13 @@ export class WebsocketClient extends EventEmitter { ); break; } + case 'v5': { + this.restClient = new RestClientV5( + this.options.restOptions, + this.options.requestOptions + ); + break; + } default: { throw neverGuard( this.options.market, @@ -260,15 +385,6 @@ export class WebsocketClient extends EventEmitter { } } - /** Get the WsStore that tracks websockets & topics */ - public getWsStore(): WsStore { - return this.wsStore; - } - - public isTestnet(): boolean { - return this.options.testnet === true; - } - public close(wsKey: WsKey, force?: boolean) { this.logger.info('Closing connection', { ...loggerCategory, wsKey }); this.setWsState(wsKey, WsConnectionStateEnum.CLOSING); @@ -310,6 +426,9 @@ export class WebsocketClient extends EventEmitter { case 'contractInverse': { return [...this.connectPublic(), this.connectPrivate()]; } + case 'v5': { + return [this.connectPrivate()]; + } default: { throw neverGuard(this.options.market, 'connectAll(): Unhandled market'); } @@ -349,6 +468,14 @@ export class WebsocketClient extends EventEmitter { return [this.connect(WS_KEY_MAP.contractUSDTPublic)]; case 'contractInverse': return [this.connect(WS_KEY_MAP.contractInversePublic)]; + case 'v5': { + return [ + this.connect(WS_KEY_MAP.v5SpotPublic), + this.connect(WS_KEY_MAP.v5LinearPublic), + this.connect(WS_KEY_MAP.v5InversePublic), + this.connect(WS_KEY_MAP.v5OptionPublic), + ]; + } default: { throw neverGuard( this.options.market, @@ -386,6 +513,9 @@ export class WebsocketClient extends EventEmitter { return this.connect(WS_KEY_MAP.contractUSDTPrivate); case 'contractInverse': return this.connect(WS_KEY_MAP.contractInversePrivate); + case 'v5': { + return this.connect(WS_KEY_MAP.v5Private); + } default: { throw neverGuard( this.options.market, @@ -423,8 +553,8 @@ export class WebsocketClient extends EventEmitter { } const authParams = await this.getAuthParams(wsKey); - const url = this.getWsUrl(wsKey) + authParams; - const ws = this.connectToWsUrl(url, wsKey); + const url = getWsUrl(wsKey, this.options.wsUrl, this.isTestnet()); + const ws = this.connectToWsUrl(url + authParams, wsKey); return this.wsStore.setWs(wsKey, ws); } catch (err) { @@ -891,85 +1021,9 @@ export class WebsocketClient extends EventEmitter { this.wsStore.setConnectionState(wsKey, state); } - private getWsUrl(wsKey: WsKey): string { - if (this.options.wsUrl) { - return this.options.wsUrl; - } - - const networkKey = this.isTestnet() ? 'testnet' : 'livenet'; - - switch (wsKey) { - case WS_KEY_MAP.linearPublic: { - return WS_BASE_URL_MAP.linear.public[networkKey]; - } - case WS_KEY_MAP.linearPrivate: { - return WS_BASE_URL_MAP.linear.private[networkKey]; - } - case WS_KEY_MAP.spotPublic: { - return WS_BASE_URL_MAP.spot.public[networkKey]; - } - case WS_KEY_MAP.spotPrivate: { - return WS_BASE_URL_MAP.spot.private[networkKey]; - } - case WS_KEY_MAP.spotV3Public: { - return WS_BASE_URL_MAP.spotv3.public[networkKey]; - } - case WS_KEY_MAP.spotV3Private: { - return WS_BASE_URL_MAP.spotv3.private[networkKey]; - } - case WS_KEY_MAP.inverse: { - // private and public are on the same WS connection - return WS_BASE_URL_MAP.inverse.public[networkKey]; - } - case WS_KEY_MAP.usdcOptionPublic: { - return WS_BASE_URL_MAP.usdcOption.public[networkKey]; - } - case WS_KEY_MAP.usdcOptionPrivate: { - return WS_BASE_URL_MAP.usdcOption.private[networkKey]; - } - case WS_KEY_MAP.usdcPerpPublic: { - return WS_BASE_URL_MAP.usdcPerp.public[networkKey]; - } - case WS_KEY_MAP.usdcPerpPrivate: { - return WS_BASE_URL_MAP.usdcPerp.private[networkKey]; - } - case WS_KEY_MAP.unifiedOptionPublic: { - return WS_BASE_URL_MAP.unifiedOption.public[networkKey]; - } - case WS_KEY_MAP.unifiedPerpUSDTPublic: { - return WS_BASE_URL_MAP.unifiedPerpUSDT.public[networkKey]; - } - case WS_KEY_MAP.unifiedPerpUSDCPublic: { - return WS_BASE_URL_MAP.unifiedPerpUSDC.public[networkKey]; - } - case WS_KEY_MAP.unifiedPrivate: { - return WS_BASE_URL_MAP.unifiedPerp.private[networkKey]; - } - case WS_KEY_MAP.contractInversePrivate: { - return WS_BASE_URL_MAP.contractInverse.private[networkKey]; - } - case WS_KEY_MAP.contractInversePublic: { - return WS_BASE_URL_MAP.contractInverse.public[networkKey]; - } - case WS_KEY_MAP.contractUSDTPrivate: { - return WS_BASE_URL_MAP.contractUSDT.private[networkKey]; - } - case WS_KEY_MAP.contractUSDTPublic: { - return WS_BASE_URL_MAP.contractUSDT.public[networkKey]; - } - default: { - this.logger.error('getWsUrl(): Unhandled wsKey: ', { - ...loggerCategory, - wsKey, - }); - throw neverGuard(wsKey, 'getWsUrl(): Unhandled wsKey'); - } - } - } - private wrongMarketError(market: APIMarket) { return new Error( - `This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}' to listen to spot topics` + `This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}'" to listen to ${market} topics` ); }