From 350ed53a65ebd7234faf038d2a8582b0386fa76d Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Fri, 16 Sep 2022 17:06:27 +0100 Subject: [PATCH] unified margin support for ws --- README.md | 10 +- doc/websocket-client.md | 223 ------------------------------------- src/types/shared.ts | 4 +- src/types/websockets.ts | 4 +- src/util/websocket-util.ts | 69 +++++++++++- src/websocket-client.ts | 59 ++++++++-- 6 files changed, 128 insertions(+), 241 deletions(-) delete mode 100644 doc/websocket-client.md diff --git a/README.md b/README.md index 8641a9b..4594f5a 100644 --- a/README.md +++ b/README.md @@ -152,15 +152,16 @@ All API groups can be used via a shared `WebsocketClient`. However, make sure to The WebsocketClient can be configured to a specific API group using the market parameter. These are the currently available API groups: | API Category | Market | Description | |:----------------------------: |:-------------------: |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Unified Margin | TBC | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. | +| Unified Margin - Options | `market: 'unifiedOption'`| The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only support options topics. If you need USDC/USDT perps, use `unifiedPerp` instead. | +| Unified Margin - Perps | `market: 'unifiedPerp'` | The [derivatives v3](https://bybit-exchange.github.io/docs/derivativesV3/unified_margin/#t-websocket) category for unified margin. Note: public topics only USDT/USDC perps topics - use `unifiedOption` if you need public options topics. | | Futures v2 - Inverse Perps | `market: 'inverse'` | The [inverse v2 perps](https://bybit-exchange.github.io/docs/futuresV2/inverse/#t-websocket) category. | | Futures v2 - USDT Perps | `market: 'linear'` | The [USDT/linear v2 perps](https://bybit-exchange.github.io/docs/futuresV2/linear/#t-websocket) category. | | Futures v2 - Inverse Futures | `market: 'inverse'` | The [inverse futures v2](https://bybit-exchange.github.io/docs/futuresV2/inverse_futures/#t-websocket) category uses the same market as inverse perps. | | Spot v3 | `market: 'spotv3'` | The [spot v3](https://bybit-exchange.github.io/docs/spot/v3/#t-websocket) category. | | Spot v1 | `market: 'spot'` | The older [spot v1](https://bybit-exchange.github.io/docs/spot/v1/#t-websocket) category. Use the `spotv3` market if possible, as the v1 category does not have automatic re-subscribe if reconnected. | -| Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to all copy trading topics. | -| USDC Perps | TBC | The [USDC perps](https://bybit-exchange.github.io/docs/usdc/perpetual/#t-websocket) category. | -| USDC Options | TBC | The [USDC options](https://bybit-exchange.github.io/docs/usdc/option/#t-websocket) category. | +| Copy Trading | `market: 'linear'` | The [copy trading](https://bybit-exchange.github.io/docs/copy_trading/#t-websocket) category. Use the linear market to listen to all copy trading topics. | +| USDC Perps | `market: 'usdcPerp` | The [USDC perps](https://bybit-exchange.github.io/docs/usdc/perpetual/#t-websocket) category. | +| USDC Options | `market: 'usdcOption'`| The [USDC options](https://bybit-exchange.github.io/docs/usdc/option/#t-websocket) category. | ```javascript const { WebsocketClient } = require('bybit-api'); @@ -181,7 +182,6 @@ const wsConfig = { // NOTE: to listen to multiple markets (spot vs inverse vs linear vs linearfutures) at once, make one WebsocketClient instance per market - // defaults to inverse: // market: 'inverse' // market: 'linear' // market: 'spot' diff --git a/doc/websocket-client.md b/doc/websocket-client.md deleted file mode 100644 index 23ecfbd..0000000 --- a/doc/websocket-client.md +++ /dev/null @@ -1,223 +0,0 @@ -# Websocket API -## Class: WebsocketClient - -The `WebsocketClient` inherits from `EventEmitter`. After establishing a -connection, the client sends heartbeats in regular intervalls, and reconnects -to the server once connection has been lost. - -### new WebsocketClient([options][, logger]) -- `options` {Object} Configuration options - - `key` {String} Bybit API Key. Only needed if private topics are subscribed - - `secret` {String} Bybit private Key. Only needed if private topics are - subscribed - - `livenet` {Bool} Weather to connect to livenet (`true`). Default `false`. - - `pingInterval` {Integer} Interval in ms for heartbeat ping. Default: `10000`, - - `pongTimeout` {Integer} Timeout in ms waiting for heartbeat pong response - from server. Default: `1000`, - - `reconnectTimeout` {Integer} Timeout in ms the client waits before trying - to reconnect after a lost connection. Default: 500 -- `logger` {Object} Optional custom logger - -Custom logger must contain the following methods: -```js -const logger = { - silly: function(message, data) {}, - debug: function(message, data) {}, - notice: function(message, data) {}, - info: function(message, data) {}, - warning: function(message, data) {}, - error: function(message, data) {}, -} -``` - -### ws.subscribe(topics) -- `topics` {String|Array} Single topic as string or multiple topics as array of strings. -Subscribe to one or multiple topics. See [available topics](#available-topics) - -### ws.unsubscribe(topics) -- `topics` {String|Array} Single topic as string or multiple topics as array of strings. -Unsubscribe from one or multiple topics. - -### ws.close() -Close the connection to the server. - -### Event: 'open' -Emmited when the connection has been opened for the first time. - -### Event: 'reconnected' -Emmited when the client has been opened after a reconnect. - -### Event: 'update' -- `message` {Object} - - `topic` {String} the topic for which the update occured - - `data` {Array|Object} updated data (see docs for each [topic](#available-topics)). - - `type` {String} Some topics might have different update types (see docs for each [topic](#available-topics)). - -Emmited whenever an update to a subscribed topic occurs. - -### Event: 'response' -- `response` {Object} - - `success` {Bool} - - `ret_msg` {String} empty if operation was successfull, otherwise error message. - - `conn_id` {String} connection id - - `request` {Object} Original request, to which the response belongs - - `op` {String} operation - - `args` {Array} Request Arguments - -Emited when the server responds to an operation sent by the client (usually after subscribing to a topic). - -### Event: 'close' -Emitted when the connection has been finally closed, after a call to `ws.close()` - -### Event: 'reconnect' -Emitted when the connection has been closed, but the client will try to reconnect. - -### Event: 'error' -- `error` {Error} - -Emitted when an error occurs. - -## Available Topics -Generaly all [public](https://bybit-exchange.github.io/docs/inverse/#t-publictopics) and [private](https://bybit-exchange.github.io/docs/inverse/#t-privatetopics) - topics are available. - -### Private topics -#### Positions of your account -All positions of your account. -Topic: `position` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketposition) - -#### Execution message -Execution message, whenever an order has been (partially) filled. -Topic: `execution` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketexecution) - -#### Update for your orders -Updates for your active orders -Topic: `order` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketorder) - -#### Update for your conditional orders -Updates for your active conditional orders -Topic: `stop_order` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketstoporder) - -### Public topics -#### Candlestick chart -Candlestick OHLC "candles" for selected symbol and interval. -Example topic: `klineV2.BTCUSD.1m` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketklinev2) - -#### Real-time trading information -All trades as they occur. -Topic: `trade` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websockettrade) - -#### Daily insurance fund update -Topic: `insurance` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketinsurance) - -#### OrderBook of 25 depth per side -OrderBook for selected symbol -Example topic: `orderBookL2_25.BTCUSD` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketorderbook25) - -#### OrderBook of 200 depth per side -OrderBook for selected symbol -Example topic: `orderBook_200.100ms.BTCUS` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketorderbook200) - -#### Latest information for symbol -Latest information for selected symbol -Example topic: `instrument_info.100ms.BTCUSD` - -[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-websocketinstrumentinfo) - -## Examples -### Klines -```javascript -const {WebsocketClient} = require('bybit-api'); - -const API_KEY = 'xxx'; -const PRIVATE_KEY = 'yyy'; - -const ws = new WebsocketClient({key: API_KEY, secret: PRIVATE_KEY}); - -ws.subscribe(['position', 'execution', 'trade']); -ws.subscribe('kline.BTCUSD.1m'); - -ws.on('open', function() { - console.log('connection open'); -}); - -ws.on('update', function(message) { - console.log('update', message); -}); - -ws.on('response', function(response) { - console.log('response', response); -}); - -ws.on('close', function() { - console.log('connection closed'); -}); - -ws.on('error', function(err) { - console.error('ERR', err); -}); -``` - -### OrderBook Events -```javascript -const { WebsocketClient, DefaultLogger } = require('bybit-api'); -const { OrderBooksStore, OrderBookLevel } = require('orderbooks'); - -const OrderBooks = new OrderBooksStore({ traceLog: true, checkTimestamps: false }); - -// connect to a websocket and relay orderbook events to handlers -DefaultLogger.silly = () => {}; -const ws = new WebsocketClient({ livenet: true }); -ws.on('update', message => { - if (message.topic.toLowerCase().startsWith('orderbook')) { - return handleOrderbookUpdate(message); - } -}); -ws.subscribe('orderBookL2_25.BTCUSD'); - -// parse orderbook messages, detect snapshot vs delta, and format properties using OrderBookLevel -const handleOrderbookUpdate = message => { - const { topic, type, data, timestamp_e6 } = message; - const [ topicKey, symbol ] = topic.split('.'); - - if (type == 'snapshot') { - return OrderBooks.handleSnapshot(symbol, data.map(mapBybitBookSlice), timestamp_e6 / 1000, message).print(); - } - - if (type == 'delta') { - const deleteLevels = data.delete.map(mapBybitBookSlice); - const updateLevels = data.update.map(mapBybitBookSlice); - const insertLevels = data.insert.map(mapBybitBookSlice); - return OrderBooks.handleDelta( - symbol, - deleteLevels, - updateLevels, - insertLevels, - timestamp_e6 / 1000 - ).print(); - } -} - -// Low level map of exchange properties to expected local properties -const mapBybitBookSlice = level => { - return OrderBookLevel(level.symbol, +level.price, level.side, level.size); -}; -``` diff --git a/src/types/shared.ts b/src/types/shared.ts index 10aa83d..901e225 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -2,6 +2,7 @@ import { InverseClient } from '../inverse-client'; import { LinearClient } from '../linear-client'; import { SpotClient } from '../spot-client'; import { SpotClientV3 } from '../spot-client-v3'; +import { UnifiedMarginClient } from '../unified-margin-client'; import { USDCOptionClient } from '../usdc-option-client'; import { USDCPerpetualClient } from '../usdc-perpetual-client'; @@ -11,7 +12,8 @@ export type RESTClient = | SpotClient | SpotClientV3 | USDCOptionClient - | USDCPerpetualClient; + | USDCPerpetualClient + | UnifiedMarginClient; export type numberInString = string; diff --git a/src/types/websockets.ts b/src/types/websockets.ts index a44a737..68b100f 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -7,7 +7,9 @@ export type APIMarket = | 'spot' | 'spotv3' | 'usdcOption' - | 'usdcPerp'; + | 'usdcPerp' + | 'unifiedPerp' + | 'unifiedOption'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index dbc743e..f4f2219 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -10,7 +10,7 @@ interface NetworkMapV3 { type PublicPrivateNetwork = 'public' | 'private'; export const WS_BASE_URL_MAP: Record< - APIMarket, + APIMarket | 'unifiedPerpUSDT' | 'unifiedPerpUSDC', Record > = { inverse: { @@ -81,6 +81,46 @@ export const WS_BASE_URL_MAP: Record< testnet: 'wss://stream-testnet.bybit.com/trade/option/usdc/private/v1', }, }, + unifiedOption: { + public: { + livenet: 'wss://stream.bybit.com/option/usdc/public/v3', + testnet: 'wss://stream-testnet.bybit.com/option/usdc/public/v3', + }, + private: { + livenet: 'wss://stream.bybit.com/unified/private/v3', + testnet: 'wss://stream-testnet.bybit.com/unified/private/v3', + }, + }, + unifiedPerp: { + public: { + livenet: 'useBaseSpecificEndpoint', + testnet: 'useBaseSpecificEndpoint', + }, + private: { + livenet: 'wss://stream.bybit.com/unified/private/v3', + testnet: 'wss://stream-testnet.bybit.com/unified/private/v3', + }, + }, + unifiedPerpUSDT: { + public: { + 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', + }, + }, }; export const WS_KEY_MAP = { @@ -95,6 +135,10 @@ export const WS_KEY_MAP = { usdcOptionPublic: 'usdcOptionPublic', usdcPerpPrivate: 'usdcPerpPrivate', usdcPerpPublic: 'usdcPerpPublic', + unifiedPrivate: 'unifiedPrivate', + unifiedOptionPublic: 'unifiedOptionPublic', + unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic', + unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic', } as const; export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [ @@ -180,6 +224,29 @@ export function getWsKeyForTopic( ? WS_KEY_MAP.usdcPerpPrivate : WS_KEY_MAP.usdcPerpPublic; } + case 'unifiedOption': { + return isPrivateTopic + ? WS_KEY_MAP.unifiedPrivate + : WS_KEY_MAP.unifiedOptionPublic; + } + case 'unifiedPerp': { + if (isPrivateTopic) { + return WS_KEY_MAP.unifiedPrivate; + } + + const upperTopic = topic.toUpperCase(); + if (upperTopic.indexOf('USDT') !== -1) { + return WS_KEY_MAP.unifiedPerpUSDTPublic; + } + + if (upperTopic.indexOf('USDC') !== -1) { + return WS_KEY_MAP.unifiedPerpUSDCPublic; + } + + throw new Error( + `Failed to determine wskey for unified perps topic: "${topic}` + ); + } default: { throw neverGuard(market, `getWsKeyForTopic(): Unhandled market`); } diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 1dc88c7..04d8323 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -33,6 +33,7 @@ import { } from './util'; import { USDCOptionClient } from './usdc-option-client'; import { USDCPerpetualClient } from './usdc-perpetual-client'; +import { UnifiedMarginClient } from './unified-margin-client'; const loggerCategory = { category: 'bybit-ws' }; @@ -163,6 +164,17 @@ export class WebsocketClient extends EventEmitter { ); break; } + case 'unifiedOption': + case 'unifiedPerp': { + this.restClient = new UnifiedMarginClient( + undefined, + undefined, + !this.isTestnet(), + this.options.restOptions, + this.options.requestOptions + ); + break; + } default: { throw neverGuard( this.options.market, @@ -198,15 +210,17 @@ export class WebsocketClient extends EventEmitter { switch (this.options.market) { case 'inverse': { // only one for inverse - return [this.connectPublic()]; + return [...this.connectPublic()]; } // these all have separate public & private ws endpoints case 'linear': case 'spot': case 'spotv3': case 'usdcOption': - case 'usdcPerp': { - return [this.connectPublic(), this.connectPrivate()]; + case 'usdcPerp': + case 'unifiedPerp': + case 'unifiedOption': { + return [...this.connectPublic(), this.connectPrivate()]; } default: { throw neverGuard(this.options.market, `connectAll(): Unhandled market`); @@ -214,25 +228,34 @@ export class WebsocketClient extends EventEmitter { } } - public connectPublic(): Promise { + public connectPublic(): Promise[] { switch (this.options.market) { case 'inverse': { - return this.connect(WS_KEY_MAP.inverse); + return [this.connect(WS_KEY_MAP.inverse)]; } case 'linear': { - return this.connect(WS_KEY_MAP.linearPublic); + return [this.connect(WS_KEY_MAP.linearPublic)]; } case 'spot': { - return this.connect(WS_KEY_MAP.spotPublic); + return [this.connect(WS_KEY_MAP.spotPublic)]; } case 'spotv3': { - return this.connect(WS_KEY_MAP.spotV3Public); + return [this.connect(WS_KEY_MAP.spotV3Public)]; } case 'usdcOption': { - return this.connect(WS_KEY_MAP.usdcOptionPublic); + return [this.connect(WS_KEY_MAP.usdcOptionPublic)]; } case 'usdcPerp': { - return this.connect(WS_KEY_MAP.usdcPerpPublic); + return [this.connect(WS_KEY_MAP.usdcPerpPublic)]; + } + case 'unifiedOption': { + return [this.connect(WS_KEY_MAP.unifiedOptionPublic)]; + } + case 'unifiedPerp': { + return [ + this.connect(WS_KEY_MAP.unifiedPerpUSDTPublic), + this.connect(WS_KEY_MAP.unifiedPerpUSDCPublic), + ]; } default: { throw neverGuard( @@ -263,6 +286,10 @@ export class WebsocketClient extends EventEmitter { case 'usdcPerp': { return this.connect(WS_KEY_MAP.usdcPerpPrivate); } + case 'unifiedPerp': + case 'unifiedOption': { + return this.connect(WS_KEY_MAP.unifiedPrivate); + } default: { throw neverGuard( this.options.market, @@ -719,6 +746,18 @@ export class WebsocketClient extends EventEmitter { 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]; + } default: { this.logger.error('getWsUrl(): Unhandled wsKey: ', { ...loggerCategory,