feat(): upgrade WebSocket layer to extend BaseWS abstraction. feat(): add promisified WS workflows, feat(): add WS API integration
This commit is contained in:
136
src/types/websockets/ws-api.ts
Normal file
136
src/types/websockets/ws-api.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { APIID, WS_KEY_MAP } from '../../util';
|
||||||
|
import {
|
||||||
|
AmendOrderParamsV5,
|
||||||
|
CancelOrderParamsV5,
|
||||||
|
OrderParamsV5,
|
||||||
|
} from '../request';
|
||||||
|
import { WsKey } from './ws-general';
|
||||||
|
|
||||||
|
export type WSAPIOperation = 'order.create' | 'order.amend' | 'order.cancel';
|
||||||
|
|
||||||
|
export type WsOperation =
|
||||||
|
| 'subscribe'
|
||||||
|
| 'unsubscribe'
|
||||||
|
| 'auth'
|
||||||
|
| 'ping'
|
||||||
|
| 'pong';
|
||||||
|
|
||||||
|
export const WS_API_Operations: WSAPIOperation[] = [
|
||||||
|
'order.create',
|
||||||
|
'order.amend',
|
||||||
|
'order.cancel',
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface WsRequestOperationBybit<
|
||||||
|
TWSTopic extends string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||||
|
// TWSPayload = any,
|
||||||
|
> {
|
||||||
|
req_id: string;
|
||||||
|
op: WsOperation;
|
||||||
|
args?: (TWSTopic | string | number)[];
|
||||||
|
// payload?: TWSPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WSAPIRequest<
|
||||||
|
TRequestParams = undefined,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
TWSOperation extends WSAPIOperation = any,
|
||||||
|
> {
|
||||||
|
reqId: string;
|
||||||
|
op: TWSOperation;
|
||||||
|
header: {
|
||||||
|
'X-BAPI-TIMESTAMP': string;
|
||||||
|
'X-BAPI-RECV-WINDOW': string;
|
||||||
|
Referer: typeof APIID;
|
||||||
|
};
|
||||||
|
args: [TRequestParams];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WsAPIWsKeyTopicMap {
|
||||||
|
[WS_KEY_MAP.v5PrivateTrade]: WSAPIOperation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WsAPITopicRequestParamMap {
|
||||||
|
'order.create': OrderParamsV5;
|
||||||
|
'order.amend': AmendOrderParamsV5;
|
||||||
|
'order.cancel': CancelOrderParamsV5;
|
||||||
|
// ping: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WsAPITopicRequestParams =
|
||||||
|
WsAPITopicRequestParamMap[keyof WsAPITopicRequestParamMap];
|
||||||
|
|
||||||
|
export interface WSAPIResponse<
|
||||||
|
TResponseData extends object = object,
|
||||||
|
TOperation extends WSAPIOperation = WSAPIOperation,
|
||||||
|
> {
|
||||||
|
wsKey: WsKey;
|
||||||
|
/** Auto-generated */
|
||||||
|
reqId: string;
|
||||||
|
retCode: 0 | number;
|
||||||
|
retMsg: 'OK' | string;
|
||||||
|
op: TOperation;
|
||||||
|
data: [TResponseData];
|
||||||
|
header?: {
|
||||||
|
'X-Bapi-Limit': string;
|
||||||
|
'X-Bapi-Limit-Status': string;
|
||||||
|
'X-Bapi-Limit-Reset-Timestamp': string;
|
||||||
|
Traceid: string;
|
||||||
|
Timenow: string;
|
||||||
|
};
|
||||||
|
connId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface WsAPIResponseMap<TChannel extends WSAPITopic = WSAPITopic> {
|
||||||
|
// 'spot.login': WSAPIResponse<WSAPILoginResponse, TChannel>;
|
||||||
|
// 'futures.login': WSAPIResponse<WSAPILoginResponse, TChannel>;
|
||||||
|
// string: object;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface WsAPIOperationResponseMap<
|
||||||
|
TResponseType extends object = object,
|
||||||
|
> {
|
||||||
|
'order.create': WSAPIResponse<TResponseType, 'order.cancel'>;
|
||||||
|
'order.amend': WSAPIResponse<TResponseType, 'order.amend'>;
|
||||||
|
'order.cancel': WSAPIResponse<TResponseType, 'order.cancel'>;
|
||||||
|
ping: {
|
||||||
|
retCode: 0 | number;
|
||||||
|
retMsg: 'OK' | string;
|
||||||
|
op: 'pong';
|
||||||
|
data: [string];
|
||||||
|
connId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 'spot.login': WSAPIResponse<WSAPILoginResponse, 'spot.login'>;
|
||||||
|
// 'futures.login': WSAPIResponse<WSAPILoginResponse, 'futures.login'>;
|
||||||
|
|
||||||
|
// 'spot.order_place': WSAPIResponse<TResponseType, 'spot.order_place'>;
|
||||||
|
// 'spot.order_cancel': WSAPIResponse<TResponseType, 'spot.order_cancel'>;
|
||||||
|
// 'spot.order_cancel_ids': WSAPIResponse<
|
||||||
|
// TResponseType,
|
||||||
|
// 'spot.order_cancel_ids'
|
||||||
|
// >;
|
||||||
|
// 'spot.order_cancel_cp': WSAPIResponse<TResponseType, 'spot.order_cancel_cp'>;
|
||||||
|
// 'spot.order_amend': WSAPIResponse<TResponseType, 'spot.order_amend'>;
|
||||||
|
// 'spot.order_status': WSAPIResponse<
|
||||||
|
// WSAPIOrderStatusResponse,
|
||||||
|
// 'spot.order_status'
|
||||||
|
// >;
|
||||||
|
// 'futures.order_place': WSAPIResponse<TResponseType[], 'futures.order_place'>;
|
||||||
|
// 'futures.order_batch_place': WSAPIResponse<
|
||||||
|
// TResponseType[],
|
||||||
|
// 'futures.order_batch_place'
|
||||||
|
// >;
|
||||||
|
// 'futures.order_cancel': WSAPIResponse<TResponseType, 'futures.order_cancel'>;
|
||||||
|
// 'futures.order_cancel_cp': WSAPIResponse<
|
||||||
|
// TResponseType,
|
||||||
|
// 'futures.order_cancel_cp'
|
||||||
|
// >;
|
||||||
|
// 'futures.order_amend': WSAPIResponse<TResponseType, 'futures.order_amend'>;
|
||||||
|
// 'futures.order_list': WSAPIResponse<TResponseType[], 'futures.order_list'>;
|
||||||
|
// 'futures.order_status': WSAPIResponse<
|
||||||
|
// WSAPIOrderStatusResponse,
|
||||||
|
// 'futures.order_status'
|
||||||
|
// >;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import WebSocket from 'isomorphic-ws';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CategoryV5,
|
CategoryV5,
|
||||||
ExecTypeV5,
|
ExecTypeV5,
|
||||||
@@ -18,7 +20,23 @@ import {
|
|||||||
TPSLModeV5,
|
TPSLModeV5,
|
||||||
TradeModeV5,
|
TradeModeV5,
|
||||||
} from '../shared-v5';
|
} from '../shared-v5';
|
||||||
import { WsKey } from '.';
|
|
||||||
|
import { WsKey } from './ws-general';
|
||||||
|
|
||||||
|
export interface MessageEventLike {
|
||||||
|
target: WebSocket;
|
||||||
|
type: 'message';
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMessageEvent(msg: unknown): msg is MessageEventLike {
|
||||||
|
if (typeof msg !== 'object' || !msg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = msg as MessageEventLike;
|
||||||
|
return message['type'] === 'message' && typeof message['data'] === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
export interface WSPublicTopicEventV5<TTopic extends string, TType, TData> {
|
export interface WSPublicTopicEventV5<TTopic extends string, TType, TData> {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
@@ -82,10 +82,23 @@ export type WsTopic = WsPublicTopics | WsPrivateTopic;
|
|||||||
|
|
||||||
/** This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) */
|
/** This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) */
|
||||||
export type WsKey = (typeof WS_KEY_MAP)[keyof typeof WS_KEY_MAP];
|
export type WsKey = (typeof WS_KEY_MAP)[keyof typeof WS_KEY_MAP];
|
||||||
|
export type WsMarket = 'all';
|
||||||
|
|
||||||
export interface WSClientConfigurableOptions {
|
export interface WSClientConfigurableOptions {
|
||||||
|
/** Your API key */
|
||||||
key?: string;
|
key?: string;
|
||||||
|
|
||||||
|
/** Your API secret */
|
||||||
secret?: string;
|
secret?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to `true` to connect to Bybit's testnet environment.
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
*
|
||||||
|
* - If demo trading, `testnet` should be set to false!
|
||||||
|
* - If testing a strategy, use demo trading instead. Testnet market data is very different from real market conditions.
|
||||||
|
*/
|
||||||
testnet?: boolean;
|
testnet?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,28 +109,54 @@ export interface WSClientConfigurableOptions {
|
|||||||
demoTrading?: boolean;
|
demoTrading?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The API group this client should connect to.
|
* The API group this client should connect to. The V5 market is currently used by default.
|
||||||
*
|
*
|
||||||
* For the V3 APIs use `v3` as the market (spot/unified margin/usdc/account asset/copy trading)
|
* For the V3 APIs use `v3` as the market (spot/unified margin/usdc/account asset/copy trading)
|
||||||
*/
|
*/
|
||||||
market: APIMarket;
|
market?: APIMarket;
|
||||||
|
|
||||||
pongTimeout?: number;
|
/** Define a recv window when preparing a private websocket signature. This is in milliseconds, so 5000 == 5 seconds */
|
||||||
pingInterval?: number;
|
|
||||||
reconnectTimeout?: number;
|
|
||||||
/** Override the recv window for authenticating over websockets (default: 5000 ms) */
|
|
||||||
recvWindow?: number;
|
recvWindow?: number;
|
||||||
|
|
||||||
|
/** How often to check if the connection is alive */
|
||||||
|
pingInterval?: number;
|
||||||
|
|
||||||
|
/** How long to wait for a pong (heartbeat reply) before assuming the connection is dead */
|
||||||
|
pongTimeout?: number;
|
||||||
|
|
||||||
|
/** Delay in milliseconds before respawning the connection */
|
||||||
|
reconnectTimeout?: number;
|
||||||
|
|
||||||
restOptions?: RestClientOptions;
|
restOptions?: RestClientOptions;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
requestOptions?: any;
|
requestOptions?: any;
|
||||||
wsUrl?: string;
|
wsUrl?: string;
|
||||||
/** If true, fetch server time before trying to authenticate (disabled by default) */
|
|
||||||
fetchTimeOffsetBeforeAuth?: boolean;
|
/**
|
||||||
|
* Allows you to provide a custom "signMessage" function, e.g. to use node's much faster createHmac method
|
||||||
|
*
|
||||||
|
* Look in the examples folder for a demonstration on using node's createHmac instead.
|
||||||
|
*/
|
||||||
|
customSignMessageFn?: (message: string, secret: string) => Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you authenticated the WS API before, automatically try to
|
||||||
|
* re-authenticate the WS API if you're disconnected/reconnected for any reason.
|
||||||
|
*/
|
||||||
|
reauthWSAPIOnReconnect?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WS configuration that's always defined, regardless of user configuration
|
||||||
|
* (usually comes from defaults if there's no user-provided values)
|
||||||
|
*/
|
||||||
export interface WebsocketClientOptions extends WSClientConfigurableOptions {
|
export interface WebsocketClientOptions extends WSClientConfigurableOptions {
|
||||||
market: APIMarket;
|
market: APIMarket;
|
||||||
pongTimeout: number;
|
pongTimeout: number;
|
||||||
pingInterval: number;
|
pingInterval: number;
|
||||||
reconnectTimeout: number;
|
reconnectTimeout: number;
|
||||||
|
recvWindow: number;
|
||||||
|
authPrivateConnectionsOnConnect: boolean;
|
||||||
|
authPrivateRequests: boolean;
|
||||||
|
reauthWSAPIOnReconnect: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
1305
src/util/BaseWSClient.ts
Normal file
1305
src/util/BaseWSClient.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,22 +4,13 @@
|
|||||||
export type LogParams = null | any;
|
export type LogParams = null | any;
|
||||||
|
|
||||||
export const DefaultLogger = {
|
export const DefaultLogger = {
|
||||||
/** Ping/pong events and other raw messages that might be noisy */
|
/** Ping/pong events and other raw messages that might be noisy. Enable this while troubleshooting. */
|
||||||
silly: (...params: LogParams): void => {
|
trace: (..._params: LogParams): void => {
|
||||||
// console.log(params);
|
// console.log(_params);
|
||||||
},
|
|
||||||
debug: (...params: LogParams): void => {
|
|
||||||
console.log(params);
|
|
||||||
},
|
|
||||||
notice: (...params: LogParams): void => {
|
|
||||||
console.log(params);
|
|
||||||
},
|
},
|
||||||
info: (...params: LogParams): void => {
|
info: (...params: LogParams): void => {
|
||||||
console.info(params);
|
console.info(params);
|
||||||
},
|
},
|
||||||
warning: (...params: LogParams): void => {
|
|
||||||
console.error(params);
|
|
||||||
},
|
|
||||||
error: (...params: LogParams): void => {
|
error: (...params: LogParams): void => {
|
||||||
console.error(params);
|
console.error(params);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
WebsocketSucceededTopicSubscriptionConfirmationEvent,
|
WebsocketSucceededTopicSubscriptionConfirmationEvent,
|
||||||
WebsocketTopicSubscriptionConfirmationEvent,
|
WebsocketTopicSubscriptionConfirmationEvent,
|
||||||
} from '../types/websockets/ws-confirmations';
|
} from '../types/websockets/ws-confirmations';
|
||||||
|
import { WSAPIResponse, WS_API_Operations } from '../types/websockets/ws-api';
|
||||||
|
|
||||||
export interface RestClientOptions {
|
export interface RestClientOptions {
|
||||||
/** Your API key */
|
/** Your API key */
|
||||||
@@ -199,6 +200,20 @@ export function isTopicSubscriptionConfirmation(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isWSAPIResponse(
|
||||||
|
msg: unknown,
|
||||||
|
): msg is Omit<WSAPIResponse, 'wsKey'> {
|
||||||
|
if (typeof msg !== 'object' || !msg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof msg['op'] !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (WS_API_Operations as string[]).includes(msg['op']);
|
||||||
|
}
|
||||||
|
|
||||||
export function isTopicSubscriptionSuccess(
|
export function isTopicSubscriptionSuccess(
|
||||||
msg: unknown,
|
msg: unknown,
|
||||||
): msg is WebsocketSucceededTopicSubscriptionConfirmationEvent {
|
): msg is WebsocketSucceededTopicSubscriptionConfirmationEvent {
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ export function isDeepObjectMatch(object1: unknown, object2: unknown): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFERRED_PROMISE_REF = {
|
export const DEFERRED_PROMISE_REF = {
|
||||||
CONNECTION_IN_PROGRESS: 'CONNECTION_IN_PROGRESS',
|
CONNECTION_IN_PROGRESS: 'CONNECTION_IN_PROGRESS',
|
||||||
|
AUTHENTICATION_IN_PROGRESS: 'AUTHENTICATION_IN_PROGRESS',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type DeferredPromiseRef =
|
type DeferredPromiseRef =
|
||||||
@@ -266,6 +267,15 @@ export class WsStore<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAuthenticationInProgressPromise(
|
||||||
|
wsKey: WsKey,
|
||||||
|
): DeferredPromise<WSConnectedResult & { event: any }> | undefined {
|
||||||
|
return this.getDeferredPromise(
|
||||||
|
wsKey,
|
||||||
|
DEFERRED_PROMISE_REF.AUTHENTICATION_IN_PROGRESS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a deferred promise designed to track a connection attempt in progress.
|
* Create a deferred promise designed to track a connection attempt in progress.
|
||||||
*
|
*
|
||||||
@@ -282,6 +292,17 @@ export class WsStore<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createAuthenticationInProgressPromise(
|
||||||
|
wsKey: WsKey,
|
||||||
|
throwIfExists: boolean,
|
||||||
|
): DeferredPromise<WSConnectedResult & { event: any }> {
|
||||||
|
return this.createDeferredPromise(
|
||||||
|
wsKey,
|
||||||
|
DEFERRED_PROMISE_REF.AUTHENTICATION_IN_PROGRESS,
|
||||||
|
throwIfExists,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Remove promise designed to track a connection attempt in progress */
|
/** Remove promise designed to track a connection attempt in progress */
|
||||||
removeConnectingInProgressPromise(wsKey: WsKey): void {
|
removeConnectingInProgressPromise(wsKey: WsKey): void {
|
||||||
return this.removeDeferredPromise(
|
return this.removeDeferredPromise(
|
||||||
@@ -290,6 +311,13 @@ export class WsStore<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeAuthenticationInProgressPromise(wsKey: WsKey): void {
|
||||||
|
return this.removeDeferredPromise(
|
||||||
|
wsKey,
|
||||||
|
DEFERRED_PROMISE_REF.AUTHENTICATION_IN_PROGRESS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* connection state */
|
/* connection state */
|
||||||
|
|
||||||
isWsOpen(key: WsKey): boolean {
|
isWsOpen(key: WsKey): boolean {
|
||||||
|
|||||||
@@ -1,7 +1,141 @@
|
|||||||
import WebSocket from 'isomorphic-ws';
|
import WebSocket from 'isomorphic-ws';
|
||||||
|
import {
|
||||||
|
APIMarket,
|
||||||
|
CategoryV5,
|
||||||
|
WebsocketClientOptions,
|
||||||
|
WsKey,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
import { APIMarket, CategoryV5, WebsocketClientOptions, WsKey } from '../types';
|
import { DefaultLogger } from '../logger';
|
||||||
import { DefaultLogger } from './logger';
|
import { WSAPIRequest } from '../../types/websockets/ws-api';
|
||||||
|
|
||||||
|
export const WS_LOGGER_CATEGORY = { category: 'bybit-ws' };
|
||||||
|
|
||||||
|
export const WS_KEY_MAP = {
|
||||||
|
inverse: 'inverse',
|
||||||
|
linearPrivate: 'linearPrivate',
|
||||||
|
linearPublic: 'linearPublic',
|
||||||
|
spotPrivate: 'spotPrivate',
|
||||||
|
spotPublic: 'spotPublic',
|
||||||
|
spotV3Private: 'spotV3Private',
|
||||||
|
spotV3Public: 'spotV3Public',
|
||||||
|
usdcOptionPrivate: 'usdcOptionPrivate',
|
||||||
|
usdcOptionPublic: 'usdcOptionPublic',
|
||||||
|
usdcPerpPrivate: 'usdcPerpPrivate',
|
||||||
|
usdcPerpPublic: 'usdcPerpPublic',
|
||||||
|
unifiedPrivate: 'unifiedPrivate',
|
||||||
|
unifiedOptionPublic: 'unifiedOptionPublic',
|
||||||
|
unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic',
|
||||||
|
unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic',
|
||||||
|
contractUSDTPublic: 'contractUSDTPublic',
|
||||||
|
contractUSDTPrivate: 'contractUSDTPrivate',
|
||||||
|
contractInversePublic: 'contractInversePublic',
|
||||||
|
contractInversePrivate: 'contractInversePrivate',
|
||||||
|
v5SpotPublic: 'v5SpotPublic',
|
||||||
|
v5LinearPublic: 'v5LinearPublic',
|
||||||
|
v5InversePublic: 'v5InversePublic',
|
||||||
|
v5OptionPublic: 'v5OptionPublic',
|
||||||
|
v5Private: 'v5Private',
|
||||||
|
/**
|
||||||
|
* The V5 Websocket API (for sending orders over WS)
|
||||||
|
*/
|
||||||
|
v5PrivateTrade: 'v5PrivateTrade',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [
|
||||||
|
WS_KEY_MAP.spotV3Private,
|
||||||
|
WS_KEY_MAP.usdcOptionPrivate,
|
||||||
|
WS_KEY_MAP.usdcPerpPrivate,
|
||||||
|
WS_KEY_MAP.unifiedPrivate,
|
||||||
|
WS_KEY_MAP.contractUSDTPrivate,
|
||||||
|
WS_KEY_MAP.contractInversePrivate,
|
||||||
|
WS_KEY_MAP.v5Private,
|
||||||
|
WS_KEY_MAP.v5PrivateTrade,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PUBLIC_WS_KEYS = [
|
||||||
|
WS_KEY_MAP.linearPublic,
|
||||||
|
WS_KEY_MAP.spotPublic,
|
||||||
|
WS_KEY_MAP.spotV3Public,
|
||||||
|
WS_KEY_MAP.usdcOptionPublic,
|
||||||
|
WS_KEY_MAP.usdcPerpPublic,
|
||||||
|
WS_KEY_MAP.unifiedOptionPublic,
|
||||||
|
WS_KEY_MAP.unifiedPerpUSDTPublic,
|
||||||
|
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 = [
|
||||||
|
'stop_order',
|
||||||
|
'outboundAccountInfo',
|
||||||
|
'executionReport',
|
||||||
|
'ticketInfo',
|
||||||
|
// copy trading apis
|
||||||
|
'copyTradePosition',
|
||||||
|
'copyTradeOrder',
|
||||||
|
'copyTradeExecution',
|
||||||
|
'copyTradeWallet',
|
||||||
|
// usdc options
|
||||||
|
'user.openapi.option.position',
|
||||||
|
'user.openapi.option.trade',
|
||||||
|
'user.order',
|
||||||
|
'user.openapi.option.order',
|
||||||
|
'user.service',
|
||||||
|
'user.openapi.greeks',
|
||||||
|
'user.mmp.event',
|
||||||
|
// usdc perps
|
||||||
|
'user.openapi.perp.position',
|
||||||
|
'user.openapi.perp.trade',
|
||||||
|
'user.openapi.perp.order',
|
||||||
|
'user.service',
|
||||||
|
// unified margin
|
||||||
|
'user.position.unifiedAccount',
|
||||||
|
'user.execution.unifiedAccount',
|
||||||
|
'user.order.unifiedAccount',
|
||||||
|
'user.wallet.unifiedAccount',
|
||||||
|
'user.greeks.unifiedAccount',
|
||||||
|
// contract v3
|
||||||
|
'user.position.contractAccount',
|
||||||
|
'user.execution.contractAccount',
|
||||||
|
'user.order.contractAccount',
|
||||||
|
'user.wallet.contractAccount',
|
||||||
|
// v5
|
||||||
|
'position',
|
||||||
|
'execution',
|
||||||
|
'order',
|
||||||
|
'wallet',
|
||||||
|
'greeks',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalised internal format for a request (subscribe/unsubscribe/etc) on a topic, with optional parameters.
|
||||||
|
*
|
||||||
|
* - Topic: the topic this event is for
|
||||||
|
* - Payload: the parameters to include, optional. E.g. auth requires key + sign. Some topics allow configurable parameters.
|
||||||
|
* - Category: required for bybit, since different categories have different public endpoints
|
||||||
|
*/
|
||||||
|
export interface WsTopicRequest<
|
||||||
|
TWSTopic extends string = string,
|
||||||
|
TWSPayload = unknown,
|
||||||
|
> {
|
||||||
|
topic: TWSTopic;
|
||||||
|
payload?: TWSPayload;
|
||||||
|
category?: CategoryV5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conveniently allow users to request a topic either as string topics or objects (containing string topic + params)
|
||||||
|
*/
|
||||||
|
export type WsTopicRequestOrStringTopic<
|
||||||
|
TWSTopic extends string,
|
||||||
|
TWSPayload = unknown,
|
||||||
|
> = WsTopicRequest<TWSTopic, TWSPayload> | string;
|
||||||
|
|
||||||
interface NetworkMapV3 {
|
interface NetworkMapV3 {
|
||||||
livenet: string;
|
livenet: string;
|
||||||
@@ -33,7 +167,55 @@ export const WS_BASE_URL_MAP: Record<
|
|||||||
APIMarket,
|
APIMarket,
|
||||||
Record<PublicPrivateNetwork, NetworkMapV3>
|
Record<PublicPrivateNetwork, NetworkMapV3>
|
||||||
> &
|
> &
|
||||||
Record<PublicOnlyWsKeys, Record<'public', NetworkMapV3>> = {
|
Record<PublicOnlyWsKeys, Record<'public', NetworkMapV3>> &
|
||||||
|
Record<
|
||||||
|
typeof WS_KEY_MAP.v5PrivateTrade,
|
||||||
|
Record<PublicPrivateNetwork, NetworkMapV3>
|
||||||
|
> = {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
v5PrivateTrade: {
|
||||||
|
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/trade',
|
||||||
|
testnet: 'wss://stream-testnet.bybit.com/v5/trade',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
inverse: {
|
inverse: {
|
||||||
public: {
|
public: {
|
||||||
livenet: 'wss://stream.bybit.com/realtime',
|
livenet: 'wss://stream.bybit.com/realtime',
|
||||||
@@ -154,139 +336,8 @@ export const WS_BASE_URL_MAP: Record<
|
|||||||
testnet: 'wss://stream-testnet.bybit.com/contract/private/v3',
|
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 = {
|
|
||||||
inverse: 'inverse',
|
|
||||||
linearPrivate: 'linearPrivate',
|
|
||||||
linearPublic: 'linearPublic',
|
|
||||||
spotPrivate: 'spotPrivate',
|
|
||||||
spotPublic: 'spotPublic',
|
|
||||||
spotV3Private: 'spotV3Private',
|
|
||||||
spotV3Public: 'spotV3Public',
|
|
||||||
usdcOptionPrivate: 'usdcOptionPrivate',
|
|
||||||
usdcOptionPublic: 'usdcOptionPublic',
|
|
||||||
usdcPerpPrivate: 'usdcPerpPrivate',
|
|
||||||
usdcPerpPublic: 'usdcPerpPublic',
|
|
||||||
unifiedPrivate: 'unifiedPrivate',
|
|
||||||
unifiedOptionPublic: 'unifiedOptionPublic',
|
|
||||||
unifiedPerpUSDTPublic: 'unifiedPerpUSDTPublic',
|
|
||||||
unifiedPerpUSDCPublic: 'unifiedPerpUSDCPublic',
|
|
||||||
contractUSDTPublic: 'contractUSDTPublic',
|
|
||||||
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[] = [
|
|
||||||
WS_KEY_MAP.spotV3Private,
|
|
||||||
WS_KEY_MAP.usdcOptionPrivate,
|
|
||||||
WS_KEY_MAP.usdcPerpPrivate,
|
|
||||||
WS_KEY_MAP.unifiedPrivate,
|
|
||||||
WS_KEY_MAP.contractUSDTPrivate,
|
|
||||||
WS_KEY_MAP.contractInversePrivate,
|
|
||||||
WS_KEY_MAP.v5Private,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const PUBLIC_WS_KEYS = [
|
|
||||||
WS_KEY_MAP.linearPublic,
|
|
||||||
WS_KEY_MAP.spotPublic,
|
|
||||||
WS_KEY_MAP.spotV3Public,
|
|
||||||
WS_KEY_MAP.usdcOptionPublic,
|
|
||||||
WS_KEY_MAP.usdcPerpPublic,
|
|
||||||
WS_KEY_MAP.unifiedOptionPublic,
|
|
||||||
WS_KEY_MAP.unifiedPerpUSDTPublic,
|
|
||||||
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 = [
|
|
||||||
'stop_order',
|
|
||||||
'outboundAccountInfo',
|
|
||||||
'executionReport',
|
|
||||||
'ticketInfo',
|
|
||||||
// copy trading apis
|
|
||||||
'copyTradePosition',
|
|
||||||
'copyTradeOrder',
|
|
||||||
'copyTradeExecution',
|
|
||||||
'copyTradeWallet',
|
|
||||||
// usdc options
|
|
||||||
'user.openapi.option.position',
|
|
||||||
'user.openapi.option.trade',
|
|
||||||
'user.order',
|
|
||||||
'user.openapi.option.order',
|
|
||||||
'user.service',
|
|
||||||
'user.openapi.greeks',
|
|
||||||
'user.mmp.event',
|
|
||||||
// usdc perps
|
|
||||||
'user.openapi.perp.position',
|
|
||||||
'user.openapi.perp.trade',
|
|
||||||
'user.openapi.perp.order',
|
|
||||||
'user.service',
|
|
||||||
// unified margin
|
|
||||||
'user.position.unifiedAccount',
|
|
||||||
'user.execution.unifiedAccount',
|
|
||||||
'user.order.unifiedAccount',
|
|
||||||
'user.wallet.unifiedAccount',
|
|
||||||
'user.greeks.unifiedAccount',
|
|
||||||
// contract v3
|
|
||||||
'user.position.contractAccount',
|
|
||||||
'user.execution.contractAccount',
|
|
||||||
'user.order.contractAccount',
|
|
||||||
'user.wallet.contractAccount',
|
|
||||||
// v5
|
|
||||||
'position',
|
|
||||||
'execution',
|
|
||||||
'order',
|
|
||||||
'wallet',
|
|
||||||
'greeks',
|
|
||||||
];
|
|
||||||
|
|
||||||
export function isPrivateWsTopic(topic: string): boolean {
|
export function isPrivateWsTopic(topic: string): boolean {
|
||||||
return PRIVATE_TOPICS.includes(topic);
|
return PRIVATE_TOPICS.includes(topic);
|
||||||
}
|
}
|
||||||
@@ -416,6 +467,24 @@ export function getWsUrl(
|
|||||||
const networkKey = isTestnet ? 'testnet' : 'livenet';
|
const networkKey = isTestnet ? 'testnet' : 'livenet';
|
||||||
|
|
||||||
switch (wsKey) {
|
switch (wsKey) {
|
||||||
|
case WS_KEY_MAP.v5Private: {
|
||||||
|
return WS_BASE_URL_MAP.v5.private[networkKey];
|
||||||
|
}
|
||||||
|
case WS_KEY_MAP.v5PrivateTrade: {
|
||||||
|
return WS_BASE_URL_MAP[wsKey].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];
|
||||||
|
}
|
||||||
case WS_KEY_MAP.linearPublic: {
|
case WS_KEY_MAP.linearPublic: {
|
||||||
return WS_BASE_URL_MAP.linear.public[networkKey];
|
return WS_BASE_URL_MAP.linear.public[networkKey];
|
||||||
}
|
}
|
||||||
@@ -474,21 +543,6 @@ export function getWsUrl(
|
|||||||
case WS_KEY_MAP.contractUSDTPublic: {
|
case WS_KEY_MAP.contractUSDTPublic: {
|
||||||
return WS_BASE_URL_MAP.contractUSDT.public[networkKey];
|
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: {
|
default: {
|
||||||
logger.error('getWsUrl(): Unhandled wsKey: ', {
|
logger.error('getWsUrl(): Unhandled wsKey: ', {
|
||||||
category: 'bybit-ws',
|
category: 'bybit-ws',
|
||||||
@@ -569,3 +623,13 @@ export function safeTerminateWs(ws?: WebSocket | unknown) {
|
|||||||
ws.terminate();
|
ws.terminate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* WS API promises are stored using a primary key. This key is constructed using
|
||||||
|
* properties found in every request & reply.
|
||||||
|
*/
|
||||||
|
export function getPromiseRefForWSAPIRequest(
|
||||||
|
requestEvent: WSAPIRequest<unknown>,
|
||||||
|
): string {
|
||||||
|
const promiseRef = [requestEvent.op, requestEvent.reqId].join('_');
|
||||||
|
return promiseRef;
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user