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

View File

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

View File

@@ -1,7 +1,7 @@
import { RestClientOptions, WS_KEY_MAP } from '../util'; import { RestClientOptions, WS_KEY_MAP } from '../util';
/** For spot markets, spotV3 is recommended */ /** 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 // Same as inverse futures
export type WsPublicInverseTopic = export type WsPublicInverseTopic =

View File

@@ -61,15 +61,23 @@ export function getRestBaseUrl(
return exchangeBaseUrls.testnet; return exchangeBaseUrls.testnet;
} }
export function isWsPong(response: any) { export function isWsPong(msg: any): boolean {
if (response.pong || response.ping) { if (!msg) {
return false;
}
if (msg.pong || msg.ping) {
return true; return true;
} }
if (msg['op'] === 'pong') {
return true;
}
return ( return (
response.request && msg.request &&
response.request.op === 'ping' && msg.request.op === 'ping' &&
response.ret_msg === 'pong' && msg.ret_msg === 'pong' &&
response.success === true 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', 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 = { export const WS_KEY_MAP = {
@@ -67,6 +79,10 @@ export const WS_KEY_MAP = {
spotPublic: 'spotPublic', spotPublic: 'spotPublic',
spotV3Private: 'spotV3Private', spotV3Private: 'spotV3Private',
spotV3Public: 'spotV3Public', spotV3Public: 'spotV3Public',
usdcOptionPrivate: 'usdcOptionPrivate',
usdcOptionPublic: 'usdcOptionPublic',
// usdcPerpPrivate: 'usdcPerpPrivate',
// usdcPerpPublic: 'usdcPerpPublic',
} as const; } as const;
export const WS_AUTH_ON_CONNECT_KEYS: WsKey[] = [WS_KEY_MAP.spotV3Private]; 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 = [ export const PUBLIC_WS_KEYS = [
WS_KEY_MAP.linearPublic, WS_KEY_MAP.linearPublic,
WS_KEY_MAP.spotPublic, WS_KEY_MAP.spotPublic,
WS_KEY_MAP.spotV3Public,
WS_KEY_MAP.usdcOptionPublic,
] as string[]; ] as string[];
export function getLinearWsKeyForTopic(topic: string): WsKey { /** Used to automatically determine if a sub request should be to the public or private ws (when there's two) */
const privateTopics = [ const PRIVATE_TOPICS = [
'position', 'position',
'execution', 'execution',
'order', 'order',
'stop_order', 'stop_order',
'wallet', 'wallet',
];
if (privateTopics.includes(topic)) {
return WS_KEY_MAP.linearPrivate;
}
return WS_KEY_MAP.linearPublic;
}
export function getSpotWsKeyForTopic(
topic: string,
apiVersion: 'v1' | 'v3'
): WsKey {
const privateTopics = [
'position',
'execution',
'order',
'stop_order',
'outboundAccountInfo', 'outboundAccountInfo',
'executionReport', 'executionReport',
'ticketInfo', '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',
];
if (apiVersion === 'v3') { export function getWsKeyForTopic(
if (privateTopics.includes(topic)) { market: APIMarket,
return WS_KEY_MAP.spotV3Private; topic: string,
isPrivate?: boolean
): WsKey {
const isPrivateTopic = isPrivate === true || PRIVATE_TOPICS.includes(topic);
switch (market) {
case 'inverse': {
return WS_KEY_MAP.inverse;
} }
return WS_KEY_MAP.spotV3Public; 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`);
}
}
}
if (privateTopics.includes(topic)) { export function getUsdcWsKeyForTopic(
return WS_KEY_MAP.spotPrivate; topic: string,
subGroup: 'option' | 'perp'
): WsKey {
const isPrivateTopic = PRIVATE_TOPICS.includes(topic);
if (subGroup === 'option') {
return isPrivateTopic
? WS_KEY_MAP.usdcOptionPrivate
: WS_KEY_MAP.usdcOptionPublic;
} }
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 = { export const WS_ERROR_ENUM = {
NOT_AUTHENTICATED_SPOT_V3: '-1004', NOT_AUTHENTICATED_SPOT_V3: '-1004',
BAD_API_KEY_SPOT_V3: '10003', 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 { import {
serializeParams, serializeParams,
isWsPong, isWsPong,
getLinearWsKeyForTopic,
getSpotWsKeyForTopic,
WsConnectionStateEnum, WsConnectionStateEnum,
PUBLIC_WS_KEYS, PUBLIC_WS_KEYS,
WS_AUTH_ON_CONNECT_KEYS, WS_AUTH_ON_CONNECT_KEYS,
WS_KEY_MAP, WS_KEY_MAP,
DefaultLogger, DefaultLogger,
WS_BASE_URL_MAP, WS_BASE_URL_MAP,
getWsKeyForTopic,
neverGuard,
} from './util'; } from './util';
import { USDCOptionClient } from './usdc-option-client';
function neverGuard(x: never, msg: string): Error {
return new Error(`Unhandled value exception "x", ${msg}`);
}
const loggerCategory = { category: 'bybit-ws' }; const loggerCategory = { category: 'bybit-ws' };
@@ -94,10 +91,8 @@ export class WebsocketClient extends EventEmitter {
...options, ...options,
}; };
if (this.options.fetchTimeOffsetBeforeAuth) {
this.prepareRESTClient(); this.prepareRESTClient();
} }
}
/** /**
* Only used if we fetch exchange time before attempting auth. * Only used if we fetch exchange time before attempting auth.
@@ -148,15 +143,28 @@ export class WebsocketClient extends EventEmitter {
this.connectPublic(); this.connectPublic();
break; break;
} }
// if (this.isV3()) { case 'spotv3': {
// this.restClient = new SpotClientV3( this.restClient = new SpotClientV3(
// undefined, undefined,
// undefined, undefined,
// this.isLivenet(), !this.isTestnet(),
// this.options.restOptions, this.options.restOptions,
// this.options.requestOptions 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: { default: {
throw neverGuard( throw neverGuard(
this.options.market, this.options.market,
@@ -208,25 +216,15 @@ export class WebsocketClient extends EventEmitter {
public connectAll(): Promise<WebSocket | undefined>[] { public connectAll(): Promise<WebSocket | undefined>[] {
switch (this.options.market) { switch (this.options.market) {
case 'inverse': { case 'inverse': {
return [this.connect(WS_KEY_MAP.inverse)]; // only one for inverse
return [this.connectPublic()];
} }
case 'linear': { // these all have separate public & private ws endpoints
return [ case 'linear':
this.connect(WS_KEY_MAP.linearPublic), case 'spot':
this.connect(WS_KEY_MAP.linearPrivate), case 'spotv3':
]; case 'usdcOption': {
} return [this.connectPublic(), this.connectPrivate()];
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),
];
} }
default: { default: {
throw neverGuard(this.options.market, `connectAll(): Unhandled market`); throw neverGuard(this.options.market, `connectAll(): Unhandled market`);
@@ -248,6 +246,9 @@ export class WebsocketClient extends EventEmitter {
case 'spotv3': { 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);
}
default: { default: {
throw neverGuard( throw neverGuard(
this.options.market, 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) { switch (this.options.market) {
case 'inverse': { case 'inverse': {
return this.connect(WS_KEY_MAP.inverse); return this.connect(WS_KEY_MAP.inverse);
@@ -271,6 +272,9 @@ export class WebsocketClient extends EventEmitter {
case 'spotv3': { case 'spotv3': {
return this.connect(WS_KEY_MAP.spotV3Private); return this.connect(WS_KEY_MAP.spotV3Private);
} }
case 'usdcOption': {
return this.connect(WS_KEY_MAP.usdcOptionPrivate);
}
default: { default: {
throw neverGuard( throw neverGuard(
this.options.market, 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 // any message can clear the pong timer - wouldn't get a message if the ws dropped
this.clearPongTimer(wsKey); this.clearPongTimer(wsKey);
// this.logger.silly('Received event', { ...this.logger, wsKey, event });
const msg = JSON.parse((event && event.data) || 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)) { if (isWsPong(msg)) {
this.logger.silly('Received pong', { ...loggerCategory, wsKey }); this.logger.silly('Received pong', { ...loggerCategory, wsKey });
} else { } else {
@@ -608,6 +617,9 @@ export class WebsocketClient extends EventEmitter {
return; return;
} }
if (msg['finalFragment']) {
return this.emit('response', msg);
}
if (msg?.topic) { if (msg?.topic) {
return this.emit('update', msg); return this.emit('update', msg);
} }
@@ -701,6 +713,18 @@ export class WebsocketClient extends EventEmitter {
// private and public are on the same WS connection // private and public are on the same WS connection
return WS_BASE_URL_MAP.inverse.public[networkKey]; 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: { default: {
this.logger.error('getWsUrl(): Unhandled wsKey: ', { this.logger.error('getWsUrl(): Unhandled wsKey: ', {
...loggerCategory, ...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) { private wrongMarketError(market: APIMarket) {
return new Error( 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 spot topics`
@@ -742,11 +743,16 @@ export class WebsocketClient extends EventEmitter {
/** /**
* Add topic/topics to WS subscription list * 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]; const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach((topic) => 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 // attempt to send subscription topic per websocket
@@ -776,11 +782,16 @@ export class WebsocketClient extends EventEmitter {
/** /**
* Remove topic/topics from WS subscription list * 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]; const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach((topic) => 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) => { 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) { export function logAllEvents(wsClient: WebsocketClient) {
wsClient.on('update', (data) => { wsClient.on('update', (data) => {
console.log('wsUpdate: ', JSON.stringify(data, null, 2)); console.log('wsUpdate: ', JSON.stringify(data, null, 2));