Initial commit
This commit is contained in:
8
lib/logger.js
Normal file
8
lib/logger.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
module.exports = {
|
||||
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)},
|
||||
}
|
||||
92
lib/request.js
Normal file
92
lib/request.js
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const request = require('request');
|
||||
|
||||
const {signMessage} = require('./utility.js');
|
||||
|
||||
const baseUrls = {
|
||||
livenet: 'https://api.bybit.com',
|
||||
testnet: 'https://api-testnet.bybit.com'
|
||||
}
|
||||
|
||||
module.exports = class Request {
|
||||
|
||||
constructor(key, secret, livenet=false) {
|
||||
this.baseUrl = baseUrls[livenet === true ? 'livenet' : 'testnet'];
|
||||
|
||||
if(key) assert(secret, 'Secret is required for private enpoints');
|
||||
|
||||
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 _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');
|
||||
|
||||
params = this._signRequest(params);
|
||||
}
|
||||
|
||||
const options = {
|
||||
url: [this.baseUrl, endpoint].join('/'),
|
||||
method: method,
|
||||
json: true
|
||||
};
|
||||
|
||||
switch(method) {
|
||||
case 'GET':
|
||||
options.qs = params
|
||||
break;
|
||||
case 'POST':
|
||||
options.body = params
|
||||
break;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(options, function callback(error, response, body) {
|
||||
if(!error && response.statusCode == 200) {
|
||||
resolve(body);
|
||||
} else if(error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_signRequest(data) {
|
||||
const params = {
|
||||
...data,
|
||||
api_key: this.key,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
if(this.key && this.secret) {
|
||||
params.sign = signMessage(this._serializeParams(params), this.secret);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
_serializeParams(params) {
|
||||
return Object.keys(params)
|
||||
.sort()
|
||||
.map(key => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
}
|
||||
}
|
||||
124
lib/rest-client.js
Normal file
124
lib/rest-client.js
Normal file
@@ -0,0 +1,124 @@
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const Request = require('./request.js');
|
||||
|
||||
module.exports = class RestClient {
|
||||
|
||||
constructor(key, secret, livenet=false) {
|
||||
this.request = new Request(...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('/open-api/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.order_id, 'Parameter order_id is required');
|
||||
|
||||
return await this.request.post('/open-api/order/cancel', 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.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 getConditioanlOrder(params) {
|
||||
return await this.request.get('/open-api/stop-order/list', params);
|
||||
}
|
||||
|
||||
async cancelConditionalOrder(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 getUserLeverage() {
|
||||
return await this.request.get('/user/leverage');
|
||||
}
|
||||
|
||||
async changeUserLeverage(params) {
|
||||
assert(params, 'No params passed');
|
||||
assert(params.leverage, 'Parameter leverage is required');
|
||||
assert(params.symbol, 'Parameter symbol is required');
|
||||
|
||||
return await this.request.post('/user/leverage/save', params);
|
||||
}
|
||||
|
||||
async getPosition() {
|
||||
return await this.request.get('/position/list');
|
||||
}
|
||||
|
||||
async changePositionMargin(params) {
|
||||
assert(params, 'No params passed');
|
||||
assert(params.margin, 'Parameter margin is required');
|
||||
assert(params.symbol, 'Parameter symbol is required');
|
||||
|
||||
return await this.request.post('/position/change-position-margin', params);
|
||||
}
|
||||
|
||||
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 getOrderTradeRecords(params) {
|
||||
assert(params, 'No params passed');
|
||||
assert(params.order_id, 'Parameter order_id 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 getLatestInformation() {
|
||||
return await this.request.get('/v2/public/tickers');
|
||||
}
|
||||
}
|
||||
9
lib/utility.js
Normal file
9
lib/utility.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const {createHmac} = require('crypto');
|
||||
|
||||
module.exports = {
|
||||
signMessage(message, secret) {
|
||||
return createHmac('sha256', secret)
|
||||
.update(message)
|
||||
.digest('hex');
|
||||
}
|
||||
}
|
||||
177
lib/websocket-client.js
Normal file
177
lib/websocket-client.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const {EventEmitter} = require('events');
|
||||
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const defaultLogger = require('./logger.js');
|
||||
const {signMessage} = require('./utility.js');
|
||||
|
||||
const wsUrls = {
|
||||
livenet: 'wss://stream.bybit.com/realtime',
|
||||
testnet: 'wss://stream-testnet.bybit.com/realtime'
|
||||
};
|
||||
|
||||
const READY_STATE_INITIAL = 0;
|
||||
const READY_STATE_CONNECTING = 1;
|
||||
const READY_STATE_CONNECTED = 2;
|
||||
const READY_STATE_CLOSING = 3;
|
||||
|
||||
module.exports = class WebsocketClient extends EventEmitter {
|
||||
constructor(options, logger) {
|
||||
super();
|
||||
|
||||
this.logger = logger || defaultLogger;
|
||||
|
||||
this.readyState = READY_STATE_INITIAL;
|
||||
this.pingInterval = null;
|
||||
this.pongTimeout = null;
|
||||
|
||||
this.options = {
|
||||
livenet: false,
|
||||
pongTimeout: 1000,
|
||||
pingInterval: 10000,
|
||||
reconnectTimeout: 500,
|
||||
...options
|
||||
}
|
||||
|
||||
this._connect();
|
||||
}
|
||||
|
||||
subscribe(topics) {
|
||||
if(!Array.isArray(topics)) topics = [topics];
|
||||
|
||||
const msgStr = JSON.stringify({
|
||||
op: 'subscribe',
|
||||
'args': topics
|
||||
});
|
||||
|
||||
this.ws.send(msgStr);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.logger.info('Closing connection', {category: 'bybit-ws'});
|
||||
this.readyState = READY_STATE_CLOSING;
|
||||
this._teardown();
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
_connect() {
|
||||
if(this.readyState === READY_STATE_INITIAL) this.readyState = READY_STATE_CONNECTING;
|
||||
|
||||
const url = wsUrls[this.options.livenet ? 'livenet' : 'testnet'] + this._authenticate();
|
||||
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
_authenticate() {
|
||||
if(this.options.key && this.options.secret) {
|
||||
this.logger.debug('Starting authenticated websocket client.', {category: 'bybit-ws'});
|
||||
const params = {
|
||||
api_key: this.options.key,
|
||||
expires: (Date.now() + 10000)
|
||||
};
|
||||
|
||||
params.signature = signMessage('GET/realtime' + params.expires, this.options.secret);
|
||||
|
||||
return '?' + Object.keys(params)
|
||||
.sort()
|
||||
.map(key => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
} 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 {
|
||||
this.logger.debug('Starting public only websocket client.', {category: 'bybit-ws'});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
_reconnect(timeout) {
|
||||
this._teardown();
|
||||
|
||||
setTimeout(() => {
|
||||
this.logger.info('Reconnecting to server', {category: 'bybit-ws'});
|
||||
|
||||
this._connect();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
_ping() {
|
||||
clearTimeout(this.pongTimeout);
|
||||
this.pongTimeout = null;
|
||||
|
||||
this.logger.debug('Sending ping', {category: 'bybit-ws'});
|
||||
this.ws.send(JSON.stringify({op: 'ping'}));
|
||||
|
||||
this.pongTimeout = setTimeout(() => {
|
||||
this.logger.info('Pong timeout', {category: 'bybit-ws'});
|
||||
this._teardown();
|
||||
this.ws.terminate();
|
||||
}, this.options.pongTimeout);
|
||||
}
|
||||
|
||||
_teardown() {
|
||||
if(this.pingInterval) clearInterval(this.pingInterval);
|
||||
if(this.pongTimeout) clearTimeout(this.pongTimeout);
|
||||
|
||||
this.pongTimeout = null;
|
||||
this.pingInterval = null;
|
||||
}
|
||||
|
||||
_wsOpenHandler() {
|
||||
if(this.readyState === READY_STATE_CONNECTING) {
|
||||
this.logger.info('Websocket connected', {category: 'bybit-ws', livenet: this.options.livenet});
|
||||
this.emit('open');
|
||||
}
|
||||
|
||||
this.readyState = READY_STATE_CONNECTED;
|
||||
this.pingInterval = setInterval(this._ping.bind(this), this.options.pingInterval);
|
||||
}
|
||||
|
||||
_wsMessageHandler(message) {
|
||||
let msg = JSON.parse(message);
|
||||
|
||||
if('success' in msg) {
|
||||
this._handleResponse(msg);
|
||||
} else if(msg.topic) {
|
||||
this._handleUpdate(msg);
|
||||
} else {
|
||||
this.logger.warning('Got unhandled ws message', msg);
|
||||
}
|
||||
}
|
||||
|
||||
_wsOnErrorHandler(err) {
|
||||
this.logger.error('Websocket error', {category: 'bybit-ws', err});
|
||||
if(this.readyState === READY_STATE_CONNECTED) this.emit('error', err);
|
||||
}
|
||||
|
||||
_wsCloseHandler() {
|
||||
this.logger.info('Websocket connection closed', {category: 'bybit-ws'});
|
||||
|
||||
if(this.readyState !== READY_STATE_CLOSING) {
|
||||
this._reconnect(this.options.reconnectTimeout);
|
||||
} else {
|
||||
this.readyState = READY_STATE_INITIAL;
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
_handleResponse(response) {
|
||||
if(response.request && response.request.op === 'ping' && response.ret_msg === 'pong') {
|
||||
if(response.success === true) {
|
||||
this.logger.debug('pong recieved', {category: 'bybit-ws'});
|
||||
clearTimeout(this.pongTimeout);
|
||||
}
|
||||
} else {
|
||||
this.emit('response', response);
|
||||
}
|
||||
}
|
||||
|
||||
_handleUpdate(message) {
|
||||
this.emit('update', message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user