Merge pull request #75 from peepopoggers/master
Support for Linear (USDT) on the updated typescript version.
This commit is contained in:
2
.github/workflows/npmpublish.yml
vendored
2
.github/workflows/npmpublish.yml
vendored
@@ -42,6 +42,8 @@ jobs:
|
||||
|
||||
- run: npm ci
|
||||
if: steps.version-updated.outputs.has-updated
|
||||
- run: npm run clean
|
||||
if: steps.version-updated.outputs.has-updated
|
||||
- run: npm run build
|
||||
if: steps.version-updated.outputs.has-updated
|
||||
- run: npm publish
|
||||
|
||||
127
README.md
127
README.md
@@ -13,11 +13,12 @@ A production-ready Node.js connector for the Bybit APIs and WebSockets, with Typ
|
||||
## Issues & Discussion
|
||||
- Issues? Check the [issues tab](https://github.com/tiagosiebler/bybit-api/issues).
|
||||
- Discuss & collaborate with other node devs? Join our [Node.js Algo Traders](https://t.me/nodetraders) engineering community on telegram.
|
||||
- `'bybit-api' has no exported member 'RestClient'`: use `InverseClient` instead of `RestClient`
|
||||
|
||||
## Documentation
|
||||
Most methods accept JS objects. These can be populated using parameters specified by Bybit's API documentation.
|
||||
- [Bybit API Inverse Documentation](https://bybit-exchange.github.io/docs/inverse/#t-introduction).
|
||||
- [Bybit API Linear Documentation (not supported yet)](https://bybit-exchange.github.io/docs/linear/#t-introduction)
|
||||
- [Bybit API Linear Documentation](https://bybit-exchange.github.io/docs/linear/#t-introduction)
|
||||
|
||||
## Structure
|
||||
This project uses typescript. Resources are stored in 3 key structures:
|
||||
@@ -39,13 +40,10 @@ Build a bundle using webpack:
|
||||
The bundle can be found in `dist/`. Altough usage should be largely consistent, smaller differences will exist. Documentation is still TODO.
|
||||
|
||||
### Inverse Contracts
|
||||
#### Rest client
|
||||
```javascript
|
||||
const { RestClient } = require('bybit-api');
|
||||
Since inverse and linear (USDT) contracts don't use the exact same APIs, the REST abstractions are split into two modules. To use the inverse REST APIs, import the `InverseClient`:
|
||||
|
||||
const API_KEY = 'xxx';
|
||||
const PRIVATE_KEY = 'yyy';
|
||||
const useLivenet = false;
|
||||
```javascript
|
||||
const { InverseClient } = require('bybit-api');
|
||||
|
||||
const restInverseOptions = {
|
||||
// override the max size of the request window (in ms)
|
||||
@@ -68,7 +66,11 @@ const restInverseOptions = {
|
||||
parse_exceptions?: boolean;
|
||||
};
|
||||
|
||||
const client = new RestClient(
|
||||
const API_KEY = 'xxx';
|
||||
const PRIVATE_KEY = 'yyy';
|
||||
const useLivenet = false;
|
||||
|
||||
const client = new InverseClient(
|
||||
API_KEY,
|
||||
PRIVATE_KEY,
|
||||
|
||||
@@ -88,9 +90,67 @@ client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'})
|
||||
});
|
||||
```
|
||||
|
||||
See inverse [rest-client.ts](./src/rest-client.ts) for further information.
|
||||
See inverse [inverse-client.ts](./src/inverse-client.ts) for further information.
|
||||
|
||||
### Linear Contracts
|
||||
To use the Linear (USDT) REST APIs, import the `LinearClient`:
|
||||
|
||||
```javascript
|
||||
const { LinearClient } = require('bybit-api');
|
||||
|
||||
const restInverseOptions = {
|
||||
// override the max size of the request window (in ms)
|
||||
recv_window?: number;
|
||||
|
||||
// how often to sync time drift with bybit servers
|
||||
sync_interval_ms?: number | string;
|
||||
|
||||
// Default: false. Disable above sync mechanism if true.
|
||||
disable_time_sync?: boolean;
|
||||
|
||||
// Default: false. If true, we'll throw errors if any params are undefined
|
||||
strict_param_validation?: boolean;
|
||||
|
||||
// Optionally override API protocol + domain
|
||||
// e.g 'https://api.bytick.com'
|
||||
baseUrl?: string;
|
||||
|
||||
// Default: true. whether to try and post-process request exceptions.
|
||||
parse_exceptions?: boolean;
|
||||
};
|
||||
|
||||
const API_KEY = 'xxx';
|
||||
const PRIVATE_KEY = 'yyy';
|
||||
const useLivenet = false;
|
||||
|
||||
const client = new LinearClient(
|
||||
API_KEY,
|
||||
PRIVATE_KEY,
|
||||
|
||||
// optional, uses testnet by default. Set to 'true' to use livenet.
|
||||
useLivenet,
|
||||
|
||||
// restInverseOptions,
|
||||
// requestLibraryOptions
|
||||
);
|
||||
|
||||
client.changeUserLeverage({leverage: 4, symbol: 'ETHUSDT'})
|
||||
.then(result => {
|
||||
console.log(result);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
```
|
||||
|
||||
### WebSockets
|
||||
|
||||
Inverse & linear WebSockets can be used via a shared `WebsocketClient`.
|
||||
|
||||
Note: to use the linear websockets, pass "linear: true" in the constructor options when instancing the `WebsocketClient`.
|
||||
|
||||
To connect to both linear and inverse websockets, make two instances of the WebsocketClient:
|
||||
|
||||
#### Websocket client
|
||||
```javascript
|
||||
const { WebsocketClient } = require('bybit-api');
|
||||
|
||||
@@ -101,67 +161,84 @@ const wsConfig = {
|
||||
key: API_KEY,
|
||||
secret: PRIVATE_KEY,
|
||||
|
||||
// The following parameters are optional:
|
||||
/*
|
||||
The following parameters are optional:
|
||||
*/
|
||||
|
||||
// defaults to false == testnet. set to true for livenet.
|
||||
// defaults to false == testnet. Set to true for livenet.
|
||||
// livenet: true
|
||||
|
||||
// override which URL to use for websocket connections
|
||||
// wsUrl: 'wss://stream.bytick.com/realtime'
|
||||
|
||||
// how often to check (in ms) that WS connection is still alive
|
||||
// pingInterval: 10000,
|
||||
// defaults to fase == inverse. Set to true for linear (USDT) trading.
|
||||
// linear: true
|
||||
|
||||
// how long to wait (in ms) before deciding the connection should be terminated & reconnected
|
||||
// pongTimeout: 1000,
|
||||
|
||||
// how often to check (in ms) that WS connection is still alive
|
||||
// pingInterval: 10000,
|
||||
|
||||
// how long to wait before attempting to reconnect (in ms) after connection is closed
|
||||
// reconnectTimeout: 500,
|
||||
|
||||
// config options sent to RestClient (used for time sync). See RestClient docs.
|
||||
// restOptions: { },
|
||||
|
||||
// config for axios to pass to RestClient. E.g for proxy support
|
||||
// config for axios used for HTTP requests. E.g for proxy support
|
||||
// requestOptions: { }
|
||||
|
||||
// override which URL to use for websocket connections
|
||||
// wsUrl: 'wss://stream.bytick.com/realtime'
|
||||
};
|
||||
|
||||
const ws = new WebsocketClient(wsConfig);
|
||||
|
||||
// subscribe to multiple topics at once
|
||||
ws.subscribe(['position', 'execution', 'trade']);
|
||||
|
||||
// and/or subscribe to individual topics on demand
|
||||
ws.subscribe('kline.BTCUSD.1m');
|
||||
|
||||
ws.on('open', () => {
|
||||
console.log('connection open');
|
||||
// Listen to events coming from websockets. This is the primary data source
|
||||
ws.on('update', data => {
|
||||
console.log('update', data);
|
||||
});
|
||||
|
||||
ws.on('update', message => {
|
||||
console.log('update', message);
|
||||
// Optional: Listen to websocket connection open event (automatic after subscribing to one or more topics)
|
||||
ws.on('open', ({ wsKey, event }) => {
|
||||
console.log('connection open for websocket with ID: ' + wsKey);
|
||||
});
|
||||
|
||||
// Optional: Listen to responses to websocket queries (e.g. the response after subscribing to a topic)
|
||||
ws.on('response', response => {
|
||||
console.log('response', response);
|
||||
});
|
||||
|
||||
// Optional: Listen to connection close event. Unexpected connection closes are automatically reconnected.
|
||||
ws.on('close', () => {
|
||||
console.log('connection closed');
|
||||
});
|
||||
|
||||
// Optional: Listen to raw error events.
|
||||
// Note: responses to invalid topics are currently only sent in the "response" event.
|
||||
ws.on('error', err => {
|
||||
console.error('ERR', err);
|
||||
});
|
||||
```
|
||||
See inverse [websocket-client.ts](./src/websocket-client.ts) for further information.
|
||||
See [websocket-client.ts](./src/websocket-client.ts) for further information.
|
||||
|
||||
### Customise Logging
|
||||
Pass a custom logger which supports the log methods `silly`, `debug`, `notice`, `info`, `warning` and `error`, or override methods from the default logger as desired:
|
||||
|
||||
```js
|
||||
const { RestClient, WebsocketClient, DefaultLogger } = require('bybit-api');
|
||||
const { WebsocketClient, DefaultLogger } = require('bybit-api');
|
||||
|
||||
// Disable all logging on the silly level
|
||||
DefaultLogger.silly = () => {};
|
||||
|
||||
const ws = new WebsocketClient({key: 'xxx', secret: 'yyy'}, DefaultLogger);
|
||||
const ws = new WebsocketClient(
|
||||
{ key: 'xxx', secret: 'yyy' },
|
||||
DefaultLogger
|
||||
);
|
||||
```
|
||||
|
||||
## Contributions & Thanks
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bybit-api",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"clean": "rm -rf lib dist",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "tsc",
|
||||
"build:clean": "npm run clean && npm run build",
|
||||
"build:watch": "npm run clean && tsc --watch",
|
||||
"pack": "webpack --config webpack/webpack.config.js",
|
||||
"prepublish": "npm run build",
|
||||
"prepublish": "npm run build:clean",
|
||||
"betapublish": "npm publish --tag beta"
|
||||
},
|
||||
"author": "Tiago Siebler (https://github.com/tiagosiebler)",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './rest-client';
|
||||
export * from './inverse-client';
|
||||
export * from './linear-client';
|
||||
export * from './websocket-client';
|
||||
export * from './logger';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { GenericAPIResponse, getBaseRESTInverseUrl, RestClientInverseOptions } from './util/requestUtils';
|
||||
import { GenericAPIResponse, getRestBaseUrl, RestClientOptions } from './util/requestUtils';
|
||||
import RequestWrapper from './util/requestWrapper';
|
||||
import SharedEndpoints from './shared-endpoints';
|
||||
|
||||
export class RestClient {
|
||||
private requestWrapper: RequestWrapper;
|
||||
export class InverseClient extends SharedEndpoints {
|
||||
protected requestWrapper: RequestWrapper;
|
||||
|
||||
/**
|
||||
* @public Creates an instance of the inverse REST API client.
|
||||
@@ -11,22 +12,23 @@ export class RestClient {
|
||||
* @param {string} key - your API key
|
||||
* @param {string} secret - your API secret
|
||||
* @param {boolean} [useLivenet=false]
|
||||
* @param {RestClientInverseOptions} [restInverseOptions={}] options to configure REST API connectivity
|
||||
* @param {RestClientOptions} [restInverseOptions={}] options to configure REST API connectivity
|
||||
* @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios
|
||||
*/
|
||||
constructor(
|
||||
key?: string | undefined,
|
||||
secret?: string | undefined,
|
||||
useLivenet?: boolean,
|
||||
restInverseOptions: RestClientInverseOptions = {},
|
||||
httpOptions: AxiosRequestConfig = {}
|
||||
restInverseOptions: RestClientOptions = {},
|
||||
requestOptions: AxiosRequestConfig = {}
|
||||
) {
|
||||
super()
|
||||
this.requestWrapper = new RequestWrapper(
|
||||
key,
|
||||
secret,
|
||||
getBaseRESTInverseUrl(useLivenet),
|
||||
getRestBaseUrl(useLivenet),
|
||||
restInverseOptions,
|
||||
httpOptions
|
||||
requestOptions
|
||||
);
|
||||
return this;
|
||||
}
|
||||
@@ -37,12 +39,6 @@ export class RestClient {
|
||||
*
|
||||
*/
|
||||
|
||||
getOrderBook(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/orderBook/L2', params);
|
||||
}
|
||||
|
||||
getKline(params: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
@@ -57,15 +53,9 @@ export class RestClient {
|
||||
*/
|
||||
getLatestInformation(params?: {
|
||||
symbol?: string;
|
||||
}): GenericAPIResponse {
|
||||
}): GenericAPIResponse {
|
||||
return this.getTickers(params);
|
||||
}
|
||||
|
||||
getTickers(params?: {
|
||||
symbol?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/tickers', params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use getTrades() instead
|
||||
@@ -86,10 +76,6 @@ export class RestClient {
|
||||
return this.requestWrapper.get('v2/public/trading-records', params);
|
||||
}
|
||||
|
||||
getSymbols(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/symbols');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use getLiquidations() instead
|
||||
*/
|
||||
@@ -103,16 +89,6 @@ export class RestClient {
|
||||
return this.getLiquidations(params);
|
||||
}
|
||||
|
||||
getLiquidations(params: {
|
||||
symbol: string;
|
||||
from?: number;
|
||||
limit?: number;
|
||||
start_time?: number;
|
||||
end_time?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/liq-records', params);
|
||||
}
|
||||
|
||||
getMarkPriceKline(params: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
@@ -122,33 +98,32 @@ export class RestClient {
|
||||
return this.requestWrapper.get('v2/public/mark-price-kline', params);
|
||||
}
|
||||
|
||||
getOpenInterest(params: {
|
||||
getIndexPriceKline(params: {
|
||||
symbol: string;
|
||||
period: string;
|
||||
interval: string;
|
||||
from: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/open-interest', params);
|
||||
return this.requestWrapper.get('v2/public/index-price-kline', params);
|
||||
}
|
||||
|
||||
getLatestBigDeal(params: {
|
||||
getPremiumIndexKline(params: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
from: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/big-deal', params);
|
||||
}
|
||||
|
||||
getLongShortRatio(params: {
|
||||
symbol: string;
|
||||
period: string;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/account-ratio', params);
|
||||
return this.requestWrapper.get('v2/public/premium-index-kline', params);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Account Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Active orders
|
||||
*/
|
||||
|
||||
placeActiveOrder(orderRequest: {
|
||||
@@ -164,9 +139,6 @@ export class RestClient {
|
||||
close_on_trigger?: boolean;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
// if (orderRequest.order_type === 'Limit' && !orderRequest.price) {
|
||||
// throw new Error('Price required for limit orders');
|
||||
// }
|
||||
return this.requestWrapper.post('v2/private/order/create', orderRequest);
|
||||
}
|
||||
|
||||
@@ -180,29 +152,11 @@ export class RestClient {
|
||||
return this.requestWrapper.get('v2/private/order/list', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use getActiveOrderList() instead
|
||||
*/
|
||||
getActiveOrder(params: {
|
||||
order_id?: string;
|
||||
order_link_id?: string;
|
||||
symbol?: string;
|
||||
order?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
order_status?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/order/list', params);
|
||||
}
|
||||
|
||||
cancelActiveOrder(params: {
|
||||
symbol: string;
|
||||
order_id?: string;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
// if (!params.order_id && !params.order_link_id) {
|
||||
// throw new Error('Parameter order_id OR order_link_id is required');
|
||||
// }
|
||||
return this.requestWrapper.post('v2/private/order/cancel', params);
|
||||
}
|
||||
|
||||
@@ -219,33 +173,21 @@ export class RestClient {
|
||||
p_r_qty?: string;
|
||||
p_r_price?: string;
|
||||
}): GenericAPIResponse {
|
||||
// if (!params.order_id && !params.order_link_id) {
|
||||
// throw new Error('Parameter order_id OR order_link_id is required');
|
||||
// }
|
||||
return this.requestWrapper.post('v2/private/order/replace', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use replaceActiveOrder()
|
||||
*/
|
||||
replaceActiveOrderOld(params: any): GenericAPIResponse {
|
||||
// if (!params.order_id && !params.order_link_id) {
|
||||
// throw new Error('Parameter order_id OR order_link_id is required');
|
||||
// }
|
||||
return this.requestWrapper.post('open-api/order/replace', params);
|
||||
}
|
||||
|
||||
queryActiveOrder(params: {
|
||||
order_id?: string;
|
||||
order_link_id?: string;
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
// if (!params.order_id && !params.order_link_id) {
|
||||
// throw new Error('Parameter order_id OR order_link_id is required');
|
||||
// }
|
||||
return this.requestWrapper.get('v2/private/order', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional orders
|
||||
*/
|
||||
|
||||
placeConditionalOrder(params: {
|
||||
side: string;
|
||||
symbol: string;
|
||||
@@ -259,22 +201,9 @@ export class RestClient {
|
||||
close_on_trigger?: boolean;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
// if (params.order_type === 'Limit' && !params.price) {
|
||||
// throw new Error('Parameter price is required for limit orders');
|
||||
// }
|
||||
return this.requestWrapper.post('v2/private/stop-order/create', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use placeConditionalOrder
|
||||
*/
|
||||
placeConditionalOrderOld(params: any): GenericAPIResponse {
|
||||
// if (params.order_type === 'Limit' && !params.price) {
|
||||
// throw new Error('Parameter price is required for limit orders');
|
||||
// }
|
||||
return this.requestWrapper.post('open-api/stop-order/create', params);
|
||||
}
|
||||
|
||||
getConditionalOrder(params: {
|
||||
symbol: string;
|
||||
stop_order_status?: string;
|
||||
@@ -285,34 +214,14 @@ export class RestClient {
|
||||
return this.requestWrapper.get('v2/private/stop-order/list', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use placeConditionalOrder
|
||||
*/
|
||||
getConditionalOrderOld(params: any): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/stop-order/list', params);
|
||||
}
|
||||
|
||||
cancelConditionalOrder(params: {
|
||||
symbol: string;
|
||||
stop_order_id?: string;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
// if (!params.stop_order_id && !params.order_link_id) {
|
||||
// throw new Error('Parameter stop_order_id OR order_link_id is required');
|
||||
// }
|
||||
return this.requestWrapper.post('v2/private/stop-order/cancel', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use cancelConditionalOrder
|
||||
*/
|
||||
cancelConditionalOrderOld(params: any): GenericAPIResponse {
|
||||
// if (!params.stop_order_id && !params.order_link_id) {
|
||||
// throw new Error('Parameter stop_order_id OR order_link_id is required');
|
||||
// }
|
||||
return this.requestWrapper.post('open-api/stop-order/cancel', params);
|
||||
}
|
||||
|
||||
cancelAllConditionalOrders(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
@@ -327,30 +236,21 @@ export class RestClient {
|
||||
p_r_price?: string;
|
||||
p_r_trigger_price?: string;
|
||||
}): GenericAPIResponse {
|
||||
// if (!params.stop_order_id && !params.order_link_id) {
|
||||
// throw new Error('Parameter stop_order_id OR order_link_id is required');
|
||||
// }
|
||||
return this.requestWrapper.post('v2/private/stop-order/replace', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use replaceConditionalOrder
|
||||
*/
|
||||
replaceConditionalOrderOld(params: any): GenericAPIResponse {
|
||||
return this.requestWrapper.post('open-api/stop-order/replace', params);
|
||||
}
|
||||
|
||||
queryConditionalOrder(params: {
|
||||
symbol: string;
|
||||
stop_order_id?: string;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
// if (!params.stop_order_id && !params.order_link_id) {
|
||||
// throw new Error('Parameter stop_order_id OR order_link_id is required');
|
||||
// }
|
||||
return this.requestWrapper.get('v2/private/stop-order', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated use getPosition() instead
|
||||
*/
|
||||
@@ -386,14 +286,14 @@ export class RestClient {
|
||||
sl_trigger_by?: string;
|
||||
new_trailing_active?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('open-api/position/trading-stop', params);
|
||||
return this.requestWrapper.post('v2/private/position/trading-stop', params);
|
||||
}
|
||||
|
||||
setUserLeverage(params: {
|
||||
symbol: string;
|
||||
leverage: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('user/leverage/save', params);
|
||||
return this.requestWrapper.post('v2/private/position/leverage/save', params);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,6 +325,10 @@ export class RestClient {
|
||||
return this.requestWrapper.get('v2/private/trade/closed-pnl/list', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk Limit
|
||||
*/
|
||||
|
||||
getRiskLimitList(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/wallet/risk-limit/list');
|
||||
}
|
||||
@@ -436,96 +340,35 @@ export class RestClient {
|
||||
return this.requestWrapper.post('open-api/wallet/risk-limit', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Funding
|
||||
*/
|
||||
|
||||
getLastFundingRate(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/funding/prev-funding-rate', params);
|
||||
return this.requestWrapper.get('v2/public/funding/prev-funding-rate', params);
|
||||
}
|
||||
|
||||
getMyLastFundingFee(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/funding/prev-funding', params);
|
||||
return this.requestWrapper.get('v2/private/funding/prev-funding', params);
|
||||
}
|
||||
|
||||
getPredictedFunding(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/funding/predicted-funding', params);
|
||||
return this.requestWrapper.get('v2/private/funding/predicted-funding', params);
|
||||
}
|
||||
|
||||
getApiKeyInfo(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/api-key');
|
||||
}
|
||||
/**
|
||||
* LCP Info
|
||||
*/
|
||||
|
||||
getLcpInfo(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/private/account/lcp', params);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Wallet Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
getWalletBalance(params?: {
|
||||
coin?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/private/wallet/balance', params);
|
||||
}
|
||||
|
||||
getWalletFundRecords(params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
currency?: string;
|
||||
coin?: string;
|
||||
wallet_fund_type?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/wallet/fund/records', params);
|
||||
}
|
||||
|
||||
getWithdrawRecords(params: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
coin?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('open-api/wallet/withdraw/list', params);
|
||||
}
|
||||
|
||||
getAssetExchangeRecords(params?: {
|
||||
limit?: number;
|
||||
from?: number;
|
||||
direction?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/private/exchange-order/list', params);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* API Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
getServerTime(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/time');
|
||||
}
|
||||
|
||||
getApiAnnouncements(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/announcement');
|
||||
}
|
||||
|
||||
async getTimeOffset(): Promise<number> {
|
||||
const start = Date.now();
|
||||
return this.getServerTime().then(result => {
|
||||
const end = Date.now();
|
||||
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
|
||||
});
|
||||
}
|
||||
};
|
||||
356
src/linear-client.ts
Normal file
356
src/linear-client.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { GenericAPIResponse, getRestBaseUrl, RestClientOptions } from './util/requestUtils';
|
||||
import RequestWrapper from './util/requestWrapper';
|
||||
import SharedEndpoints from './shared-endpoints';
|
||||
|
||||
export class LinearClient extends SharedEndpoints {
|
||||
protected requestWrapper: RequestWrapper;
|
||||
|
||||
/**
|
||||
* @public Creates an instance of the inverse REST API client.
|
||||
*
|
||||
* @param {string} key - your API key
|
||||
* @param {string} secret - your API secret
|
||||
* @param {boolean} [useLivenet=false]
|
||||
* @param {RestClientOptions} [restInverseOptions={}] options to configure REST API connectivity
|
||||
* @param {AxiosRequestConfig} [requestOptions={}] HTTP networking options for axios
|
||||
*/
|
||||
constructor(
|
||||
key?: string | undefined,
|
||||
secret?: string | undefined,
|
||||
useLivenet?: boolean,
|
||||
restInverseOptions: RestClientOptions = {},
|
||||
requestOptions: AxiosRequestConfig = {}
|
||||
) {
|
||||
super()
|
||||
this.requestWrapper = new RequestWrapper(
|
||||
key,
|
||||
secret,
|
||||
getRestBaseUrl(useLivenet),
|
||||
restInverseOptions,
|
||||
requestOptions
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Market Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
getKline(params: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
from: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('public/linear/kline', params);
|
||||
}
|
||||
|
||||
getTrades(params: {
|
||||
symbol: string;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('public/linear/recent-trading-records', params);
|
||||
}
|
||||
|
||||
getLastFundingRate(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('public/linear/funding/prev-funding-rate', params);
|
||||
}
|
||||
|
||||
getMarkPriceKline(params: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
from: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('public/linear/mark-price-kline', params);
|
||||
}
|
||||
|
||||
getIndexPriceKline(params: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
from: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('public/linear/index-price-kline', params);
|
||||
}
|
||||
|
||||
getPremiumIndexKline(params: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
from: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('public/linear/premium-index-kline', params);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Account Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
placeActiveOrder(params: {
|
||||
side: string;
|
||||
symbol: string;
|
||||
order_type: string;
|
||||
qty: number;
|
||||
price?: number;
|
||||
time_in_force: string;
|
||||
take_profit?: number;
|
||||
stop_loss?: number;
|
||||
tp_trigger_by?: string;
|
||||
sl_trigger_by?: string;
|
||||
reduce_only?: boolean;
|
||||
close_on_trigger?: boolean;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/order/create', params);
|
||||
}
|
||||
|
||||
getActiveOrderList(params: {
|
||||
order_id?: string;
|
||||
order_link_id?: string;
|
||||
symbol: string;
|
||||
order?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
order_status?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/order/list', params);
|
||||
}
|
||||
|
||||
cancelActiveOrder(params: {
|
||||
symbol: string;
|
||||
order_id?: string;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/order/cancel', params);
|
||||
}
|
||||
|
||||
cancelAllActiveOrders(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/order/cancel-all', params);
|
||||
}
|
||||
|
||||
replaceActiveOrder(params: {
|
||||
order_id?: string;
|
||||
order_link_id?: string;
|
||||
symbol: string;
|
||||
p_r_qty?: number;
|
||||
p_r_price?: number;
|
||||
take_profit?: number;
|
||||
stop_loss?: number;
|
||||
tp_trigger_by?: string;
|
||||
sl_trigger_by?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/order/replace', params);
|
||||
}
|
||||
|
||||
queryActiveOrder(params: {
|
||||
order_id?: string;
|
||||
order_link_id?: string;
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/order/search', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional orders
|
||||
*/
|
||||
|
||||
placeConditionalOrder(params: {
|
||||
side: string;
|
||||
symbol: string;
|
||||
order_type: string;
|
||||
qty: number;
|
||||
price?: number;
|
||||
base_price: number;
|
||||
stop_px: number;
|
||||
time_in_force: string;
|
||||
trigger_by?: string;
|
||||
close_on_trigger?: boolean;
|
||||
order_link_id?: string;
|
||||
reduce_only: boolean;
|
||||
take_profit?: number;
|
||||
stop_loss?: number;
|
||||
tp_trigger_by?: string;
|
||||
sl_trigger_by?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/stop-order/create', params);
|
||||
}
|
||||
|
||||
getConditionalOrder(params: {
|
||||
stop_order_id?: string;
|
||||
order_link_id?: string;
|
||||
symbol: string;
|
||||
stop_order_status?: string;
|
||||
order?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/stop-order/list', params);
|
||||
}
|
||||
|
||||
cancelConditionalOrder(params: {
|
||||
symbol: string;
|
||||
stop_order_id?: string;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/stop-order/cancel', params);
|
||||
}
|
||||
|
||||
cancelAllConditionalOrders(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/stop-order/cancel-all', params);
|
||||
}
|
||||
|
||||
replaceConditionalOrder(params: {
|
||||
stop_order_id?: string;
|
||||
order_link_id?: string;
|
||||
symbol: string;
|
||||
p_r_qty?: number;
|
||||
p_r_price?: number;
|
||||
p_r_trigger_price?: number;
|
||||
take_profit?: number;
|
||||
stop_loss?: number;
|
||||
tp_trigger_by?: string;
|
||||
sl_trigger_by?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/stop-order/replace', params);
|
||||
}
|
||||
|
||||
queryConditionalOrder(params: {
|
||||
symbol: string;
|
||||
stop_order_id?: string;
|
||||
order_link_id?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/stop-order/search', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position
|
||||
*/
|
||||
|
||||
getPosition(params?: {
|
||||
symbol?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/position/list', params);
|
||||
}
|
||||
|
||||
setAutoAddMargin(params?: {
|
||||
symbol: string;
|
||||
side: string;
|
||||
auto_add_margin: boolean;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/position/set-auto-add-margin', params);
|
||||
}
|
||||
|
||||
setMarginSwitch(params?: {
|
||||
symbol: string;
|
||||
is_isolated: boolean;
|
||||
buy_leverage: number;
|
||||
sell_leverage: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/position/switch-isolated', params);
|
||||
}
|
||||
|
||||
setSwitchMode(params?: {
|
||||
symbol: string;
|
||||
tp_sl_mode: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/tpsl/switch-mode', params);
|
||||
}
|
||||
|
||||
setAddReduceMargin(params?: {
|
||||
symbol: string;
|
||||
side: string;
|
||||
margin: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/position/add-margin', params);
|
||||
}
|
||||
|
||||
setUserLeverage(params: {
|
||||
symbol: string;
|
||||
buy_leverage: number;
|
||||
sell_leverage: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/position/set-leverage', params);
|
||||
}
|
||||
|
||||
setTradingStop(params: {
|
||||
symbol: string;
|
||||
side: string;
|
||||
take_profit?: number;
|
||||
stop_loss?: number;
|
||||
trailing_stop?: number;
|
||||
tp_trigger_by?: string;
|
||||
sl_trigger_by?: string;
|
||||
sl_size?: number;
|
||||
tp_size?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.post('private/linear/position/trading-stop', params);
|
||||
}
|
||||
|
||||
getTradeRecords(params: {
|
||||
symbol: string;
|
||||
start_time?: number;
|
||||
end_time?: number;
|
||||
exec_type?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/trade/execution/list', params);
|
||||
}
|
||||
|
||||
getClosedPnl(params: {
|
||||
symbol: string;
|
||||
start_time?: number;
|
||||
end_time?: number;
|
||||
exec_type?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/tpsl/switch-mode', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk Limit
|
||||
*/
|
||||
|
||||
getRiskLimitList(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('public/linear/risk-limit', params);
|
||||
}
|
||||
|
||||
setRiskLimit(params: {
|
||||
symbol: string;
|
||||
side: string;
|
||||
risk_id: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/position/set-risk', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Funding
|
||||
*/
|
||||
|
||||
getPredictedFundingFee(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/funding/predicted-funding', params);
|
||||
}
|
||||
|
||||
getLastFundingFee(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('private/linear/funding/prev-funding', params);
|
||||
}
|
||||
}
|
||||
143
src/shared-endpoints.ts
Normal file
143
src/shared-endpoints.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { GenericAPIResponse } from './util/requestUtils';
|
||||
import RequestWrapper from './util/requestWrapper';
|
||||
|
||||
export default class SharedEndpoints {
|
||||
// TODO: Is there a way to say that Base has to provide this?
|
||||
protected requestWrapper: RequestWrapper;
|
||||
|
||||
/**
|
||||
*
|
||||
* Market Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
getOrderBook(params: {
|
||||
symbol: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/orderBook/L2', params);
|
||||
}
|
||||
|
||||
getTickers(params?: {
|
||||
symbol?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/tickers', params);
|
||||
}
|
||||
|
||||
getSymbols(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/symbols');
|
||||
}
|
||||
|
||||
getLiquidations(params: {
|
||||
symbol: string;
|
||||
from?: number;
|
||||
limit?: number;
|
||||
start_time?: number;
|
||||
end_time?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/liq-records', params);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Market Data : Advanced
|
||||
*
|
||||
*/
|
||||
|
||||
getOpenInterest(params: {
|
||||
symbol: string;
|
||||
period: string;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/open-interest', params);
|
||||
}
|
||||
|
||||
getLatestBigDeal(params: {
|
||||
symbol: string;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/big-deal', params);
|
||||
}
|
||||
|
||||
getLongShortRatio(params: {
|
||||
symbol: string;
|
||||
period: string;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/account-ratio', params);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Account Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
getApiKeyInfo(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/private/account/api-key');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Wallet Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
getWalletBalance(params: {
|
||||
coin?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/private/wallet/balance', params)
|
||||
}
|
||||
|
||||
getWalletFundRecords(params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
currency?: string;
|
||||
coin?: string;
|
||||
wallet_fund_type?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/private/wallet/fund/records', params);
|
||||
}
|
||||
|
||||
getWithdrawRecords(params: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
coin?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/private/wallet/withdraw/list', params);
|
||||
}
|
||||
|
||||
getAssetExchangeRecords(params?: {
|
||||
limit?: number;
|
||||
from?: number;
|
||||
direction?: string;
|
||||
}): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/private/exchange-order/list', params);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* API Data Endpoints
|
||||
*
|
||||
*/
|
||||
|
||||
getServerTime(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/time');
|
||||
}
|
||||
|
||||
getApiAnnouncements(): GenericAPIResponse {
|
||||
return this.requestWrapper.get('v2/public/announcement');
|
||||
}
|
||||
|
||||
async getTimeOffset(): Promise<number> {
|
||||
const start = Date.now();
|
||||
return this.getServerTime().then(result => {
|
||||
const end = Date.now();
|
||||
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
|
||||
});
|
||||
}
|
||||
}
|
||||
124
src/util/WsStore.ts
Normal file
124
src/util/WsStore.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { WsConnectionState } from '../websocket-client';
|
||||
import { DefaultLogger } from '../logger';
|
||||
|
||||
import WebSocket from 'isomorphic-ws';
|
||||
|
||||
type WsTopicList = Set<string>;
|
||||
type KeyedWsTopicLists = {
|
||||
[key: string]: WsTopicList;
|
||||
};
|
||||
|
||||
interface WsStoredState {
|
||||
ws?: WebSocket;
|
||||
connectionState?: WsConnectionState;
|
||||
activePingTimer?: NodeJS.Timeout | undefined;
|
||||
activePongTimer?: NodeJS.Timeout | undefined;
|
||||
subscribedTopics: WsTopicList;
|
||||
};
|
||||
|
||||
export default class WsStore {
|
||||
private wsState: {
|
||||
[key: string]: WsStoredState;
|
||||
}
|
||||
private logger: typeof DefaultLogger;
|
||||
|
||||
constructor(logger: typeof DefaultLogger) {
|
||||
this.logger = logger || DefaultLogger;
|
||||
this.wsState = {};
|
||||
}
|
||||
|
||||
get(key: string, createIfMissing?: boolean): WsStoredState | undefined {
|
||||
if (this.wsState[key]) {
|
||||
return this.wsState[key];
|
||||
}
|
||||
|
||||
if (createIfMissing) {
|
||||
return this.create(key);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getKeys(): string[] {
|
||||
return Object.keys(this.wsState);
|
||||
}
|
||||
|
||||
create(key: string): WsStoredState | undefined {
|
||||
if (this.hasExistingActiveConnection(key)) {
|
||||
this.logger.warning('WsStore setConnection() overwriting existing open connection: ', this.getWs(key));
|
||||
}
|
||||
this.wsState[key] = {
|
||||
subscribedTopics: new Set(),
|
||||
connectionState: WsConnectionState.READY_STATE_INITIAL
|
||||
};
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
if (this.hasExistingActiveConnection(key)) {
|
||||
const ws = this.getWs(key);
|
||||
this.logger.warning('WsStore deleting state for connection still open: ', ws);
|
||||
ws?.close();
|
||||
}
|
||||
delete this.wsState[key];
|
||||
}
|
||||
|
||||
/* connection websocket */
|
||||
|
||||
hasExistingActiveConnection(key) {
|
||||
return this.get(key) && this.isWsOpen(key);
|
||||
}
|
||||
|
||||
getWs(key: string): WebSocket | undefined {
|
||||
return this.get(key)?.ws;
|
||||
}
|
||||
|
||||
setWs(key: string, wsConnection: WebSocket): WebSocket {
|
||||
if (this.isWsOpen(key)) {
|
||||
this.logger.warning('WsStore setConnection() overwriting existing open connection: ', this.getWs(key));
|
||||
}
|
||||
this.get(key, true)!.ws = wsConnection;
|
||||
return wsConnection;
|
||||
}
|
||||
|
||||
/* connection state */
|
||||
|
||||
isWsOpen(key: string): boolean {
|
||||
const existingConnection = this.getWs(key);
|
||||
return !!existingConnection && existingConnection.readyState === existingConnection.OPEN;
|
||||
}
|
||||
|
||||
getConnectionState(key: string): WsConnectionState {
|
||||
return this.get(key, true)!.connectionState!;
|
||||
}
|
||||
|
||||
setConnectionState(key: string, state: WsConnectionState) {
|
||||
this.get(key, true)!.connectionState = state;
|
||||
}
|
||||
|
||||
isConnectionState(key: string, state: WsConnectionState): boolean {
|
||||
return this.getConnectionState(key) === state;
|
||||
}
|
||||
|
||||
/* subscribed topics */
|
||||
|
||||
getTopics(key: string): WsTopicList {
|
||||
return this.get(key, true)!.subscribedTopics;
|
||||
}
|
||||
|
||||
getTopicsByKey(): KeyedWsTopicLists {
|
||||
const result = {};
|
||||
for (const refKey in this.wsState) {
|
||||
result[refKey] = this.getTopics(refKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
addTopic(key: string, topic: string) {
|
||||
return this.getTopics(key).add(topic);
|
||||
}
|
||||
|
||||
deleteTopic(key: string, topic: string) {
|
||||
return this.getTopics(key).delete(topic);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHmac } from 'crypto';
|
||||
|
||||
export interface RestClientInverseOptions {
|
||||
export interface RestClientOptions {
|
||||
// override the max size of the request window (in ms)
|
||||
recv_window?: number;
|
||||
|
||||
@@ -42,7 +42,7 @@ export function serializeParams(params: object = {}, strict_validation = false):
|
||||
.join('&');
|
||||
};
|
||||
|
||||
export function getBaseRESTInverseUrl(useLivenet?: boolean, restInverseOptions?: RestClientInverseOptions) {
|
||||
export function getRestBaseUrl(useLivenet?: boolean, restInverseOptions?: RestClientOptions) {
|
||||
const baseUrlsInverse = {
|
||||
livenet: 'https://api.bybit.com',
|
||||
testnet: 'https://api-testnet.bybit.com'
|
||||
@@ -56,4 +56,23 @@ export function getBaseRESTInverseUrl(useLivenet?: boolean, restInverseOptions?:
|
||||
return baseUrlsInverse.livenet;
|
||||
}
|
||||
return baseUrlsInverse.testnet;
|
||||
}
|
||||
|
||||
export function isPublicEndpoint (endpoint: string): boolean {
|
||||
if (endpoint.startsWith('v2/public')) {
|
||||
return true;
|
||||
}
|
||||
if (endpoint.startsWith('public/linear')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isWsPong(response: any) {
|
||||
return (
|
||||
response.request &&
|
||||
response.request.op === 'ping' &&
|
||||
response.ret_msg === 'pong' &&
|
||||
response.success === true
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
|
||||
|
||||
import { signMessage, serializeParams, RestClientInverseOptions, GenericAPIResponse } from './requestUtils';
|
||||
import { signMessage, serializeParams, RestClientOptions, GenericAPIResponse, isPublicEndpoint } from './requestUtils';
|
||||
|
||||
export default class RequestUtil {
|
||||
private timeOffset: number | null;
|
||||
private syncTimePromise: null | Promise<any>;
|
||||
private options: RestClientInverseOptions;
|
||||
private options: RestClientOptions;
|
||||
private baseUrl: string;
|
||||
private globalRequestOptions: AxiosRequestConfig;
|
||||
private key: string | undefined;
|
||||
@@ -15,7 +15,7 @@ export default class RequestUtil {
|
||||
key: string | undefined,
|
||||
secret: string | undefined,
|
||||
baseUrl: string,
|
||||
options: RestClientInverseOptions = {},
|
||||
options: RestClientOptions = {},
|
||||
requestOptions: AxiosRequestConfig = {}
|
||||
) {
|
||||
this.timeOffset = null;
|
||||
@@ -68,9 +68,7 @@ export default class RequestUtil {
|
||||
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
|
||||
*/
|
||||
async _call(method: Method, endpoint: string, params?: any): GenericAPIResponse {
|
||||
const publicEndpoint = endpoint.startsWith('v2/public');
|
||||
|
||||
if (!publicEndpoint) {
|
||||
if (!isPublicEndpoint(endpoint)) {
|
||||
if (!this.key || !this.secret) {
|
||||
throw new Error('Private endpoints require api and private keys set');
|
||||
}
|
||||
|
||||
@@ -1,27 +1,51 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { RestClient } from './rest-client';
|
||||
import { InverseClient } from './inverse-client';
|
||||
import { LinearClient } from './linear-client';
|
||||
import { DefaultLogger } from './logger';
|
||||
import { signMessage, serializeParams } from './util/requestUtils';
|
||||
// import WebSocket from 'ws';
|
||||
import WebSocket from 'isomorphic-ws';
|
||||
import { signMessage, serializeParams, isWsPong } from './util/requestUtils';
|
||||
|
||||
const wsUrls = {
|
||||
import WebSocket from 'isomorphic-ws';
|
||||
import WsStore from './util/WsStore';
|
||||
|
||||
const inverseEndpoints = {
|
||||
livenet: 'wss://stream.bybit.com/realtime',
|
||||
testnet: 'wss://stream-testnet.bybit.com/realtime'
|
||||
};
|
||||
|
||||
const linearEndpoints = {
|
||||
private: {
|
||||
livenet: 'wss://stream.bybit.com/realtime_private',
|
||||
livenet2: 'wss://stream.bytick.com/realtime_public',
|
||||
testnet: 'wss://stream-testnet.bybit.com/realtime_private'
|
||||
},
|
||||
public: {
|
||||
livenet: 'wss://stream.bybit.com/realtime_public',
|
||||
livenet2: 'wss://stream.bytick.com/realtime_private',
|
||||
testnet: 'wss://stream-testnet.bybit.com/realtime_public'
|
||||
}
|
||||
};
|
||||
|
||||
const loggerCategory = { category: 'bybit-ws' };
|
||||
|
||||
const READY_STATE_INITIAL = 0;
|
||||
const READY_STATE_CONNECTING = 1;
|
||||
const READY_STATE_CONNECTED = 2;
|
||||
const READY_STATE_CLOSING = 3;
|
||||
const READY_STATE_RECONNECTING = 4;
|
||||
|
||||
export interface WebsocketClientOptions {
|
||||
export enum WsConnectionState {
|
||||
READY_STATE_INITIAL,
|
||||
READY_STATE_CONNECTING,
|
||||
READY_STATE_CONNECTED,
|
||||
READY_STATE_CLOSING,
|
||||
READY_STATE_RECONNECTING
|
||||
};
|
||||
|
||||
export interface WSClientConfigurableOptions {
|
||||
key?: string;
|
||||
secret?: string;
|
||||
livenet?: boolean;
|
||||
|
||||
linear?: boolean;
|
||||
pongTimeout?: number;
|
||||
pingInterval?: number;
|
||||
reconnectTimeout?: number;
|
||||
@@ -30,231 +54,400 @@ export interface WebsocketClientOptions {
|
||||
wsUrl?: string;
|
||||
};
|
||||
|
||||
type Logger = typeof DefaultLogger;
|
||||
export interface WebsocketClientOptions extends WSClientConfigurableOptions {
|
||||
livenet: boolean;
|
||||
linear: boolean;
|
||||
pongTimeout: number;
|
||||
pingInterval: number;
|
||||
reconnectTimeout: number;
|
||||
};
|
||||
|
||||
export const wsKeyInverse = 'inverse';
|
||||
export const wsKeyLinearPrivate = 'linearPrivate';
|
||||
export const wsKeyLinearPublic = 'linearPublic';
|
||||
|
||||
const getLinearWsKeyForTopic = (topic: string) => {
|
||||
const privateLinearTopics = ['position', 'execution', 'order', 'stop_order', 'wallet'];
|
||||
if (privateLinearTopics.includes(topic)) {
|
||||
return wsKeyLinearPrivate;
|
||||
}
|
||||
|
||||
return wsKeyLinearPublic;
|
||||
}
|
||||
|
||||
export declare interface WebsocketClient {
|
||||
on(event: 'open' | 'reconnected', listener: ({ wsKey: string, event: any }) => void): this;
|
||||
on(event: 'response' | 'update' | 'error', listener: (response: any) => void): this;
|
||||
on(event: 'reconnect' | 'close', listener: () => void): this;
|
||||
}
|
||||
|
||||
export class WebsocketClient extends EventEmitter {
|
||||
private logger: Logger;
|
||||
private readyState: number;
|
||||
private pingInterval?: number | undefined;
|
||||
private pongTimeout?: number | undefined;
|
||||
private client: RestClient;
|
||||
private _subscriptions: Set<unknown>;
|
||||
private ws: WebSocket;
|
||||
private logger: typeof DefaultLogger;
|
||||
private restClient: InverseClient | LinearClient;
|
||||
private options: WebsocketClientOptions;
|
||||
private wsStore: WsStore;
|
||||
|
||||
constructor(options: WebsocketClientOptions, logger?: Logger) {
|
||||
constructor(options: WSClientConfigurableOptions, logger?: typeof DefaultLogger) {
|
||||
super();
|
||||
|
||||
this.logger = logger || DefaultLogger;
|
||||
|
||||
this.readyState = READY_STATE_INITIAL;
|
||||
this.pingInterval = undefined;
|
||||
this.pongTimeout = undefined;
|
||||
this.wsStore = new WsStore(this.logger);
|
||||
|
||||
this.options = {
|
||||
livenet: false,
|
||||
linear: false,
|
||||
pongTimeout: 1000,
|
||||
pingInterval: 10000,
|
||||
reconnectTimeout: 500,
|
||||
...options
|
||||
};
|
||||
|
||||
this.client = new RestClient(undefined, undefined, this.options.livenet, this.options.restOptions, this.options.requestOptions);
|
||||
this._subscriptions = new Set();
|
||||
|
||||
this._connect();
|
||||
}
|
||||
|
||||
subscribe(topics) {
|
||||
if (!Array.isArray(topics)) topics = [topics];
|
||||
topics.forEach(topic => this._subscriptions.add(topic));
|
||||
|
||||
// subscribe not necessary if not yet connected (will subscribe onOpen)
|
||||
if (this.readyState === READY_STATE_CONNECTED) this._subscribe(topics);
|
||||
}
|
||||
|
||||
unsubscribe(topics) {
|
||||
if (!Array.isArray(topics)) topics = [topics];
|
||||
|
||||
topics.forEach(topic => this._subscriptions.delete(topic));
|
||||
|
||||
// unsubscribe not necessary if not yet connected
|
||||
if (this.readyState === READY_STATE_CONNECTED) this._unsubscribe(topics);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.logger.info('Closing connection', {category: 'bybit-ws'});
|
||||
this.readyState = READY_STATE_CLOSING;
|
||||
this._teardown();
|
||||
this.ws && this.ws.close();
|
||||
}
|
||||
|
||||
_getWsUrl() {
|
||||
if (this.options.wsUrl) {
|
||||
return this.options.wsUrl;
|
||||
if (this.isLinear()) {
|
||||
this.restClient = new LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions);
|
||||
} else {
|
||||
this.restClient = new InverseClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions);
|
||||
}
|
||||
return wsUrls[this.options.livenet ? 'livenet' : 'testnet'];
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
public isLivenet(): boolean {
|
||||
return this.options.livenet === true;
|
||||
}
|
||||
|
||||
public isLinear(): boolean {
|
||||
return this.options.linear === true;
|
||||
}
|
||||
|
||||
public isInverse(): boolean {
|
||||
return !this.isLinear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add topic/topics to WS subscription list
|
||||
*/
|
||||
public subscribe(wsTopics: string[] | string) {
|
||||
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
||||
topics.forEach(topic => this.wsStore.addTopic(
|
||||
this.getWsKeyForTopic(topic),
|
||||
topic
|
||||
));
|
||||
|
||||
// attempt to send subscription topic per websocket
|
||||
this.wsStore.getKeys().forEach(wsKey => {
|
||||
// if connected, send subscription request
|
||||
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) {
|
||||
return this.requestSubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)]);
|
||||
}
|
||||
|
||||
// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
|
||||
if (
|
||||
!this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING) &&
|
||||
!this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)
|
||||
) {
|
||||
return this.connect(wsKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove topic/topics from WS subscription list
|
||||
*/
|
||||
public unsubscribe(wsTopics: string[] | string) {
|
||||
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
||||
topics.forEach(topic => this.wsStore.deleteTopic(
|
||||
this.getWsKeyForTopic(topic),
|
||||
topic
|
||||
));
|
||||
|
||||
this.wsStore.getKeys().forEach(wsKey => {
|
||||
// unsubscribe request only necessary if active connection exists
|
||||
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) {
|
||||
this.requestUnsubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)])
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public close(wsKey: string) {
|
||||
this.logger.info('Closing connection', { ...loggerCategory, wsKey });
|
||||
this.setWsState(wsKey, READY_STATE_CLOSING);
|
||||
this.clearTimers(wsKey);
|
||||
|
||||
this.getWs(wsKey)?.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request connection of all dependent websockets, instead of waiting for automatic connection by library
|
||||
*/
|
||||
public connectAll(): Promise<WebSocket | undefined>[] | undefined {
|
||||
if (this.isInverse()) {
|
||||
return [this.connect(wsKeyInverse)];
|
||||
}
|
||||
|
||||
if (this.isLinear()) {
|
||||
return [this.connect(wsKeyLinearPublic), this.connect(wsKeyLinearPrivate)];
|
||||
}
|
||||
}
|
||||
|
||||
private async connect(wsKey: string): Promise<WebSocket | undefined> {
|
||||
try {
|
||||
if (this.readyState === READY_STATE_INITIAL) this.readyState = READY_STATE_CONNECTING;
|
||||
if (this.wsStore.isWsOpen(wsKey)) {
|
||||
this.logger.error('Refused to connect to ws with existing active connection', { ...loggerCategory, wsKey })
|
||||
return this.wsStore.getWs(wsKey);
|
||||
}
|
||||
|
||||
const authParams = await this._authenticate();
|
||||
const url = this._getWsUrl() + authParams;
|
||||
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
|
||||
this.logger.error('Refused to connect to ws, connection attempt already active', { ...loggerCategory, wsKey })
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
if (
|
||||
!this.wsStore.getConnectionState(wsKey) ||
|
||||
this.wsStore.isConnectionState(wsKey, READY_STATE_INITIAL)
|
||||
) {
|
||||
this.setWsState(wsKey, READY_STATE_CONNECTING);
|
||||
}
|
||||
|
||||
ws.onopen = this._wsOpenHandler.bind(this);
|
||||
ws.onmessage = this._wsMessageHandler.bind(this);
|
||||
ws.onerror = this._wsOnErrorHandler.bind(this);
|
||||
ws.onclose = this._wsCloseHandler.bind(this);
|
||||
|
||||
this.ws = ws;
|
||||
const authParams = await this.getAuthParams(wsKey);
|
||||
const url = this.getWsUrl(wsKey) + authParams;
|
||||
const ws = this.connectToWsUrl(url, wsKey);
|
||||
|
||||
return this.wsStore.setWs(wsKey, ws);
|
||||
} catch (err) {
|
||||
this.logger.error('Connection failed: ', err);
|
||||
this._reconnect(this.options.reconnectTimeout);
|
||||
this.parseWsError('Connection failed', err, wsKey);
|
||||
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
|
||||
}
|
||||
}
|
||||
|
||||
async _authenticate() {
|
||||
if (this.options.key && this.options.secret) {
|
||||
this.logger.debug('Starting authenticated websocket client.', {category: 'bybit-ws'});
|
||||
private parseWsError(context: string, error, wsKey: string) {
|
||||
if (!error.message) {
|
||||
this.logger.error(`${context} due to unexpected error: `, error);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeOffset = await this.client.getTimeOffset();
|
||||
switch (error.message) {
|
||||
case 'Unexpected server response: 401':
|
||||
this.logger.error(`${context} due to 401 authorization failure.`, { ...loggerCategory, wsKey });
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.error(`{context} due to unexpected response error: ${error.msg}`, { ...loggerCategory, wsKey });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return params required to make authorized request
|
||||
*/
|
||||
private async getAuthParams(wsKey: string): Promise<string> {
|
||||
const { key, secret } = this.options;
|
||||
|
||||
if (key && secret && wsKey !== wsKeyLinearPublic) {
|
||||
this.logger.debug('Getting auth\'d request params', { ...loggerCategory, wsKey });
|
||||
|
||||
const timeOffset = await this.restClient.getTimeOffset();
|
||||
|
||||
const params: any = {
|
||||
api_key: this.options.key,
|
||||
expires: (Date.now() + timeOffset + 5000)
|
||||
};
|
||||
|
||||
params.signature = signMessage('GET/realtime' + params.expires, this.options.secret);
|
||||
params.signature = signMessage('GET/realtime' + params.expires, secret);
|
||||
return '?' + serializeParams(params);
|
||||
|
||||
} else if (this.options.key || this.options.secret) {
|
||||
this.logger.warning('Could not authenticate websocket, either api key or private key missing.', { category: 'bybit-ws' });
|
||||
} else if (!key || !secret) {
|
||||
this.logger.warning('Connot authenticate websocket, either api or private keys missing.', { ...loggerCategory, wsKey });
|
||||
} else {
|
||||
this.logger.debug('Starting public only websocket client.', { category: 'bybit-ws' });
|
||||
this.logger.debug('Starting public only websocket client.', { ...loggerCategory, wsKey });
|
||||
}
|
||||
|
||||
return '';
|
||||
return '';
|
||||
}
|
||||
|
||||
_reconnect(timeout) {
|
||||
this._teardown();
|
||||
if (this.readyState !== READY_STATE_CONNECTING) {
|
||||
this.readyState = READY_STATE_RECONNECTING;
|
||||
private reconnectWithDelay(wsKey: string, connectionDelayMs: number) {
|
||||
this.clearTimers(wsKey);
|
||||
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CONNECTING) {
|
||||
this.setWsState(wsKey, READY_STATE_RECONNECTING);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.logger.info('Reconnecting to server', { category: 'bybit-ws' });
|
||||
|
||||
this._connect();
|
||||
}, timeout);
|
||||
this.logger.info('Reconnecting to websocket', { ...loggerCategory, wsKey });
|
||||
this.connect(wsKey);
|
||||
}, connectionDelayMs);
|
||||
}
|
||||
|
||||
_ping() {
|
||||
clearTimeout(this.pongTimeout!);
|
||||
delete this.pongTimeout;
|
||||
private ping(wsKey: string) {
|
||||
this.clearPongTimer(wsKey);
|
||||
|
||||
this.logger.silly('Sending ping', { category: 'bybit-ws' });
|
||||
this.ws.send(JSON.stringify({op: 'ping'}));
|
||||
this.logger.silly('Sending ping', { ...loggerCategory, wsKey });
|
||||
this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' }));
|
||||
|
||||
this.pongTimeout = setTimeout(() => {
|
||||
this.logger.info('Pong timeout', { category: 'bybit-ws' });
|
||||
this._teardown();
|
||||
// this.ws.terminate();
|
||||
// TODO: does this work?
|
||||
this.ws.close();
|
||||
this.wsStore.get(wsKey, true)!.activePongTimer = setTimeout(() => {
|
||||
this.logger.info('Pong timeout - closing socket to reconnect', { ...loggerCategory, wsKey });
|
||||
this.getWs(wsKey)?.close();
|
||||
}, this.options.pongTimeout);
|
||||
}
|
||||
|
||||
_teardown() {
|
||||
if (this.pingInterval) clearInterval(this.pingInterval);
|
||||
if (this.pongTimeout) clearTimeout(this.pongTimeout);
|
||||
|
||||
this.pongTimeout = undefined;
|
||||
this.pingInterval = undefined;
|
||||
private clearTimers(wsKey: string) {
|
||||
this.clearPingTimer(wsKey);
|
||||
this.clearPongTimer(wsKey);
|
||||
}
|
||||
|
||||
_wsOpenHandler() {
|
||||
if (this.readyState === READY_STATE_CONNECTING) {
|
||||
this.logger.info('Websocket connected', { category: 'bybit-ws', livenet: this.options.livenet });
|
||||
this.emit('open');
|
||||
} else if (this.readyState === READY_STATE_RECONNECTING) {
|
||||
this.logger.info('Websocket reconnected', { category: 'bybit-ws', livenet: this.options.livenet });
|
||||
this.emit('reconnected');
|
||||
// Send a ping at intervals
|
||||
private clearPingTimer(wsKey: string) {
|
||||
const wsState = this.wsStore.get(wsKey);
|
||||
if (wsState?.activePingTimer) {
|
||||
clearInterval(wsState.activePingTimer);
|
||||
wsState.activePingTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Expect a pong within a time limit
|
||||
private clearPongTimer(wsKey: string) {
|
||||
const wsState = this.wsStore.get(wsKey);
|
||||
if (wsState?.activePongTimer) {
|
||||
clearTimeout(wsState.activePongTimer);
|
||||
wsState.activePongTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send WS message to subscribe to topics.
|
||||
*/
|
||||
private requestSubscribeTopics(wsKey: string, topics: string[]) {
|
||||
const wsMessage = JSON.stringify({
|
||||
op: 'subscribe',
|
||||
args: topics
|
||||
});
|
||||
|
||||
this.tryWsSend(wsKey, wsMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send WS message to unsubscribe from topics.
|
||||
*/
|
||||
private requestUnsubscribeTopics(wsKey: string, topics: string[]) {
|
||||
const wsMessage = JSON.stringify({
|
||||
op: 'unsubscribe',
|
||||
args: topics
|
||||
});
|
||||
|
||||
this.tryWsSend(wsKey, wsMessage);
|
||||
}
|
||||
|
||||
private tryWsSend(wsKey: string, wsMessage: string) {
|
||||
try {
|
||||
this.logger.silly(`Sending upstream ws message: `, { ...loggerCategory, wsMessage, wsKey });
|
||||
if (!wsKey) {
|
||||
throw new Error('Cannot send message due to no known websocket for this wsKey');
|
||||
}
|
||||
this.getWs(wsKey)?.send(wsMessage);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to send WS message`, { ...loggerCategory, wsMessage, wsKey, exception: e });
|
||||
}
|
||||
}
|
||||
|
||||
private connectToWsUrl(url: string, wsKey: string): WebSocket {
|
||||
this.logger.silly(`Opening WS connection to URL: ${url}`, { ...loggerCategory, wsKey })
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
ws.onopen = event => this.onWsOpen(event, wsKey);
|
||||
ws.onmessage = event => this.onWsMessage(event, wsKey);
|
||||
ws.onerror = event => this.onWsError(event, wsKey);
|
||||
ws.onclose = event => this.onWsClose(event, wsKey);
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
private onWsOpen(event, wsKey: string) {
|
||||
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
|
||||
this.logger.info('Websocket connected', { ...loggerCategory, wsKey, livenet: this.isLivenet(), linear: this.isLinear() });
|
||||
this.emit('open', { wsKey, event });
|
||||
} else if (this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)) {
|
||||
this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey });
|
||||
this.emit('reconnected', { wsKey, event });
|
||||
}
|
||||
|
||||
this.readyState = READY_STATE_CONNECTED;
|
||||
this.setWsState(wsKey, READY_STATE_CONNECTED);
|
||||
|
||||
this._subscribe([...this._subscriptions]);
|
||||
this.pingInterval = setInterval(this._ping.bind(this), this.options.pingInterval);
|
||||
this.requestSubscribeTopics(wsKey, [...this.wsStore.getTopics(wsKey)]);
|
||||
|
||||
this.wsStore.get(wsKey, true)!.activePingTimer = setInterval(
|
||||
() => this.ping(wsKey),
|
||||
this.options.pingInterval
|
||||
);
|
||||
}
|
||||
|
||||
_wsMessageHandler(message) {
|
||||
const msg = JSON.parse(message && message.data || message);
|
||||
private onWsMessage(event, wsKey: string) {
|
||||
const msg = JSON.parse(event && event.data || event);
|
||||
|
||||
if ('success' in msg) {
|
||||
this._handleResponse(msg);
|
||||
this.onWsMessageResponse(msg, wsKey);
|
||||
} else if (msg.topic) {
|
||||
this._handleUpdate(msg);
|
||||
this.onWsMessageUpdate(msg);
|
||||
} else {
|
||||
this.logger.warning('Got unhandled ws message', msg);
|
||||
this.logger.warning('Got unhandled ws message', { ...loggerCategory, message: msg, event, wsKey});
|
||||
}
|
||||
}
|
||||
|
||||
_wsOnErrorHandler(err) {
|
||||
this.logger.error('Websocket error', {category: 'bybit-ws', err});
|
||||
if (this.readyState === READY_STATE_CONNECTED) this.emit('error', err);
|
||||
private onWsError(err, wsKey: string) {
|
||||
this.parseWsError('Websocket error', err, wsKey);
|
||||
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
}
|
||||
|
||||
_wsCloseHandler() {
|
||||
this.logger.info('Websocket connection closed', {category: 'bybit-ws'});
|
||||
private onWsClose(event, wsKey: string) {
|
||||
this.logger.info('Websocket connection closed', { ...loggerCategory, wsKey});
|
||||
|
||||
if (this.readyState !== READY_STATE_CLOSING) {
|
||||
this._reconnect(this.options.reconnectTimeout);
|
||||
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CLOSING) {
|
||||
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
|
||||
this.emit('reconnect');
|
||||
} else {
|
||||
this.readyState = READY_STATE_INITIAL;
|
||||
this.setWsState(wsKey, READY_STATE_INITIAL);
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
_handleResponse(response) {
|
||||
if (
|
||||
response.request &&
|
||||
response.request.op === 'ping' &&
|
||||
response.ret_msg === 'pong' &&
|
||||
response.success === true
|
||||
) {
|
||||
this.logger.silly('pong recieved', {category: 'bybit-ws'});
|
||||
clearTimeout(this.pongTimeout);
|
||||
private onWsMessageResponse(response: any, wsKey: string) {
|
||||
if (isWsPong(response)) {
|
||||
this.logger.silly('Received pong', { ...loggerCategory, wsKey });
|
||||
this.clearPongTimer(wsKey);
|
||||
} else {
|
||||
this.emit('response', response);
|
||||
}
|
||||
}
|
||||
|
||||
_handleUpdate(message) {
|
||||
private onWsMessageUpdate(message: any) {
|
||||
this.emit('update', message);
|
||||
}
|
||||
|
||||
_subscribe(topics) {
|
||||
const msgStr = JSON.stringify({
|
||||
op: 'subscribe',
|
||||
'args': topics
|
||||
});
|
||||
|
||||
this.ws.send(msgStr);
|
||||
private getWs(wsKey: string) {
|
||||
return this.wsStore.getWs(wsKey);
|
||||
}
|
||||
|
||||
_unsubscribe(topics) {
|
||||
const msgStr = JSON.stringify({
|
||||
op: 'unsubscribe',
|
||||
'args': topics
|
||||
});
|
||||
private setWsState(wsKey: string, state: WsConnectionState) {
|
||||
this.wsStore.setConnectionState(wsKey, state);
|
||||
}
|
||||
|
||||
this.ws.send(msgStr);
|
||||
private getWsUrl(wsKey: string): string {
|
||||
if (this.options.wsUrl) {
|
||||
return this.options.wsUrl;
|
||||
}
|
||||
|
||||
const networkKey = this.options.livenet ? 'livenet' : 'testnet';
|
||||
if (this.isLinear() || wsKey.startsWith('linear')){
|
||||
if (wsKey === wsKeyLinearPublic) {
|
||||
return linearEndpoints.public[networkKey];
|
||||
}
|
||||
if (wsKey === wsKeyLinearPrivate) {
|
||||
return linearEndpoints.private[networkKey];
|
||||
}
|
||||
this.logger.error('Unhandled linear wsKey: ', { ...loggerCategory, wsKey });
|
||||
return linearEndpoints[networkKey];
|
||||
}
|
||||
return inverseEndpoints[networkKey];
|
||||
}
|
||||
|
||||
private getWsKeyForTopic(topic: string) {
|
||||
return this.isInverse() ? wsKeyInverse : getLinearWsKeyForTopic(topic);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user