├── .gitignore ├── LICENSE ├── README.md ├── benchmark.js ├── bitmexError.js ├── index.js ├── package.json └── sample.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | sample-priv.js 40 | up.sh 41 | t -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Mike van Rossum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitmex-simple-rest 2 | 3 | npm install bitmex-simple-rest 4 | 5 | API wrapper for the [Bitmex REST API](https://www.bitmex.com/api/explorer/). Please refer to [their documentation](https://www.bitmex.com/api/explorer/) for all calls explained. Check out `sample.js` for some example calls. 6 | 7 | This is a low level wrapper with zero dependencies focussed on: 8 | 9 | - Speed 10 | - Uses keep-alive 11 | - Disables Nagle's algorithm 12 | - No complex code 13 | - No third party libraries 14 | - allows you to pre compile your message (see below under low latency usage) 15 | - Userland control 16 | - Passes on response headers such as information on your [rate limit quota](https://www.bitmex.com/app/restAPI#Request-Rate-Limits) 17 | - Allows you to specity timeout & expiration date per call 18 | 19 | Used by my low latency market maker that's running in production. I don't think you can go much faster in nodejs without rewriting [Node.js' core http library](https://nodejs.org/api/http.html#http_http_request_options_callback) (if you think you can, feel free to open an issue or propose a PR). 20 | 21 | ## Usage 22 | 23 | // your api key & secret 24 | const key = 'x'; 25 | const secret = 'y'; 26 | 27 | const BitmexRest = require('bitmex-simple-rest'); 28 | const bm = new BitmexRest({ 29 | key, 30 | secret, 31 | 32 | // these are optional 33 | timeout: 90 * 1000, // ms - when this lib should timeout the call 34 | expiration: 60 * 1000, // ms - after how many ms bitmex should refuse this call 35 | userAgent: 'bearwhale' // string - custom ua 36 | }); 37 | 38 | const { data, headers } = await bm.request({ 39 | path: '/user/margin', 40 | method: 'GET', 41 | data: { currency: 'XBt' } 42 | }); 43 | 44 | ### Low latency usage 45 | 46 | Sending an API request to Bitmex requires hashing the payload with your API key. **In nodejs, this process can easily take 0.15 millisecond** (on the non compute optimized AWS boxes I tested this on - because yes, you should run on AWS-EU-1 if you want to trade fast on Bitmex). You can test the speed of creating API requests yourself on your system by running `benchmark.js`, preferably with real keys and and a request similar to what your system might send. 47 | 48 | This library allows you to prepare an API request draft before hand (doing all the heavy work). The microsecond you realize you actually want to send it you simply send the draft you created previously: 49 | 50 | // create the draft before hand 51 | const draft = bm.createDraft({ 52 | path: '/user/margin', 53 | method: 'GET', 54 | data: { currency: 'XBt' } 55 | }); 56 | 57 | // later when you actually want to send 58 | const { data, headers } = await bm.requestDraft(draft); 59 | 60 | Note that this only works in scenarios where you can estimate what will happen or which scenarios might happen: You can create drafts for all of them and only end up sending one later. 61 | 62 | ## TODO 63 | 64 | - Figure out if we can reliably skip the `end` event of the packetstream (see requestDraft comment). 65 | - String compare for common errors (overload), skipping `JSON.parse`. 66 | 67 | ## Final 68 | 69 | If this library is helping you trade better on Bitmex feel free to use [my ref link](https://www.bitmex.com/register/VDPANj). You'll get a 10% fee discount for the first 6 months, lowering your market fees (on the perpetual swap) from 0.075% to a mere 0.0675%! -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | const Bitmex = require('./'); 2 | const now = require('performance-now'); 3 | 4 | // fake keys with real key lengths 5 | const key = 'xxxx_xxxxxxxxxxxxxxxxxxx'; 6 | const secret = 'xxxx_xxxxxxxxxxxxxxxxxx_xxxx_xxxxxxxxxxxxxxxxxxx'; 7 | 8 | var bm = new Bitmex({ 9 | key, 10 | secret 11 | }); 12 | 13 | 14 | const test = () => { 15 | const start = now(); 16 | const d = bm.createDraft({ 17 | method: 'POST', 18 | path: '/order', 19 | data: { 20 | symbol: 'XBTUSD', 21 | orderQty: 100 + i, 22 | side: 'Sell', 23 | ordType: 'Limit', 24 | price: 10000 - i, 25 | execInst: 'ParticipateDoNotInitiate', 26 | clOrdID: 'moon-' + i 27 | } 28 | }); 29 | console.log('drafting took', (now() - start).toFixed(5), 'ms'); 30 | } 31 | 32 | let i = 0; 33 | const limit = 30; 34 | const loop = setInterval(() => { 35 | if(i++ > limit) { 36 | return clearInterval(loop); 37 | } 38 | 39 | setTimeout(test, 100); 40 | }, 200); -------------------------------------------------------------------------------- /bitmexError.js: -------------------------------------------------------------------------------- 1 | class BitmexError extends Error { 2 | constructor(message, { statusCode, headers }, data) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | this.headers = headers; 6 | Error.captureStackTrace(this, BitmexError); 7 | } 8 | } 9 | 10 | 11 | module.exports = BitmexError; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const https = require('https'); 3 | const querystring = require('querystring'); 4 | 5 | const version = require('./package.json').version; 6 | const name = require('./package.json').name; 7 | 8 | const BitmexError = require('./bitmexError'); 9 | 10 | const USER_AGENT = `${name}@${version}`; 11 | 12 | class BitmexRest { 13 | constructor(config) { 14 | this.ua = USER_AGENT; 15 | this.timeout = 90 * 1000; 16 | this.expiration = 60 * 1000; 17 | 18 | // keep-alive 19 | this.agent = new https.Agent({ 20 | keepAlive: true, 21 | timeout: 90 * 1000, 22 | keepAliveMsecs: 1000 * 60 23 | }); 24 | 25 | if(!config) { 26 | return; 27 | } 28 | 29 | if(config.key && config.secret) { 30 | this.key = config.key; 31 | this.secret = config.secret; 32 | } 33 | 34 | if(config.timeout) { 35 | this.timeout = config.timeout; 36 | } 37 | 38 | if(config.expiration) { 39 | this.expiration = config.expiration; 40 | } 41 | 42 | if(config.userAgent) { 43 | this.ua += ' | ' + config.userAgent; 44 | } 45 | } 46 | 47 | // most code is from: 48 | // https://github.com/BitMEX/api-connectors/blob/81874dc618f953fd054f2a249f5d03fda3e48093/official-http/node-request/ 49 | // includes this fix: https://github.com/BitMEX/api-connectors/pull/308 50 | 51 | // this fn can easily take more than 0.15ms due to heavy crypto functions 52 | // if your application is _very_ latency sensitive prepare the drafts 53 | // before you realize you want to send them. 54 | createDraft({path, method, data, expiration, timeout}) { 55 | if(!expiration) { 56 | expiration = this.expiration; 57 | } 58 | 59 | if(!timeout) { 60 | timeout = this.timeout; 61 | } 62 | 63 | path = '/api/v1' + path; 64 | 65 | let payload = ''; 66 | // NOTE: bitmex allows sending payload as query string for 67 | // DELETE calls. This is not documented 68 | if(method === 'GET' || method === 'DELETE') { 69 | path += '?' + querystring.stringify(data); 70 | } else if(data) { 71 | payload = JSON.stringify(data); 72 | } 73 | 74 | const start = Date.now(); 75 | const expires = Math.round((start + expiration) / 1000); 76 | 77 | const signature = crypto.createHmac('sha256', this.secret) 78 | .update(method + path + expires + payload).digest('hex'); 79 | 80 | const options = { 81 | host: 'www.bitmex.com', 82 | path, 83 | method, 84 | agent: this.agent, 85 | headers: { 86 | 'User-Agent': this.ua, 87 | 'content-type' : 'application/json', 88 | 'Accept': 'application/json', 89 | 'X-Requested-With': 'XMLHttpRequest', 90 | 'api-expires': expires, 91 | 'api-key': this.key, 92 | 'api-signature': signature 93 | }, 94 | // merely passed through for requestDraft 95 | timeout, 96 | payload 97 | }; 98 | 99 | return options; 100 | } 101 | 102 | // a draft is an option object created (potentially previously) with createDraft 103 | requestDraft(draft) { 104 | return new Promise((resolve, reject) => { 105 | const req = https.request(draft, res => { 106 | res.setEncoding('utf8'); 107 | let buffer = ''; 108 | res.on('data', function(data) { 109 | // TODO: we receive this event up to ~0.6ms before the end 110 | // event, though if this is valid json & doesn't contain 111 | // an error we can return from here, since we dont care 112 | // about status code. 113 | buffer += data; 114 | }); 115 | res.on('end', function() { 116 | if (res.statusCode >= 300) { 117 | let message; 118 | let data; 119 | 120 | try { 121 | data = JSON.parse(buffer); 122 | message = data.error.message; 123 | } catch(e) { 124 | message = buffer; 125 | } 126 | 127 | return reject(new BitmexError(message, res, data)); 128 | } 129 | 130 | let data; 131 | try { 132 | data = JSON.parse(buffer); 133 | } catch (err) { 134 | return reject(new BitmexError(buffer, res)); 135 | } 136 | 137 | resolve({ 138 | data, 139 | headers: res.headers 140 | }); 141 | }); 142 | }); 143 | 144 | req.on('error', err => { 145 | reject(err); 146 | }); 147 | 148 | req.on('socket', socket => { 149 | if(socket.connecting) { 150 | socket.setNoDelay(true); 151 | socket.setTimeout(draft.timeout); 152 | socket.on('timeout', function() { 153 | req.abort(); 154 | }); 155 | } 156 | }); 157 | 158 | req.end(draft.payload); 159 | }); 160 | } 161 | 162 | // props: {path, method, data, expiration, timeout} 163 | request(props) { 164 | return this.requestDraft(this.createDraft(props)); 165 | } 166 | }; 167 | 168 | module.exports = BitmexRest; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitmex-simple-rest", 3 | "version": "0.0.11", 4 | "description": "simple rest API wrapper for bitmex", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/askmike/bitmex-simple-rest.git" 12 | }, 13 | "keywords": [ 14 | "bitmex", 15 | "api", 16 | "wrapper" 17 | ], 18 | "author": "Mike van Rossum ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/askmike/bitmex-simple-rest/issues" 22 | }, 23 | "homepage": "https://github.com/askmike/bitmex-simple-rest#readme" 24 | } -------------------------------------------------------------------------------- /sample.js: -------------------------------------------------------------------------------- 1 | var Bitmex = require('./'); 2 | 3 | const key = 'x'; 4 | const secret = 'y'; 5 | 6 | var bm = new Bitmex({ 7 | key, 8 | secret, 9 | 10 | timeout: 90 * 1000, 11 | expiration: 60 * 1000, 12 | userAgent: 'bearwhale' 13 | }); 14 | 15 | bm.request({ 16 | path: '/user/margin', 17 | method: 'GET', 18 | data: { currency: 'XBt' } 19 | }) 20 | .then(console.log) 21 | .catch(console.error); --------------------------------------------------------------------------------