Merge pull request #45 from tiagosiebler/feat/ts
TypeScript Introduction
This commit is contained in:
1
.github/workflows/npmpublish.yml
vendored
1
.github/workflows/npmpublish.yml
vendored
@@ -41,6 +41,7 @@ jobs:
|
|||||||
registry-url: https://registry.npmjs.org/
|
registry-url: https://registry.npmjs.org/
|
||||||
|
|
||||||
#- run: npm ci
|
#- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
- run: npm publish
|
- run: npm publish
|
||||||
if: steps.version-updated.outputs.has-updated
|
if: steps.version-updated.outputs.has-updated
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,3 +19,5 @@ node_modules/
|
|||||||
.env
|
.env
|
||||||
.env.test
|
.env.test
|
||||||
.cache
|
.cache
|
||||||
|
lib
|
||||||
|
bundleReport.html
|
||||||
|
|||||||
67
README.md
67
README.md
@@ -5,16 +5,11 @@
|
|||||||
|
|
||||||
[1]: https://www.npmjs.com/package/bybit-api
|
[1]: https://www.npmjs.com/package/bybit-api
|
||||||
|
|
||||||
A production-ready Node.js connector for the Bybit APIs and WebSockets.
|
A production-ready Node.js connector for the Bybit APIs and WebSockets, with TypeScript & browser support.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
`npm install --save bybit-api`
|
`npm install --save bybit-api`
|
||||||
|
|
||||||
## Usage
|
|
||||||
Create API credentials at Bybit
|
|
||||||
- [Livenet](https://bybit.com/app/user/api-management?affiliate_id=9410&language=en-US&group_id=0&group_type=1)
|
|
||||||
- [Testnet](https://testnet.bybit.com/app/user/api-management)
|
|
||||||
|
|
||||||
## Issues & Discussion
|
## Issues & Discussion
|
||||||
- Issues? Check the [issues tab](https://github.com/tiagosiebler/bybit-api/issues).
|
- 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.
|
- Discuss & collaborate with other node devs? Join our [Node.js Algo Traders](https://t.me/nodetraders) engineering community on telegram.
|
||||||
@@ -24,15 +19,65 @@ Most methods accept JS objects. These can be populated using parameters specifie
|
|||||||
- [Bybit API Inverse Documentation](https://bybit-exchange.github.io/docs/inverse/#t-introduction).
|
- [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 (not supported yet)](https://bybit-exchange.github.io/docs/linear/#t-introduction)
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
This project uses typescript. Resources are stored in 3 key structures:
|
||||||
|
- [src](./src) - the whole connector written in typescript
|
||||||
|
- [lib](./lib) - the javascript version of the project (compiled from typescript). This should not be edited directly, as it will be overwritten with each release.
|
||||||
|
- [dist](./dist) - the packed bundle of the project for use in browser environments.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Create API credentials at Bybit
|
||||||
|
- [Livenet](https://bybit.com/app/user/api-management?affiliate_id=9410&language=en-US&group_id=0&group_type=1)
|
||||||
|
- [Testnet](https://testnet.bybit.com/app/user/api-management)
|
||||||
|
|
||||||
|
### Browser Usage
|
||||||
|
Build a bundle using webpack:
|
||||||
|
- `npm install`
|
||||||
|
- `npm build`
|
||||||
|
- `npm pack`
|
||||||
|
|
||||||
|
The bundle can be found in `dist/`. Altough usage should be largely consistent, smaller differences will exist. Documentation is still TODO.
|
||||||
|
|
||||||
### Inverse Contracts
|
### Inverse Contracts
|
||||||
#### Rest client
|
#### Rest client
|
||||||
```javascript
|
```javascript
|
||||||
const {RestClient} = require('bybit-api');
|
const { RestClient } = require('bybit-api');
|
||||||
|
|
||||||
const API_KEY = 'xxx';
|
const API_KEY = 'xxx';
|
||||||
const PRIVATE_KEY = 'yyy';
|
const PRIVATE_KEY = 'yyy';
|
||||||
|
const useLivenet = false;
|
||||||
|
|
||||||
const client = new RestClient(API_KEY, PRIVATE_KEY);
|
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 client = new RestClient(
|
||||||
|
API_KEY,
|
||||||
|
PRIVATE_KEY,
|
||||||
|
|
||||||
|
// optional, uses testnet by default. Set to 'true' to use livenet.
|
||||||
|
useLivenet,
|
||||||
|
|
||||||
|
// restInverseOptions,
|
||||||
|
// requestLibraryOptions
|
||||||
|
);
|
||||||
|
|
||||||
client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'})
|
client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'})
|
||||||
.then(result => {
|
.then(result => {
|
||||||
@@ -43,11 +88,11 @@ client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'})
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
See inverse [rest-client.js](./lib/rest-client.js) for further information.
|
See inverse [rest-client.ts](./src/rest-client.ts) for further information.
|
||||||
|
|
||||||
#### Websocket client
|
#### Websocket client
|
||||||
```javascript
|
```javascript
|
||||||
const {WebsocketClient} = require('bybit-api');
|
const { WebsocketClient } = require('bybit-api');
|
||||||
|
|
||||||
const API_KEY = 'xxx';
|
const API_KEY = 'xxx';
|
||||||
const PRIVATE_KEY = 'yyy';
|
const PRIVATE_KEY = 'yyy';
|
||||||
@@ -105,7 +150,7 @@ ws.on('error', err => {
|
|||||||
console.error('ERR', err);
|
console.error('ERR', err);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
See inverse [websocket-client.js](./lib/websocket-client.js) & [ws api docs](./doc/websocket-client.md) for further information.
|
See inverse [websocket-client.ts](./src/websocket-client.ts) for further information.
|
||||||
|
|
||||||
### Customise Logging
|
### 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:
|
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:
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
# Rest API
|
|
||||||
|
|
||||||
|
|
||||||
## Class: RestClient
|
|
||||||
### new RestClient([key][, secret][, livenet][, options])
|
|
||||||
- `key` {String} Bybit API Key
|
|
||||||
- `secret` {String} Bybit private key
|
|
||||||
- `livenet` {Boolean} If false (default), use testnet.
|
|
||||||
- `options` {Object} Optional settings for custom behaviour.
|
|
||||||
- `recv_window` {Number} Optional, default 5000. Increase if recv errors are seen.
|
|
||||||
- `sync_interval_ms` {Number} Optional, default 3600000. Interval at which syncTime is performed.
|
|
||||||
|
|
||||||
If you only use the [public endpoints](#public-endpoints) you can omit key and secret.
|
|
||||||
|
|
||||||
### Private enpoints
|
|
||||||
#### async placeActiveOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-placev2active)
|
|
||||||
|
|
||||||
#### async getActiveOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-getactive)
|
|
||||||
|
|
||||||
#### async cancelActiveOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-cancelv2active)
|
|
||||||
|
|
||||||
#### async cancelAllActiveOrders(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-cancelallactive)
|
|
||||||
|
|
||||||
#### async replaceActiveOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-replaceactive)
|
|
||||||
|
|
||||||
#### async queryActiveOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-queryactive)
|
|
||||||
|
|
||||||
#### async placeConditionalOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-placecond)
|
|
||||||
|
|
||||||
#### async getConditionalOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-getcond)
|
|
||||||
|
|
||||||
#### async cancelConditionalOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-cancelcond)
|
|
||||||
|
|
||||||
#### async cancelAllConditionalOrders(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-cancelallcond)
|
|
||||||
|
|
||||||
#### async replaceConditionalOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-replacecond)
|
|
||||||
|
|
||||||
#### async queryConditionalOrder(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-querycond)
|
|
||||||
|
|
||||||
#### async getUserLeverage()
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-getleverage)
|
|
||||||
|
|
||||||
#### async changeUserLeverage(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-changeleverage)
|
|
||||||
|
|
||||||
#### async getPosition(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-mypositionv2)
|
|
||||||
|
|
||||||
#### async getPositions()
|
|
||||||
*Deprecated v1 method*
|
|
||||||
[See bybit documentation](https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/rest_api.md#positionlistget)
|
|
||||||
|
|
||||||
#### async changePositionMargin(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-changemargin)
|
|
||||||
|
|
||||||
#### async setTradingStop(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-tradingstop)
|
|
||||||
|
|
||||||
#### async getWalletFundRecords(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-walletrecords)
|
|
||||||
|
|
||||||
#### async getWithdrawRecords(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-withdrawrecords)
|
|
||||||
|
|
||||||
#### async getAssetExchangeRecords(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-assetexchangerecords)
|
|
||||||
|
|
||||||
#### async getWalletBalance(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-balance)
|
|
||||||
|
|
||||||
#### async setRiskLimit(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-setrisklimit)
|
|
||||||
|
|
||||||
#### async getRiskLimitList()
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-getrisklimit)
|
|
||||||
|
|
||||||
#### async getLastFundingRate(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-fundingrate)
|
|
||||||
|
|
||||||
#### async getMyLastFundingFee(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-mylastfundingfee)
|
|
||||||
|
|
||||||
#### async getPredictedFunding(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-predictedfunding)
|
|
||||||
|
|
||||||
#### async getTradeRecords(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-usertraderecords)
|
|
||||||
|
|
||||||
### Public enpoints
|
|
||||||
|
|
||||||
#### async getOrderBook(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-orderbook)
|
|
||||||
|
|
||||||
#### async getKline(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-querykline)
|
|
||||||
|
|
||||||
#### async getOpenInterest(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-marketopeninterest)
|
|
||||||
|
|
||||||
#### async getLatestBigDeal(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-marketbigdeal)
|
|
||||||
|
|
||||||
#### async getLongShortRatio(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-marketaccountratio)
|
|
||||||
|
|
||||||
#### async getLatestInformation()
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-latestsymbolinfo)
|
|
||||||
|
|
||||||
#### async getPublicTradingRecords(params)
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-publictradingrecords)
|
|
||||||
|
|
||||||
#### async getServerTime()
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-servertime)
|
|
||||||
|
|
||||||
#### async getApiAnnouncements()
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-announcement)
|
|
||||||
|
|
||||||
#### async getSymbols()
|
|
||||||
Returns symbol information (such as tick size & min notional):
|
|
||||||
[Meeting price restrictions](https://bybit-exchange.github.io/docs/inverse/#price-price)
|
|
||||||
|
|
||||||
[See bybit documentation](https://bybit-exchange.github.io/docs/inverse/#t-querysymbol)
|
|
||||||
|
|
||||||
#### async getTimeOffset()
|
|
||||||
|
|
||||||
Returns the time offset in ms to the server time retrieved by [`async getServerTime`](#async-getservertime).
|
|
||||||
If positive the time on the server is ahead of the clients time, if negative the time on the server is behind the clients time.
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const {RestClient} = require('bybit-api');
|
|
||||||
|
|
||||||
const API_KEY = 'xxx';
|
|
||||||
const PRIVATE_KEY = 'yyy';
|
|
||||||
|
|
||||||
const client = new RestClient(API_KEY, PRIVATE_KEY);
|
|
||||||
|
|
||||||
client.changeUserLeverage({leverage: 4, symbol: 'ETHUSD'})
|
|
||||||
.then(result => {
|
|
||||||
console.log(result);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
10
index.js
10
index.js
@@ -1,9 +1 @@
|
|||||||
const RestClient = require('./lib/rest-client');
|
module.exports = require('lib/index');
|
||||||
const WebsocketClient = require('./lib/websocket-client');
|
|
||||||
const DefaultLogger = require('./lib/logger');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
RestClient,
|
|
||||||
WebsocketClient,
|
|
||||||
DefaultLogger
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
module.exports = {
|
|
||||||
silly: function() {console.log(arguments);},
|
|
||||||
debug: function() {console.log(arguments);},
|
|
||||||
notice: function() {console.log(arguments);},
|
|
||||||
info: function() {console.info(arguments);},
|
|
||||||
warning: function() {console.warn(arguments);},
|
|
||||||
error: function() {console.error(arguments);},
|
|
||||||
};
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
const assert = require('assert');
|
|
||||||
const RequestWrapper = require('./util/requestWrapper');
|
|
||||||
|
|
||||||
module.exports = class RestClient {
|
|
||||||
/**
|
|
||||||
* @public Creates an instance of the inverse REST API client.
|
|
||||||
*
|
|
||||||
* @param {string} key - your API key
|
|
||||||
* @param {string} secret - your API secret
|
|
||||||
* @param {boolean} [livenet=false]
|
|
||||||
* @param {*} [options={}] options to configure REST API connectivity
|
|
||||||
* @param {*} [requestOptions={}] HTTP networking options for axios
|
|
||||||
*/
|
|
||||||
constructor(key, secret, livenet=false, options={}, requestOptions={}) {
|
|
||||||
this.request = new RequestWrapper(...arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
async placeActiveOrder(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.side, 'Parameter side is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.order_type, 'Parameter order_type is required');
|
|
||||||
assert(params.qty, 'Parameter qty is required');
|
|
||||||
assert(params.time_in_force, 'Parameter time_in_force is required');
|
|
||||||
|
|
||||||
if (params.order_type === 'Limit') assert(params.price, 'Parameter price is required for limit orders');
|
|
||||||
|
|
||||||
return await this.request.post('v2/private/order/create', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getActiveOrder(params) {
|
|
||||||
return await this.request.get('open-api/order/list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelActiveOrder(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
|
|
||||||
|
|
||||||
return await this.request.post('v2/private/order/cancel', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelAllActiveOrders(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.post('v2/private/order/cancelAll', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async replaceActiveOrder(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.post('v2/private/order/replace', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use replaceActiveOrder()
|
|
||||||
*/
|
|
||||||
async replaceActiveOrderOld(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.post('open-api/order/replace', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async queryActiveOrder(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/private/order', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async placeConditionalOrder(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.side, 'Parameter side is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.order_type, 'Parameter order_type is required');
|
|
||||||
assert(params.qty, 'Parameter qty is required');
|
|
||||||
assert(params.base_price, 'Parameter base_price is required');
|
|
||||||
assert(params.stop_px, 'Parameter stop_px is required');
|
|
||||||
assert(params.time_in_force, 'Parameter time_in_force is required');
|
|
||||||
|
|
||||||
if (params.order_type === 'Limit') assert(params.price, 'Parameter price is required for limit orders');
|
|
||||||
|
|
||||||
return await this.request.post('v2/private/stop-order/create', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use placeConditionalOrder
|
|
||||||
*/
|
|
||||||
async placeConditionalOrderOld(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.side, 'Parameter side is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.order_type, 'Parameter order_type is required');
|
|
||||||
assert(params.qty, 'Parameter qty is required');
|
|
||||||
assert(params.time_in_force, 'Parameter time_in_force is required');
|
|
||||||
assert(params.base_price, 'Parameter base_price is required');
|
|
||||||
assert(params.stop_px, 'Parameter stop_px is required');
|
|
||||||
|
|
||||||
if (params.order_type === 'Limit') assert(params.price, 'Parameter price is required for limit orders');
|
|
||||||
|
|
||||||
return await this.request.post('open-api/stop-order/create', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getConditionalOrder(params) {
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
return await this.request.get('v2/private/stop-order/list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use placeConditionalOrder
|
|
||||||
*/
|
|
||||||
async getConditionalOrderOld(params) {
|
|
||||||
return await this.request.get('open-api/stop-order/list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelConditionalOrder(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.stop_order_id || params.order_link_id, 'Parameter stop_order_id OR order_link_id is required');
|
|
||||||
|
|
||||||
return await this.request.post('v2/private/stop-order/cancel', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use cancelConditionalOrder
|
|
||||||
*/
|
|
||||||
async cancelConditionalOrderOld(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.stop_order_id, 'Parameter stop_order_id is required');
|
|
||||||
|
|
||||||
return await this.request.post('open-api/stop-order/cancel', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelAllConditionalOrders(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.post('v2/private/stop-order/cancelAll', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async replaceConditionalOrder(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.stop_order_id || params.order_link_id, 'Parameter stop_order_id OR order_link_id is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.post('v2/private/stop-order/replace', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use replaceConditionalOrder
|
|
||||||
*/
|
|
||||||
async replaceConditionalOrderOld(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.stop_order_id, 'Parameter stop_order_id is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.post('open-api/stop-order/replace', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async queryConditionalOrder(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.stop_order_id || params.order_link_id, 'Parameter order_id OR order_link_id is required');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/private/stop-order', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use getPosition() instead
|
|
||||||
*/
|
|
||||||
async getUserLeverage() {
|
|
||||||
return await this.request.get('user/leverage');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPosition(params) {
|
|
||||||
return await this.request.get('v2/private/position/list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async changeUserLeverage(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
if (typeof params.leverage === 'undefined') {
|
|
||||||
throw new Error('Parameter leverage is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.request.post('user/leverage/save', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use getPosition() instead
|
|
||||||
*/
|
|
||||||
async getPositions() {
|
|
||||||
return await this.request.get('position/list');
|
|
||||||
}
|
|
||||||
|
|
||||||
async changePositionMargin(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.margin, 'Parameter margin is required');
|
|
||||||
|
|
||||||
return await this.request.post('position/change-position-margin', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setTradingStop(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.post('open-api/position/trading-stop', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWalletFundRecords(params) {
|
|
||||||
return await this.request.get('open-api/wallet/fund/records', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWithdrawRecords(params) {
|
|
||||||
return await this.request.get('open-api/wallet/withdraw/list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAssetExchangeRecords(params) {
|
|
||||||
return await this.request.get('v2/private/exchange-order/list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWalletBalance(params) {
|
|
||||||
return await this.request.get('v2/private/wallet/balance', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setRiskLimit(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.risk_id, 'Parameter risk_id is required');
|
|
||||||
|
|
||||||
return await this.request.post('open-api/wallet/risk-limit', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRiskLimitList() {
|
|
||||||
return await this.request.get('open-api/wallet/risk-limit/list');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLastFundingRate(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('open-api/funding/prev-funding-rate', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMyLastFundingFee(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('open-api/funding/prev-funding', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPredictedFunding(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('open-api/funding/predicted-funding', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTradeRecords(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.order_id || params.symbol, 'Parameter order_id OR symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/private/execution/list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOrderBook(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/public/orderBook/L2', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getKline(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.interval, 'Parameter interval is required');
|
|
||||||
assert(params.from, 'Parameter from is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/public/kline/list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOpenInterest(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.period, 'Parameter period is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/public/open-interest', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLatestBigDeal(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/public/big-deal', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLongShortRatio(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
assert(params.period, 'Parameter period is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/public/account-ratio', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLatestInformation() {
|
|
||||||
return await this.request.get('v2/public/tickers');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPublicTradingRecords(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/public/trading-records', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPublicLiquidations(params) {
|
|
||||||
assert(params, 'No params passed');
|
|
||||||
assert(params.symbol, 'Parameter symbol is required');
|
|
||||||
|
|
||||||
return await this.request.get('v2/public/liq-records', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServerTime() {
|
|
||||||
return await this.request.get('v2/public/time');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getApiAnnouncements() {
|
|
||||||
return await this.request.get('v2/public/announcement');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSymbols() {
|
|
||||||
return await this.request.get('v2/public/symbols');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTimeOffset() {
|
|
||||||
return await this.request.getTimeOffset();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const { createHmac } = require('crypto');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
signMessage(message, secret) {
|
|
||||||
return createHmac('sha256', secret)
|
|
||||||
.update(message)
|
|
||||||
.digest('hex');
|
|
||||||
},
|
|
||||||
serializeParams(params = {}, strict_validation = false) {
|
|
||||||
return Object.keys(params)
|
|
||||||
.sort()
|
|
||||||
.map(key => {
|
|
||||||
const value = params[key];
|
|
||||||
if (strict_validation === true && typeof value === 'undefined') {
|
|
||||||
throw new Error('Failed to sign API request due to undefined parameter');
|
|
||||||
}
|
|
||||||
return `${key}=${value}`;
|
|
||||||
})
|
|
||||||
.join('&');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
const assert = require('assert');
|
|
||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
const { signMessage, serializeParams } = require('./requestUtils');
|
|
||||||
|
|
||||||
const baseUrls = {
|
|
||||||
livenet: 'https://api.bybit.com',
|
|
||||||
testnet: 'https://api-testnet.bybit.com'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = class RequestUtil {
|
|
||||||
constructor(key, secret, livenet=false, options={}, requestOptions={}) {
|
|
||||||
this._timeOffset = null;
|
|
||||||
this._syncTimePromise = null;
|
|
||||||
|
|
||||||
this.options = {
|
|
||||||
recv_window: 5000,
|
|
||||||
// how often to sync time drift with bybit servers
|
|
||||||
sync_interval_ms: 3600000,
|
|
||||||
// if true, we'll throw errors if any params are undefined
|
|
||||||
strict_param_validation: false,
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
this.baseUrl = baseUrls[livenet === true ? 'livenet' : 'testnet'];
|
|
||||||
if (options.baseUrl) {
|
|
||||||
this.baseUrl = options.baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.globalRequestOptions = {
|
|
||||||
// in ms == 5 minutes by default
|
|
||||||
timeout: 1000 * 60 * 5,
|
|
||||||
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
|
|
||||||
...requestOptions,
|
|
||||||
headers: {
|
|
||||||
'referer': 'bybitapinode'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (key) {
|
|
||||||
assert(secret, 'Secret is required for private enpoints');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._syncTime();
|
|
||||||
setInterval(this._syncTime.bind(this), parseInt(this.options.sync_interval_ms));
|
|
||||||
|
|
||||||
this.key = key;
|
|
||||||
this.secret = secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(endpoint, params) {
|
|
||||||
const result = await this._call('GET', endpoint, params);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async post(endpoint, params) {
|
|
||||||
const result = await this._call('POST', endpoint, params);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTimeOffset() {
|
|
||||||
const start = Date.now();
|
|
||||||
const result = await this.get('v2/public/time');
|
|
||||||
const end = Date.now();
|
|
||||||
|
|
||||||
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
async _call(method, endpoint, params) {
|
|
||||||
const publicEndpoint = endpoint.startsWith('v2/public');
|
|
||||||
|
|
||||||
if (!publicEndpoint) {
|
|
||||||
if (!this.key || !this.secret) {
|
|
||||||
throw new Error('Private endpoints require api and private keys set');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._timeOffset === null) {
|
|
||||||
await this._syncTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
params = this._signRequest(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
...this.globalRequestOptions,
|
|
||||||
url: [this.baseUrl, endpoint].join('/'),
|
|
||||||
method: method,
|
|
||||||
json: true
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (method) {
|
|
||||||
case 'GET':
|
|
||||||
options.params = params;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
options.data = params;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return axios(options).then(response => {
|
|
||||||
if (response.status == 200) {
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw {
|
|
||||||
code: response.status,
|
|
||||||
message: response.statusText,
|
|
||||||
body: response.data,
|
|
||||||
requestOptions: options
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
if (!e.response) {
|
|
||||||
// Something happened in setting up the request that triggered an Error
|
|
||||||
if (!e.request) {
|
|
||||||
throw e.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
// request made but no response received
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The request was made and the server responded with a status code
|
|
||||||
// that falls out of the range of 2xx
|
|
||||||
throw {
|
|
||||||
code: e.response.statusCode,
|
|
||||||
message: e.response.message,
|
|
||||||
body: e.response.body,
|
|
||||||
requestOptions: options,
|
|
||||||
headers: e.response.headers
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_signRequest(data) {
|
|
||||||
const params = {
|
|
||||||
...data,
|
|
||||||
api_key: this.key,
|
|
||||||
timestamp: Date.now() + this._timeOffset
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
|
|
||||||
if (this.options.recv_window && !params.recv_window) {
|
|
||||||
params.recv_window = this.options.recv_window;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.key && this.secret) {
|
|
||||||
const serializedParams = serializeParams(params, this.options.strict_param_validation);
|
|
||||||
params.sign = signMessage(serializedParams, this.secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
_syncTime() {
|
|
||||||
if (this._syncTimePromise !== null) {
|
|
||||||
return this._syncTimePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._syncTimePromise = this.getTimeOffset().then(offset => {
|
|
||||||
this._timeOffset = offset;
|
|
||||||
this._syncTimePromise = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this._syncTimePromise;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
2354
package-lock.json
generated
2354
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
@@ -1,14 +1,43 @@
|
|||||||
{
|
{
|
||||||
"name": "bybit-api",
|
"name": "bybit-api",
|
||||||
"version": "1.2.5",
|
"version": "1.3.0",
|
||||||
"description": "A production-ready Node.js connector for the Bybit APIs and WebSockets",
|
"description": "A production-ready Node.js connector for the Bybit APIs and WebSockets",
|
||||||
"main": "index.js",
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"lib/*",
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"clean": "rm -rf lib dist",
|
||||||
|
"prebuild": "npm run clean",
|
||||||
|
"build": "tsc",
|
||||||
|
"pack": "webpack --config webpack/webpack.config.js",
|
||||||
|
"prepublish": "npm run build",
|
||||||
|
"betapublish": "npm publish --tag beta"
|
||||||
},
|
},
|
||||||
"repository": {
|
"author": "Tiago Siebler (https://github.com/tiagosiebler)",
|
||||||
"type": "git",
|
"contributors": [
|
||||||
"url": "https://github.com/tiagosiebler/bybit-api"
|
"Stefan Aebischer <os@pixtron.ch> (https://pixtron.ch)"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.0",
|
||||||
|
"isomorphic-ws": "^4.0.1",
|
||||||
|
"ws": "^7.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^14.14.7",
|
||||||
|
"buffer": "^6.0.2",
|
||||||
|
"crypto-browserify": "^3.12.0",
|
||||||
|
"eslint": "^7.10.0",
|
||||||
|
"source-map-loader": "^1.1.2",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"ts-loader": "^8.0.11",
|
||||||
|
"typescript": "^4.0.5",
|
||||||
|
"webpack": "^5.4.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.1.0",
|
||||||
|
"webpack-cli": "^4.2.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bybit",
|
"bybit",
|
||||||
@@ -24,20 +53,17 @@
|
|||||||
"bitcoin",
|
"bitcoin",
|
||||||
"best"
|
"best"
|
||||||
],
|
],
|
||||||
"author": "Tiago Siebler (https://github.com/tiagosiebler)",
|
"funding": {
|
||||||
"contributors": [
|
"type": "individual",
|
||||||
"Stefan Aebischer <os@pixtron.ch> (https://pixtron.ch)"
|
"url": "https://github.com/sponsors/tiagosiebler"
|
||||||
],
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/tiagosiebler/bybit-api"
|
||||||
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tiagosiebler/bybit-api/issues"
|
"url": "https://github.com/tiagosiebler/bybit-api/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/tiagosiebler/bybit-api#readme",
|
"homepage": "https://github.com/tiagosiebler/bybit-api#readme"
|
||||||
"dependencies": {
|
|
||||||
"axios": "^0.21.0",
|
|
||||||
"ws": "^7.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^7.10.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/index.ts
Normal file
3
src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './rest-client';
|
||||||
|
export * from './websocket-client';
|
||||||
|
export * from './logger';
|
||||||
22
src/logger.ts
Normal file
22
src/logger.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export type LogParams = null | any;
|
||||||
|
|
||||||
|
export const DefaultLogger = {
|
||||||
|
silly: (...params: LogParams): void => {
|
||||||
|
console.log(params);
|
||||||
|
},
|
||||||
|
debug: (...params: LogParams): void => {
|
||||||
|
console.log(params);
|
||||||
|
},
|
||||||
|
notice: (...params: LogParams): void => {
|
||||||
|
console.log(params);
|
||||||
|
},
|
||||||
|
info: (...params: LogParams): void => {
|
||||||
|
console.info(params);
|
||||||
|
},
|
||||||
|
warning: (...params: LogParams): void => {
|
||||||
|
console.error(params);
|
||||||
|
},
|
||||||
|
error: (...params: LogParams): void => {
|
||||||
|
console.error(params);
|
||||||
|
}
|
||||||
|
};
|
||||||
531
src/rest-client.ts
Normal file
531
src/rest-client.ts
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
import { AxiosRequestConfig } from 'axios';
|
||||||
|
import { GenericAPIResponse, getBaseRESTInverseUrl, RestClientInverseOptions } from './util/requestUtils';
|
||||||
|
import RequestWrapper from './util/requestWrapper';
|
||||||
|
|
||||||
|
export class RestClient {
|
||||||
|
private 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 {RestClientInverseOptions} [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 = {}
|
||||||
|
) {
|
||||||
|
this.requestWrapper = new RequestWrapper(
|
||||||
|
key,
|
||||||
|
secret,
|
||||||
|
getBaseRESTInverseUrl(useLivenet),
|
||||||
|
restInverseOptions,
|
||||||
|
httpOptions
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Market Data Endpoints
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
getOrderBook(params: {
|
||||||
|
symbol: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('v2/public/orderBook/L2', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getKline(params: {
|
||||||
|
symbol: string;
|
||||||
|
interval: string;
|
||||||
|
from: number;
|
||||||
|
limit?: number;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('v2/public/kline/list', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use getTickers() instead
|
||||||
|
*/
|
||||||
|
getLatestInformation(params?: {
|
||||||
|
symbol?: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.getTickers(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTickers(params?: {
|
||||||
|
symbol?: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('v2/public/tickers', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use getTrades() instead
|
||||||
|
*/
|
||||||
|
getPublicTradingRecords(params: {
|
||||||
|
symbol: string;
|
||||||
|
from?: number;
|
||||||
|
limit?: number;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.getTrades(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrades(params: {
|
||||||
|
symbol: string;
|
||||||
|
from?: number;
|
||||||
|
limit?: number;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('v2/public/trading-records', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSymbols(): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('v2/public/symbols');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use getLiquidations() instead
|
||||||
|
*/
|
||||||
|
getPublicLiquidations(params: {
|
||||||
|
symbol: string;
|
||||||
|
from?: number;
|
||||||
|
limit?: number;
|
||||||
|
start_time?: number;
|
||||||
|
end_time?: number;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
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;
|
||||||
|
from: number;
|
||||||
|
limit?: number;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('v2/public/mark-price-kline', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
placeActiveOrder(orderRequest: {
|
||||||
|
side: string;
|
||||||
|
symbol: string;
|
||||||
|
order_type: string;
|
||||||
|
qty: number;
|
||||||
|
price?: number;
|
||||||
|
time_in_force: string;
|
||||||
|
take_profit?: number;
|
||||||
|
stop_loss?: number;
|
||||||
|
reduce_only?: boolean;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveOrderList(params: {
|
||||||
|
symbol: string;
|
||||||
|
order_status?: string;
|
||||||
|
direction?: string;
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAllActiveOrders(params: {
|
||||||
|
symbol: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.post('v2/private/order/cancelAll', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceActiveOrder(params: {
|
||||||
|
order_id?: string;
|
||||||
|
order_link_id?: string;
|
||||||
|
symbol: string;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
placeConditionalOrder(params: {
|
||||||
|
side: string;
|
||||||
|
symbol: string;
|
||||||
|
order_type: string;
|
||||||
|
qty: string;
|
||||||
|
price?: string;
|
||||||
|
base_price: string;
|
||||||
|
stop_px: string;
|
||||||
|
time_in_force: string;
|
||||||
|
trigger_by?: string;
|
||||||
|
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;
|
||||||
|
direction?: string;
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
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 {
|
||||||
|
return this.requestWrapper.post('v2/private/stop-order/cancelAll', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceConditionalOrder(params: {
|
||||||
|
stop_order_id?: string;
|
||||||
|
order_link_id?: string;
|
||||||
|
symbol: string;
|
||||||
|
p_r_qty?: number;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use getPosition() instead
|
||||||
|
*/
|
||||||
|
getUserLeverage(): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('user/leverage');
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition(params?: {
|
||||||
|
symbol?: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('v2/private/position/list', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use getPosition() instead
|
||||||
|
*/
|
||||||
|
getPositions(): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('position/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
changePositionMargin(params: {
|
||||||
|
symbol: string;
|
||||||
|
margin: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.post('position/change-position-margin', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTradingStop(params: {
|
||||||
|
symbol: string;
|
||||||
|
take_profit?: number;
|
||||||
|
stop_loss?: number;
|
||||||
|
tp_trigger_by?: string;
|
||||||
|
sl_trigger_by?: string;
|
||||||
|
new_trailing_active?: number;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.post('open-api/position/trading-stop', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserLeverage(params: {
|
||||||
|
symbol: string;
|
||||||
|
leverage: number;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.post('user/leverage/save', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use setUserLeverage() instead
|
||||||
|
*/
|
||||||
|
changeUserLeverage(params: any): GenericAPIResponse {
|
||||||
|
return this.setUserLeverage(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTradeRecords(params: {
|
||||||
|
order_id?: string;
|
||||||
|
symbol: string;
|
||||||
|
start_time?: number;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
order?: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('v2/private/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('v2/private/trade/closed-pnl/list', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRiskLimitList(): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('open-api/wallet/risk-limit/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
setRiskLimit(params: {
|
||||||
|
symbol: string;
|
||||||
|
risk_id: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.post('open-api/wallet/risk-limit', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastFundingRate(params: {
|
||||||
|
symbol: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('open-api/funding/prev-funding-rate', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMyLastFundingFee(params: {
|
||||||
|
symbol: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('open-api/funding/prev-funding', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPredictedFunding(params: {
|
||||||
|
symbol: string;
|
||||||
|
}): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('open-api/funding/predicted-funding', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getApiKeyInfo(): GenericAPIResponse {
|
||||||
|
return this.requestWrapper.get('open-api/api-key');
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
59
src/util/requestUtils.ts
Normal file
59
src/util/requestUtils.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { createHmac } from 'crypto';
|
||||||
|
|
||||||
|
export interface RestClientInverseOptions {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GenericAPIResponse = Promise<any>;
|
||||||
|
|
||||||
|
export function signMessage(message: string, secret: string): string {
|
||||||
|
return createHmac('sha256', secret)
|
||||||
|
.update(message)
|
||||||
|
.digest('hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
export function serializeParams(params: object = {}, strict_validation = false): string {
|
||||||
|
return Object.keys(params)
|
||||||
|
.sort()
|
||||||
|
.map(key => {
|
||||||
|
const value = params[key];
|
||||||
|
if (strict_validation === true && typeof value === 'undefined') {
|
||||||
|
throw new Error('Failed to sign API request due to undefined parameter');
|
||||||
|
}
|
||||||
|
return `${key}=${value}`;
|
||||||
|
})
|
||||||
|
.join('&');
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getBaseRESTInverseUrl(useLivenet?: boolean, restInverseOptions?: RestClientInverseOptions) {
|
||||||
|
const baseUrlsInverse = {
|
||||||
|
livenet: 'https://api.bybit.com',
|
||||||
|
testnet: 'https://api-testnet.bybit.com'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (restInverseOptions?.baseUrl) {
|
||||||
|
return restInverseOptions.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useLivenet === true) {
|
||||||
|
return baseUrlsInverse.livenet;
|
||||||
|
}
|
||||||
|
return baseUrlsInverse.testnet;
|
||||||
|
}
|
||||||
193
src/util/requestWrapper.ts
Normal file
193
src/util/requestWrapper.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
|
||||||
|
|
||||||
|
import { signMessage, serializeParams, RestClientInverseOptions, GenericAPIResponse } from './requestUtils';
|
||||||
|
|
||||||
|
export default class RequestUtil {
|
||||||
|
private timeOffset: number | null;
|
||||||
|
private syncTimePromise: null | Promise<any>;
|
||||||
|
private options: RestClientInverseOptions;
|
||||||
|
private baseUrl: string;
|
||||||
|
private globalRequestOptions: AxiosRequestConfig;
|
||||||
|
private key: string | undefined;
|
||||||
|
private secret: string | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
key: string | undefined,
|
||||||
|
secret: string | undefined,
|
||||||
|
baseUrl: string,
|
||||||
|
options: RestClientInverseOptions = {},
|
||||||
|
requestOptions: AxiosRequestConfig = {}
|
||||||
|
) {
|
||||||
|
this.timeOffset = null;
|
||||||
|
this.syncTimePromise = null;
|
||||||
|
|
||||||
|
this.options = {
|
||||||
|
recv_window: 5000,
|
||||||
|
// how often to sync time drift with bybit servers
|
||||||
|
sync_interval_ms: 3600000,
|
||||||
|
// if true, we'll throw errors if any params are undefined
|
||||||
|
strict_param_validation: false,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
this.globalRequestOptions = {
|
||||||
|
// in ms == 5 minutes by default
|
||||||
|
timeout: 1000 * 60 * 5,
|
||||||
|
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
|
||||||
|
...requestOptions,
|
||||||
|
headers: {
|
||||||
|
'x-referer': 'bybitapinode'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
|
||||||
|
if (key && !secret) {
|
||||||
|
throw new Error('API Key & Secret are both required for private enpoints')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.disable_time_sync !== true) {
|
||||||
|
this.syncTime();
|
||||||
|
setInterval(this.syncTime.bind(this), +this.options.sync_interval_ms!);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.key = key;
|
||||||
|
this.secret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: type check that endpoint never starts with forward slash??
|
||||||
|
get(endpoint: string, params?: any): GenericAPIResponse {
|
||||||
|
return this._call('GET', endpoint, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
post(endpoint: string, params?: any): GenericAPIResponse {
|
||||||
|
return this._call('POST', endpoint, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 (!this.key || !this.secret) {
|
||||||
|
throw new Error('Private endpoints require api and private keys set');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeOffset === null) {
|
||||||
|
await this.syncTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
params = this.signRequest(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
...this.globalRequestOptions,
|
||||||
|
url: [this.baseUrl, endpoint].join('/'),
|
||||||
|
method: method,
|
||||||
|
json: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
options.params = params;
|
||||||
|
} else {
|
||||||
|
options.data = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios(options).then(response => {
|
||||||
|
if (response.status == 200) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw response;
|
||||||
|
}).catch(this.parseException);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private generic handler to parse request exceptions
|
||||||
|
*/
|
||||||
|
parseException(e: any): unknown {
|
||||||
|
if (this.options.parse_exceptions === false) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something happened in setting up the request that triggered an Error
|
||||||
|
if (!e.response) {
|
||||||
|
if (!e.request) {
|
||||||
|
throw e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// request made but no response received
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The request was made and the server responded with a status code
|
||||||
|
// that falls out of the range of 2xx
|
||||||
|
const response: AxiosResponse = e.response;
|
||||||
|
throw {
|
||||||
|
code: response.status,
|
||||||
|
message: response.statusText,
|
||||||
|
body: response.data,
|
||||||
|
headers: response.headers,
|
||||||
|
requestOptions: this.options
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private sign request and set recv window
|
||||||
|
*/
|
||||||
|
signRequest(data: any): any {
|
||||||
|
const params = {
|
||||||
|
...data,
|
||||||
|
api_key: this.key,
|
||||||
|
timestamp: Date.now() + (this.timeOffset || 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
|
||||||
|
if (this.options.recv_window && !params.recv_window) {
|
||||||
|
params.recv_window = this.options.recv_window;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.key && this.secret) {
|
||||||
|
const serializedParams = serializeParams(params, this.options.strict_param_validation);
|
||||||
|
params.sign = signMessage(serializedParams, this.secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private trigger time sync and store promise
|
||||||
|
*/
|
||||||
|
syncTime(): GenericAPIResponse {
|
||||||
|
if (this.options.disable_time_sync === true) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.syncTimePromise !== null) {
|
||||||
|
return this.syncTimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncTimePromise = this.getTimeOffset().then(offset => {
|
||||||
|
this.timeOffset = offset;
|
||||||
|
this.syncTimePromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.syncTimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated move this somewhere else, because v2/public/time shouldn't be hardcoded here
|
||||||
|
*
|
||||||
|
* @returns {Promise<number>}
|
||||||
|
* @memberof RequestUtil
|
||||||
|
*/
|
||||||
|
async getTimeOffset(): Promise<number> {
|
||||||
|
const start = Date.now();
|
||||||
|
const result = await this.get('v2/public/time');
|
||||||
|
const end = Date.now();
|
||||||
|
|
||||||
|
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
const { EventEmitter } = require('events');
|
import { EventEmitter } from 'events';
|
||||||
const WebSocket = require('ws');
|
|
||||||
|
|
||||||
const defaultLogger = require('./logger');
|
import { RestClient } from './rest-client';
|
||||||
const RestClient = require('./rest-client');
|
import { DefaultLogger } from './logger';
|
||||||
const { signMessage, serializeParams } = require('./util/requestUtils');
|
import { signMessage, serializeParams } from './util/requestUtils';
|
||||||
|
// import WebSocket from 'ws';
|
||||||
|
import WebSocket from 'isomorphic-ws';
|
||||||
|
|
||||||
const wsUrls = {
|
const wsUrls = {
|
||||||
livenet: 'wss://stream.bybit.com/realtime',
|
livenet: 'wss://stream.bybit.com/realtime',
|
||||||
@@ -16,15 +17,39 @@ const READY_STATE_CONNECTED = 2;
|
|||||||
const READY_STATE_CLOSING = 3;
|
const READY_STATE_CLOSING = 3;
|
||||||
const READY_STATE_RECONNECTING = 4;
|
const READY_STATE_RECONNECTING = 4;
|
||||||
|
|
||||||
module.exports = class WebsocketClient extends EventEmitter {
|
export interface WebsocketClientOptions {
|
||||||
constructor(options, logger) {
|
key?: string;
|
||||||
|
secret?: string;
|
||||||
|
livenet?: boolean;
|
||||||
|
|
||||||
|
pongTimeout?: number;
|
||||||
|
pingInterval?: number;
|
||||||
|
reconnectTimeout?: number;
|
||||||
|
restOptions?: any;
|
||||||
|
requestOptions?: any;
|
||||||
|
wsUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Logger = typeof DefaultLogger;
|
||||||
|
|
||||||
|
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 options: WebsocketClientOptions;
|
||||||
|
|
||||||
|
constructor(options: WebsocketClientOptions, logger?: Logger) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.logger = logger || defaultLogger;
|
this.logger = logger || DefaultLogger;
|
||||||
|
|
||||||
this.readyState = READY_STATE_INITIAL;
|
this.readyState = READY_STATE_INITIAL;
|
||||||
this.pingInterval = null;
|
this.pingInterval = undefined;
|
||||||
this.pongTimeout = null;
|
this.pongTimeout = undefined;
|
||||||
|
|
||||||
this.options = {
|
this.options = {
|
||||||
livenet: false,
|
livenet: false,
|
||||||
@@ -34,7 +59,7 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
this.client = new RestClient(null, null, this.options.livenet, this.options.restOptions, this.options.requestOptions);
|
this.client = new RestClient(undefined, undefined, this.options.livenet, this.options.restOptions, this.options.requestOptions);
|
||||||
this._subscriptions = new Set();
|
this._subscriptions = new Set();
|
||||||
|
|
||||||
this._connect();
|
this._connect();
|
||||||
@@ -78,14 +103,17 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
const authParams = await this._authenticate();
|
const authParams = await this._authenticate();
|
||||||
const url = this._getWsUrl() + authParams;
|
const url = this._getWsUrl() + authParams;
|
||||||
|
|
||||||
this.ws = new WebSocket(url);
|
const ws = new WebSocket(url);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
this.ws.on('open', this._wsOpenHandler.bind(this));
|
|
||||||
this.ws.on('message', this._wsMessageHandler.bind(this));
|
|
||||||
this.ws.on('error', this._wsOnErrorHandler.bind(this));
|
|
||||||
this.ws.on('close', this._wsCloseHandler.bind(this));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error('Connection failed', err);
|
this.logger.error('Connection failed: ', err);
|
||||||
this._reconnect(this.options.reconnectTimeout);
|
this._reconnect(this.options.reconnectTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +124,7 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
|
|
||||||
const timeOffset = await this.client.getTimeOffset();
|
const timeOffset = await this.client.getTimeOffset();
|
||||||
|
|
||||||
const params = {
|
const params: any = {
|
||||||
api_key: this.options.key,
|
api_key: this.options.key,
|
||||||
expires: (Date.now() + timeOffset + 5000)
|
expires: (Date.now() + timeOffset + 5000)
|
||||||
};
|
};
|
||||||
@@ -105,9 +133,9 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
return '?' + serializeParams(params);
|
return '?' + serializeParams(params);
|
||||||
|
|
||||||
} else if (this.options.key || this.options.secret) {
|
} 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'});
|
this.logger.warning('Could not authenticate websocket, either api key or private key missing.', { category: 'bybit-ws' });
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug('Starting public only websocket client.', {category: 'bybit-ws'});
|
this.logger.debug('Starting public only websocket client.', { category: 'bybit-ws' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
@@ -120,23 +148,25 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.logger.info('Reconnecting to server', {category: 'bybit-ws'});
|
this.logger.info('Reconnecting to server', { category: 'bybit-ws' });
|
||||||
|
|
||||||
this._connect();
|
this._connect();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ping() {
|
_ping() {
|
||||||
clearTimeout(this.pongTimeout);
|
clearTimeout(this.pongTimeout!);
|
||||||
this.pongTimeout = null;
|
delete this.pongTimeout;
|
||||||
|
|
||||||
this.logger.silly('Sending ping', {category: 'bybit-ws'});
|
this.logger.silly('Sending ping', { category: 'bybit-ws' });
|
||||||
this.ws.send(JSON.stringify({op: 'ping'}));
|
this.ws.send(JSON.stringify({op: 'ping'}));
|
||||||
|
|
||||||
this.pongTimeout = setTimeout(() => {
|
this.pongTimeout = setTimeout(() => {
|
||||||
this.logger.info('Pong timeout', {category: 'bybit-ws'});
|
this.logger.info('Pong timeout', { category: 'bybit-ws' });
|
||||||
this._teardown();
|
this._teardown();
|
||||||
this.ws.terminate();
|
// this.ws.terminate();
|
||||||
|
// TODO: does this work?
|
||||||
|
this.ws.close();
|
||||||
}, this.options.pongTimeout);
|
}, this.options.pongTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,16 +174,16 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
if (this.pingInterval) clearInterval(this.pingInterval);
|
if (this.pingInterval) clearInterval(this.pingInterval);
|
||||||
if (this.pongTimeout) clearTimeout(this.pongTimeout);
|
if (this.pongTimeout) clearTimeout(this.pongTimeout);
|
||||||
|
|
||||||
this.pongTimeout = null;
|
this.pongTimeout = undefined;
|
||||||
this.pingInterval = null;
|
this.pingInterval = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
_wsOpenHandler() {
|
_wsOpenHandler() {
|
||||||
if (this.readyState === READY_STATE_CONNECTING) {
|
if (this.readyState === READY_STATE_CONNECTING) {
|
||||||
this.logger.info('Websocket connected', {category: 'bybit-ws', livenet: this.options.livenet});
|
this.logger.info('Websocket connected', { category: 'bybit-ws', livenet: this.options.livenet });
|
||||||
this.emit('open');
|
this.emit('open');
|
||||||
} else if (this.readyState === READY_STATE_RECONNECTING) {
|
} else if (this.readyState === READY_STATE_RECONNECTING) {
|
||||||
this.logger.info('Websocket reconnected', {category: 'bybit-ws', livenet: this.options.livenet});
|
this.logger.info('Websocket reconnected', { category: 'bybit-ws', livenet: this.options.livenet });
|
||||||
this.emit('reconnected');
|
this.emit('reconnected');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +194,7 @@ module.exports = class WebsocketClient extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_wsMessageHandler(message) {
|
_wsMessageHandler(message) {
|
||||||
const msg = JSON.parse(message);
|
const msg = JSON.parse(message && message.data || message);
|
||||||
|
|
||||||
if ('success' in msg) {
|
if ('success' in msg) {
|
||||||
this._handleResponse(msg);
|
this._handleResponse(msg);
|
||||||
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": true,
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"target": "es6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": false,
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["es2017","dom"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "lib",
|
||||||
|
"paths": {
|
||||||
|
"@src/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"**/node_modules/*",
|
||||||
|
"coverage",
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
}
|
||||||
69
webpack/webpack.config.js
Normal file
69
webpack/webpack.config.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const path = require('path');
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
|
||||||
|
function generateConfig(name) {
|
||||||
|
var config = {
|
||||||
|
entry: './lib/index.js',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, '../dist'),
|
||||||
|
filename: name + '.js',
|
||||||
|
sourceMapFilename: name + '.map',
|
||||||
|
library: 'bybitapi',
|
||||||
|
libraryTarget: 'umd'
|
||||||
|
},
|
||||||
|
devtool: "source-map",
|
||||||
|
mode: 'production',
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
// Add '.ts' and '.tsx' as resolvable extensions.
|
||||||
|
extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"],
|
||||||
|
fallback: {
|
||||||
|
"crypto": require.resolve("crypto-browserify"),
|
||||||
|
"buffer": require.resolve("buffer/"),
|
||||||
|
"stream": require.resolve("stream-browserify")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
// All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
|
||||||
|
{ test: /\.tsx?$/, loader: "ts-loader" },
|
||||||
|
|
||||||
|
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
|
||||||
|
{ test: /\.js$/, loader: "source-map-loader" },
|
||||||
|
|
||||||
|
{
|
||||||
|
test: /\.m?js$/,
|
||||||
|
exclude: /(node_modules|bower_components|samples|lib|test|coverage)/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: [['@babel/preset-env', {
|
||||||
|
'targets': {
|
||||||
|
'node': 'current'
|
||||||
|
}
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config.plugins = [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
|
||||||
|
}),
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
defaultSizes: 'stat',
|
||||||
|
analyzerMode: 'static',
|
||||||
|
reportFilename: '../doc/bundleReport.html',
|
||||||
|
openAnalyzer: false,
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = generateConfig('bybitapi');
|
||||||
Reference in New Issue
Block a user