usdc options public ws test

This commit is contained in:
tiagosiebler
2022-09-16 13:13:49 +01:00
parent 28485c0068
commit d2ba5d3e01
9 changed files with 319 additions and 115 deletions

View File

@@ -158,7 +158,7 @@ The WebsocketClient can be configured to a specific API group using the market p
| 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 private topics. |
| 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. |

View File

@@ -13,9 +13,10 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src';
{
// key: key,
// secret: secret,
market: 'linear',
// market: 'linear',
// market: 'inverse',
// market: 'spot',
market: 'usdcOption',
},
logger
);
@@ -51,10 +52,15 @@ import { DefaultLogger, WS_KEY_MAP, WebsocketClient } from '../src';
// Linear
wsClient.subscribe('trade.BTCUSDT');
setTimeout(() => {
console.log('unsubscribing');
wsClient.unsubscribe('trade.BTCUSDT');
}, 5 * 1000);
// usdc options
wsClient.subscribe(`recenttrades.BTC`);
wsClient.subscribe(`recenttrades.ETH`);
wsClient.subscribe(`recenttrades.SOL`);
// setTimeout(() => {
// console.log('unsubscribing');
// wsClient.unsubscribe('trade.BTCUSDT');
// }, 5 * 1000);
// For spot, request public connection first then send required topics on 'open'
// wsClient.connectPublic();

View File

@@ -2,12 +2,14 @@ import { InverseClient } from '../inverse-client';
import { LinearClient } from '../linear-client';
import { SpotClient } from '../spot-client';
import { SpotClientV3 } from '../spot-client-v3';
import { USDCOptionClient } from '../usdc-option-client';
export type RESTClient =
| InverseClient
| LinearClient
| SpotClient
| SpotClientV3;
| SpotClientV3
| USDCOptionClient;
export type numberInString = string;

View File

@@ -1,7 +1,7 @@
import { RestClientOptions, WS_KEY_MAP } from '../util';
/** For spot markets, spotV3 is recommended */
export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotv3'; //| 'v3';
export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotv3' | 'usdcOption';
// Same as inverse futures
export type WsPublicInverseTopic =

View File

@@ -61,15 +61,23 @@ export function getRestBaseUrl(
return exchangeBaseUrls.testnet;
}
export function isWsPong(response: any) {
if (response.pong || response.ping) {
export function isWsPong(msg: any): boolean {
if (!msg) {
return false;
}
if (msg.pong || msg.ping) {
return true;
}
if (msg['op'] === 'pong') {
return true;
}
return (
response.request &&
response.request.op === 'ping' &&
response.ret_msg === 'pong' &&
response.success === true
msg.request &&
msg.request.op === 'ping' &&
msg.ret_msg === 'pong' &&
msg.success === true
);
}

View File

@@ -57,6 +57,18 @@ export const WS_BASE_URL_MAP: Record<
testnet: 'wss://stream-testnet.bybit.com/spot/private/v3',
},
},
usdcOption: {
public: {
livenet: 'wss://stream.bybit.com/trade/option/usdc/public/v1',
livenet2: 'wss://stream.bytick.com/trade/option/usdc/public/v1',
testnet: 'wss://stream-testnet.bybit.com/trade/option/usdc/public/v1',
},
private: {
livenet: 'wss://stream.bybit.com/trade/option/usdc/private/v1',
livenet2: 'wss://stream.bytick.com/trade/option/usdc/private/v1',
testnet: 'wss://stream-testnet.bybit.com/trade/option/usdc/private/v1',
},
},
};
export const WS_KEY_MAP = {
@@ -67,6 +79,10 @@ export const WS_KEY_MAP = {
spotPublic: 'spotPublic',
spotV3Private: 'spotV3Private',
spotV3Public: 'spotV3Public',
usdcOptionPrivate: 'usdcOptionPrivate',
usdcOptionPublic: 'usdcOptionPublic',
// usdcPerpPrivate: 'usdcPerpPrivate',
// usdcPerpPublic: 'usdcPerpPublic',
} as const;
export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private];
@@ -74,51 +90,103 @@ export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private];
export const PUBLIC_WS_KEYS = [
WS_KEY_MAP.linearPublic,
WS_KEY_MAP.spotPublic,
WS_KEY_MAP.spotV3Public,
WS_KEY_MAP.usdcOptionPublic,
] as string[];
export function getLinearWsKeyForTopic(topic: string): WsKey {
const privateTopics = [
'position',
'execution',
'order',
'stop_order',
'wallet',
];
if (privateTopics.includes(topic)) {
return WS_KEY_MAP.linearPrivate;
}
/** 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',
// 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',
];
return WS_KEY_MAP.linearPublic;
export function getWsKeyForTopic(
market: APIMarket,
topic: string,
isPrivate?: boolean
): WsKey {
const isPrivateTopic = isPrivate === true || PRIVATE_TOPICS.includes(topic);
switch (market) {
case 'inverse': {
return WS_KEY_MAP.inverse;
}
case 'linear': {
return isPrivateTopic
? WS_KEY_MAP.linearPrivate
: WS_KEY_MAP.linearPublic;
}
case 'spot': {
return isPrivateTopic ? WS_KEY_MAP.spotPrivate : WS_KEY_MAP.spotPublic;
}
case 'spotv3': {
return isPrivateTopic
? WS_KEY_MAP.spotV3Private
: WS_KEY_MAP.spotV3Public;
}
case 'usdcOption': {
return isPrivateTopic
? WS_KEY_MAP.usdcOptionPrivate
: WS_KEY_MAP.usdcOptionPublic;
}
default: {
throw neverGuard(market, `getWsKeyForTopic(): Unhandled market`);
}
}
}
export function getSpotWsKeyForTopic(
export function getUsdcWsKeyForTopic(
topic: string,
apiVersion: 'v1' | 'v3'
subGroup: 'option' | 'perp'
): WsKey {
const privateTopics = [
'position',
'execution',
'order',
'stop_order',
'outboundAccountInfo',
'executionReport',
'ticketInfo',
];
if (apiVersion === 'v3') {
if (privateTopics.includes(topic)) {
return WS_KEY_MAP.spotV3Private;
}
return WS_KEY_MAP.spotV3Public;
const isPrivateTopic = PRIVATE_TOPICS.includes(topic);
if (subGroup === 'option') {
return isPrivateTopic
? WS_KEY_MAP.usdcOptionPrivate
: WS_KEY_MAP.usdcOptionPublic;
}
if (privateTopics.includes(topic)) {
return WS_KEY_MAP.spotPrivate;
}
return WS_KEY_MAP.spotPublic;
return isPrivateTopic
? WS_KEY_MAP.usdcOptionPrivate
: WS_KEY_MAP.usdcOptionPublic;
// return isPrivateTopic
// ? WS_KEY_MAP.usdcPerpPrivate
// : WS_KEY_MAP.usdcPerpPublic;
}
export const WS_ERROR_ENUM = {
NOT_AUTHENTICATED_SPOT_V3: '-1004',
BAD_API_KEY_SPOT_V3: '10003',
};
export function neverGuard(x: never, msg: string): Error {
return new Error(`Unhandled value exception "x", ${msg}`);
}

View File

@@ -22,19 +22,16 @@ import {
import {
serializeParams,
isWsPong,
getLinearWsKeyForTopic,
getSpotWsKeyForTopic,
WsConnectionStateEnum,
PUBLIC_WS_KEYS,
WS_AUTH_ON_CONNECT_KEYS,
WS_KEY_MAP,
DefaultLogger,
WS_BASE_URL_MAP,
getWsKeyForTopic,
neverGuard,
} from './util';
function neverGuard(x: never, msg: string): Error {
return new Error(`Unhandled value exception "x", ${msg}`);
}
import { USDCOptionClient } from './usdc-option-client';
const loggerCategory = { category: 'bybit-ws' };
@@ -94,9 +91,7 @@ export class WebsocketClient extends EventEmitter {
...options,
};
if (this.options.fetchTimeOffsetBeforeAuth) {
this.prepareRESTClient();
}
this.prepareRESTClient();
}
/**
@@ -148,15 +143,28 @@ export class WebsocketClient extends EventEmitter {
this.connectPublic();
break;
}
// if (this.isV3()) {
// this.restClient = new SpotClientV3(
// undefined,
// undefined,
// this.isLivenet(),
// this.options.restOptions,
// this.options.requestOptions
// );
// }
case 'spotv3': {
this.restClient = new SpotClientV3(
undefined,
undefined,
!this.isTestnet(),
this.options.restOptions,
this.options.requestOptions
);
this.connectPublic();
break;
}
case 'usdcOption': {
this.restClient = new USDCOptionClient(
undefined,
undefined,
!this.isTestnet(),
this.options.restOptions,
this.options.requestOptions
);
this.connectPublic();
break;
}
default: {
throw neverGuard(
this.options.market,
@@ -208,25 +216,15 @@ export class WebsocketClient extends EventEmitter {
public connectAll(): Promise<WebSocket | undefined>[] {
switch (this.options.market) {
case 'inverse': {
return [this.connect(WS_KEY_MAP.inverse)];
// only one for inverse
return [this.connectPublic()];
}
case 'linear': {
return [
this.connect(WS_KEY_MAP.linearPublic),
this.connect(WS_KEY_MAP.linearPrivate),
];
}
case 'spot': {
return [
this.connect(WS_KEY_MAP.spotPublic),
this.connect(WS_KEY_MAP.spotPrivate),
];
}
case 'spotv3': {
return [
this.connect(WS_KEY_MAP.spotV3Public),
this.connect(WS_KEY_MAP.spotV3Private),
];
// these all have separate public & private ws endpoints
case 'linear':
case 'spot':
case 'spotv3':
case 'usdcOption': {
return [this.connectPublic(), this.connectPrivate()];
}
default: {
throw neverGuard(this.options.market, `connectAll(): Unhandled market`);
@@ -248,6 +246,9 @@ export class WebsocketClient extends EventEmitter {
case 'spotv3': {
return this.connect(WS_KEY_MAP.spotV3Public);
}
case 'usdcOption': {
return this.connect(WS_KEY_MAP.usdcOptionPublic);
}
default: {
throw neverGuard(
this.options.market,
@@ -257,7 +258,7 @@ export class WebsocketClient extends EventEmitter {
}
}
public connectPrivate(): Promise<WebSocket | undefined> | undefined {
public connectPrivate(): Promise<WebSocket | undefined> {
switch (this.options.market) {
case 'inverse': {
return this.connect(WS_KEY_MAP.inverse);
@@ -271,6 +272,9 @@ export class WebsocketClient extends EventEmitter {
case 'spotv3': {
return this.connect(WS_KEY_MAP.spotV3Private);
}
case 'usdcOption': {
return this.connect(WS_KEY_MAP.usdcOptionPrivate);
}
default: {
throw neverGuard(
this.options.market,
@@ -596,10 +600,15 @@ export class WebsocketClient extends EventEmitter {
// any message can clear the pong timer - wouldn't get a message if the ws dropped
this.clearPongTimer(wsKey);
// this.logger.silly('Received event', { ...this.logger, wsKey, event });
const msg = JSON.parse((event && event.data) || event);
if (msg['success'] || msg?.pong) {
this.logger.silly('Received event', {
...this.logger,
wsKey,
msg: JSON.stringify(msg, null, 2),
});
// TODO: cleanme
if (msg['success'] || msg?.pong || isWsPong(msg)) {
if (isWsPong(msg)) {
this.logger.silly('Received pong', { ...loggerCategory, wsKey });
} else {
@@ -608,6 +617,9 @@ export class WebsocketClient extends EventEmitter {
return;
}
if (msg['finalFragment']) {
return this.emit('response', msg);
}
if (msg?.topic) {
return this.emit('update', msg);
}
@@ -701,6 +713,18 @@ export class WebsocketClient extends EventEmitter {
// 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.usdcOption.public[networkKey];
// }
// case WS_KEY_MAP.usdcPerpPrivate: {
// return WS_BASE_URL_MAP.usdcOption.private[networkKey];
// }
default: {
this.logger.error('getWsUrl(): Unhandled wsKey: ', {
...loggerCategory,
@@ -711,29 +735,6 @@ export class WebsocketClient extends EventEmitter {
}
}
private getWsKeyForTopic(topic: string): WsKey {
switch (this.options.market) {
case 'inverse': {
return WS_KEY_MAP.inverse;
}
case 'linear': {
return getLinearWsKeyForTopic(topic);
}
case 'spot': {
return getSpotWsKeyForTopic(topic, 'v1');
}
case 'spotv3': {
return getSpotWsKeyForTopic(topic, 'v3');
}
default: {
throw neverGuard(
this.options.market,
`connectPublic(): Unhandled market`
);
}
}
}
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`
@@ -742,11 +743,16 @@ export class WebsocketClient extends EventEmitter {
/**
* Add topic/topics to WS subscription list
* @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) {
public subscribe(wsTopics: WsTopic[] | WsTopic, isPrivateTopic?: boolean) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach((topic) =>
this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic)
this.wsStore.addTopic(
getWsKeyForTopic(this.options.market, topic, isPrivateTopic),
topic
)
);
// attempt to send subscription topic per websocket
@@ -776,11 +782,16 @@ export class WebsocketClient extends EventEmitter {
/**
* Remove topic/topics from WS subscription list
* @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) {
public unsubscribe(wsTopics: WsTopic[] | WsTopic, isPrivateTopic?: boolean) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach((topic) =>
this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic)
this.wsStore.deleteTopic(
getWsKeyForTopic(this.options.market, topic, isPrivateTopic),
topic
)
);
this.wsStore.getKeys().forEach((wsKey: WsKey) => {

View File

@@ -0,0 +1,79 @@
import {
WebsocketClient,
WSClientConfigurableOptions,
WS_KEY_MAP,
} from '../../../src';
import {
logAllEvents,
getSilentLogger,
waitForSocketEvent,
WS_OPEN_EVENT_PARTIAL,
} from '../../ws.util';
describe('Public USDC Option Websocket Client', () => {
let wsClient: WebsocketClient;
const wsClientOptions: WSClientConfigurableOptions = {
market: 'usdcOption',
};
beforeAll(() => {
wsClient = new WebsocketClient(
wsClientOptions,
getSilentLogger('expectSuccessNoAuth')
);
// logAllEvents(wsClient);
});
beforeEach(() => {
wsClient.removeAllListeners();
});
afterAll(() => {
wsClient.closeAll();
});
it('should open a public ws connection', async () => {
const wsOpenPromise = waitForSocketEvent(wsClient, 'open');
expect(wsOpenPromise).resolves.toMatchObject({
event: WS_OPEN_EVENT_PARTIAL,
wsKey: WS_KEY_MAP.usdcOptionPublic,
});
await Promise.all([wsOpenPromise]);
});
it('should subscribe to public trade events', async () => {
const wsResponsePromise = waitForSocketEvent(wsClient, 'response');
// const wsUpdatePromise = waitForSocketEvent(wsClient, 'update');
wsClient.subscribe([
'recenttrades.BTC',
'recenttrades.ETH',
'recenttrades.SOL',
]);
try {
expect(await wsResponsePromise).toMatchObject({
success: true,
data: {
failTopics: [],
successTopics: expect.any(Array),
},
type: 'COMMAND_RESP',
});
} catch (e) {
// sub failed
expect(e).toBeFalsy();
}
// Takes a while to get an event from USDC options - testing this manually for now
// try {
// expect(await wsUpdatePromise).toStrictEqual('asdfasdf');
// } catch (e) {
// // no data
// expect(e).toBeFalsy();
// }
});
});

View File

@@ -73,6 +73,36 @@ export function waitForSocketEvent(
});
}
export function listenToSocketEvents(wsClient: WebsocketClient) {
const retVal: Record<
'update' | 'open' | 'response' | 'close' | 'error',
typeof jest.fn
> = {
open: jest.fn(),
response: jest.fn(),
update: jest.fn(),
close: jest.fn(),
error: jest.fn(),
};
wsClient.on('open', retVal.open);
wsClient.on('response', retVal.response);
wsClient.on('update', retVal.update);
wsClient.on('close', retVal.close);
wsClient.on('error', retVal.error);
return {
...retVal,
cleanup: () => {
wsClient.removeListener('open', retVal.open);
wsClient.removeListener('response', retVal.response);
wsClient.removeListener('update', retVal.update);
wsClient.removeListener('close', retVal.close);
wsClient.removeListener('error', retVal.error);
},
};
}
export function logAllEvents(wsClient: WebsocketClient) {
wsClient.on('update', (data) => {
console.log('wsUpdate: ', JSON.stringify(data, null, 2));