├── .gitignore ├── LICENSE ├── README.md ├── benchmark.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 | # ftx-api-rest 2 | 3 | npm install ftx-api-rest 4 | 5 | API wrapper for the [FTX REST API](https://docs.ftx.com/#rest-api). Please refer to [their documentation](https://docs.ftx.com/#rest-api) for all calls explained. Check out `sample.js` for lib usage. 6 | 7 | This is a low level wrapper with zero dependencies focussed on speed: 8 | 9 | - Disables Nagle's algorithm 10 | - No complex code 11 | - No third party libraries 12 | - Allows you to pre compile your message (see below under low latency usage)\ 13 | - Supports keep-alive 14 | 15 | This is a fork based of my bitmex library: https://github.com/askmike/bitmex-simple-rest 16 | 17 | Used by my low(ish) latency arb system 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). 18 | 19 | ## Usage 20 | 21 | See sample.js. 22 | 23 | ### Low latency usage 24 | 25 | Sending an API request to FTX 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 in AWS ap-northeast-1 if you want to trade fast on FTX). 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. 26 | 27 | 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: 28 | 29 | // create the draft before hand 30 | const draft = ftx.createDraft({ 31 | method: 'GET', 32 | path: '/account' 33 | }); 34 | 35 | // later when you actually want to send 36 | const data = await ftx.requestDraft(draft); 37 | 38 | 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. 39 | 40 | ## TODO 41 | 42 | - Figure out if we can reliably skip the `end` event of the packetstream (see requestDraft comment). 43 | - String compare for common errors (overload), skipping `JSON.parse`. 44 | 45 | ## Final 46 | 47 | If this library is helping you trade better on FTX feel free to use [my ref link](https://ftx.com/#a=1275753). -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | const FTX = require('./'); 2 | const now = require('performance-now'); 3 | 4 | // fake keys with real key lengths 5 | const key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; 6 | const secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; 7 | const subaccunt = 'arthurhayes'; 8 | 9 | var ftx = new FTX({ 10 | key, 11 | secret, 12 | subaccount 13 | }); 14 | 15 | 16 | const test = () => { 17 | const start = now(); 18 | const d = bm.createDraft({ 19 | method: 'POST', 20 | path: '/orders', 21 | data: { 22 | market: 'BTC-PERP', 23 | size: 100 + i, 24 | side: 'sell', 25 | order_type: 'Limit', 26 | price: 10000 - 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); -------------------------------------------------------------------------------- /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 USER_AGENT = `${name}@${version}`; 9 | const DEFAULT_ENDPOINT = 'ftx.com'; 10 | const DEFAULT_HEADER_PREFIX = 'FTX'; 11 | 12 | class FTXRest { 13 | constructor(config) { 14 | this.ua = USER_AGENT; 15 | this.timeout = 90 * 1000; 16 | 17 | this.agent = new https.Agent({ 18 | keepAlive: true, 19 | timeout: 90 * 1000, 20 | keepAliveMsecs: 1000 * 60 21 | }); 22 | 23 | if(!config) { 24 | return; 25 | } 26 | 27 | if(config.key && config.secret) { 28 | this.key = config.key; 29 | this.secret = config.secret; 30 | } 31 | 32 | if(config.timeout) { 33 | this.timeout = config.timeout; 34 | } 35 | 36 | if(config.subaccount) { 37 | this.subaccount = config.subaccount; 38 | } 39 | 40 | if(config.userAgent) { 41 | this.ua += ' | ' + config.userAgent; 42 | } 43 | 44 | this.endpoint = config.endpoint || DEFAULT_ENDPOINT; 45 | this.headerPrefix = config.headerPrefix || DEFAULT_HEADER_PREFIX; 46 | } 47 | 48 | // this fn can easily take more than 0.15ms due to heavy crypto functions 49 | // if your application is _very_ latency sensitive prepare the drafts 50 | // before you realize you want to send them. 51 | createDraft({path, method, data, timeout, subaccount}) { 52 | if(!timeout) { 53 | timeout = this.timeout; 54 | } 55 | 56 | path = '/api' + path; 57 | 58 | let payload = ''; 59 | if(method === 'GET' && data) { 60 | path += '?' + querystring.stringify(data); 61 | } else if(method === 'DELETE' && typeof data === 'number') { 62 | // cancel single order 63 | path += data; 64 | } else if(data) { 65 | payload = JSON.stringify(data); 66 | } 67 | 68 | const start = +new Date; 69 | 70 | const signature = crypto.createHmac('sha256', this.secret) 71 | .update(start + method + path + payload).digest('hex'); 72 | 73 | const options = { 74 | host: this.endpoint, 75 | path: path, 76 | method, 77 | agent: this.agent, 78 | headers: { 79 | 'User-Agent': this.ua, 80 | 'content-type' : 'application/json', 81 | 'Accept': 'application/json', 82 | 'X-Requested-With': 'XMLHttpRequest', 83 | [this.headerPrefix + '-TS']: start, 84 | [this.headerPrefix + '-KEY']: this.key, 85 | [this.headerPrefix + '-SIGN']: signature 86 | }, 87 | // merely passed through for requestDraft 88 | timeout, 89 | payload 90 | }; 91 | 92 | // required for delete all 93 | if(method === 'DELETE' && payload !== '') { 94 | options.headers['Content-Length'] = payload.length; 95 | } 96 | 97 | if(this.subaccount || subaccount) { 98 | options.headers[this.headerPrefix + '-SUBACCOUNT'] = this.subaccount || subaccount 99 | } 100 | 101 | return options; 102 | } 103 | 104 | // a draft is an option object created (potentially previously) with createDraft 105 | requestDraft(draft) { 106 | return new Promise((resolve, reject) => { 107 | const req = https.request(draft, res => { 108 | res.setEncoding('utf8'); 109 | let buffer = ''; 110 | res.on('data', function(data) { 111 | // TODO: we receive this event up to ~0.6ms before the end 112 | // event, though if this is valid json & doesn't contain 113 | // an error we can return from here, since we dont care 114 | // about status code. 115 | buffer += data; 116 | }); 117 | res.on('end', function() { 118 | if (res.statusCode >= 300) { 119 | let message; 120 | let data; 121 | 122 | try { 123 | data = JSON.parse(buffer); 124 | message = data 125 | } catch(e) { 126 | message = buffer; 127 | } 128 | 129 | console.error('ERROR!', res.statusCode, message); 130 | const error = new Error(message.error) 131 | error.statusCode = res.statusCode; 132 | return reject(error); 133 | } 134 | 135 | let data; 136 | try { 137 | data = JSON.parse(buffer); 138 | } catch (err) { 139 | console.error('JSON ERROR!', buffer); 140 | return reject(new Error('Json error')); 141 | } 142 | 143 | resolve(data); 144 | }); 145 | }); 146 | 147 | req.on('error', err => { 148 | reject(err); 149 | }); 150 | 151 | req.on('socket', socket => { 152 | if(socket.connecting) { 153 | socket.setNoDelay(true); 154 | socket.setTimeout(draft.timeout); 155 | socket.on('timeout', function() { 156 | req.abort(); 157 | }); 158 | } 159 | }); 160 | 161 | req.end(draft.payload); 162 | }); 163 | } 164 | 165 | // props: {path, method, data, timeout} 166 | request(props) { 167 | return this.requestDraft(this.createDraft(props)); 168 | } 169 | }; 170 | 171 | module.exports = FTXRest; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ftx-api-rest", 3 | "version": "0.0.7", 4 | "description": "simple rest API wrapper for FTX", 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/ftx-api-rest.git" 12 | }, 13 | "keywords": [ 14 | "ftx", 15 | "api", 16 | "wrapper", 17 | "profit" 18 | ], 19 | "author": "Mike van Rossum ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/askmike/ftx-api-rest/issues" 23 | }, 24 | "homepage": "https://github.com/askmike/ftx-api-rest#readme" 25 | } -------------------------------------------------------------------------------- /sample.js: -------------------------------------------------------------------------------- 1 | const FTXRest = require('./'); 2 | 3 | const ftx = new FTXRest({ 4 | key: 'x', 5 | secret: 'y', 6 | subaccount: 'z' 7 | }) 8 | 9 | ftx.request({ 10 | method: 'GET', 11 | path: '/account' 12 | }).then(console.log); --------------------------------------------------------------------------------