├── .eslintrc.js ├── .gitignore ├── README.md ├── node-huobi-api.js ├── package.json └── test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "accessor-pairs": "error", 14 | "array-bracket-newline": "error", 15 | "array-bracket-spacing": [ 16 | "error", 17 | "never" 18 | ], 19 | "array-callback-return": "error", 20 | "array-element-newline": "off", 21 | "arrow-body-style": "error", 22 | "arrow-parens": [ 23 | "error", 24 | "as-needed" 25 | ], 26 | "arrow-spacing": [ 27 | "error", 28 | { 29 | "after": true, 30 | "before": true 31 | } 32 | ], 33 | "block-scoped-var": "error", 34 | "block-spacing": [ 35 | "error", 36 | "never" 37 | ], 38 | "brace-style": [ 39 | "error", 40 | "1tbs", 41 | { 42 | "allowSingleLine": true 43 | } 44 | ], 45 | "callback-return": "off", 46 | "camelcase": "off", 47 | "capitalized-comments": "off", 48 | "class-methods-use-this": "error", 49 | "comma-dangle": "error", 50 | "comma-spacing": "off", 51 | "comma-style": [ 52 | "error", 53 | "last" 54 | ], 55 | "complexity": "error", 56 | "computed-property-spacing": [ 57 | "error", 58 | "never" 59 | ], 60 | "consistent-return": "off", 61 | "consistent-this": "error", 62 | "curly": "off", 63 | "default-case": "error", 64 | "dot-location": "error", 65 | "dot-notation": "error", 66 | "eol-last": "error", 67 | "eqeqeq": [ 68 | "error", 69 | "always" 70 | ], 71 | "for-direction": "error", 72 | "func-call-spacing": "error", 73 | "func-name-matching": "off", 74 | "func-names": "off", 75 | "func-style": [ 76 | "error", 77 | "expression" 78 | ], 79 | "function-paren-newline": "error", 80 | "generator-star-spacing": "error", 81 | "getter-return": "error", 82 | "global-require": "off", 83 | "guard-for-in": "off", 84 | "handle-callback-err": "error", 85 | "id-blacklist": "error", 86 | "id-length": "off", 87 | "id-match": "error", 88 | "implicit-arrow-linebreak": [ 89 | "error", 90 | "beside" 91 | ], 92 | "indent": "off", 93 | "indent-legacy": "off", 94 | "init-declarations": "off", 95 | "jsx-quotes": "error", 96 | "key-spacing": "off", 97 | "keyword-spacing": [ 98 | "error", 99 | { 100 | "after": true, 101 | "before": true 102 | } 103 | ], 104 | "line-comment-position": "off", 105 | "linebreak-style": [ 106 | "error", 107 | "unix" 108 | ], 109 | "lines-around-comment": "off", 110 | "lines-around-directive": "off", 111 | "lines-between-class-members": "error", 112 | "max-depth": "error", 113 | "max-len": "off", 114 | "max-lines": "off", 115 | "max-nested-callbacks": "error", 116 | "max-params": "off", 117 | "max-statements": "off", 118 | "max-statements-per-line": "off", 119 | "multiline-comment-style": [ 120 | "off", 121 | "bare-block" 122 | ], 123 | "new-cap": "error", 124 | "new-parens": "error", 125 | "newline-after-var": [ 126 | "off", 127 | "never" 128 | ], 129 | "newline-before-return": "off", 130 | "newline-per-chained-call": "off", 131 | "no-alert": "error", 132 | "no-array-constructor": "error", 133 | "no-await-in-loop": "error", 134 | "no-bitwise": "off", 135 | "no-buffer-constructor": "error", 136 | "no-caller": "error", 137 | "no-catch-shadow": "off", 138 | "no-confusing-arrow": "error", 139 | "no-console": [ 140 | "warn", 141 | { 142 | allow: [ 143 | "log", 144 | "warn", 145 | "error" 146 | ] 147 | } 148 | ], 149 | "no-continue": "error", 150 | "no-div-regex": "error", 151 | "no-duplicate-imports": "error", 152 | "no-else-return": [ 153 | "error", 154 | { 155 | "allowElseIf": true 156 | } 157 | ], 158 | "no-empty-function": "error", 159 | "no-eq-null": "error", 160 | "no-eval": "error", 161 | "no-extend-native": "error", 162 | "no-extra-bind": "error", 163 | "no-extra-label": "error", 164 | "no-extra-parens": "off", 165 | "no-floating-decimal": "error", 166 | "no-implicit-coercion": "error", 167 | "no-implicit-globals": "error", 168 | "no-implied-eval": "error", 169 | "no-inline-comments": "off", 170 | "no-invalid-this": "off", 171 | "no-iterator": "error", 172 | "no-label-var": "error", 173 | "no-labels": "error", 174 | "no-lone-blocks": "error", 175 | "no-lonely-if": "error", 176 | "no-loop-func": "off", 177 | "no-magic-numbers": "off", 178 | "no-mixed-operators": [ 179 | "error", 180 | { 181 | "allowSamePrecedence": true 182 | } 183 | ], 184 | "no-mixed-requires": "error", 185 | "no-multi-assign": "error", 186 | "no-multi-spaces": "error", 187 | "no-multi-str": "error", 188 | "no-multiple-empty-lines": "error", 189 | "no-native-reassign": "error", 190 | "no-negated-condition": "off", 191 | "no-negated-in-lhs": "error", 192 | "no-nested-ternary": "error", 193 | "no-new": "error", 194 | "no-new-func": "error", 195 | "no-new-object": "error", 196 | "no-new-require": "error", 197 | "no-new-wrappers": "error", 198 | "no-octal-escape": "error", 199 | "no-param-reassign": "off", 200 | "no-path-concat": "error", 201 | "no-plusplus": "off", 202 | "no-process-env": "off", 203 | "no-process-exit": "error", 204 | "no-proto": "error", 205 | "no-prototype-builtins": "error", 206 | "no-restricted-globals": "error", 207 | "no-restricted-imports": "error", 208 | "no-restricted-modules": "error", 209 | "no-restricted-properties": "error", 210 | "no-restricted-syntax": "error", 211 | "no-return-assign": "error", 212 | "no-return-await": "error", 213 | "no-script-url": "error", 214 | "no-self-compare": "error", 215 | "no-sequences": "error", 216 | "no-shadow": "off", 217 | "no-shadow-restricted-names": "error", 218 | "no-spaced-func": "error", 219 | "no-sync": "off", 220 | "no-tabs": "off", 221 | "no-template-curly-in-string": "error", 222 | "no-ternary": "off", 223 | "no-throw-literal": "off", 224 | "no-trailing-spaces": "error", 225 | "no-undef-init": "error", 226 | "no-undefined": "error", 227 | "no-underscore-dangle": "error", 228 | "no-unmodified-loop-condition": "error", 229 | "no-unneeded-ternary": "error", 230 | "no-unused-expressions": "error", 231 | "no-use-before-define": "off", 232 | "no-useless-call": "error", 233 | "no-useless-computed-key": "error", 234 | "no-useless-concat": "error", 235 | "no-useless-constructor": "error", 236 | "no-useless-rename": "error", 237 | "no-useless-return": "error", 238 | "no-var": "error", 239 | "no-void": "error", 240 | "no-warning-comments": "off", 241 | "no-whitespace-before-property": "error", 242 | "no-with": "error", 243 | "nonblock-statement-body-position": "error", 244 | "object-curly-newline": "error", 245 | "object-curly-spacing": "off", 246 | "object-property-newline": [ 247 | "error", 248 | { 249 | "allowMultiplePropertiesPerLine": true 250 | } 251 | ], 252 | "object-shorthand": "off", 253 | "one-var": "off", 254 | "one-var-declaration-per-line": "off", 255 | "operator-assignment": [ 256 | "error", 257 | "always" 258 | ], 259 | "operator-linebreak": "error", 260 | "padded-blocks": "off", 261 | "padding-line-between-statements": "error", 262 | "prefer-arrow-callback": "off", 263 | "prefer-const": "off", 264 | "prefer-destructuring": "off", 265 | "prefer-numeric-literals": "error", 266 | "prefer-promise-reject-errors": "error", 267 | "prefer-reflect": "off", 268 | "prefer-rest-params": "error", 269 | "prefer-spread": "off", 270 | "prefer-template": "off", 271 | "quote-props": "off", 272 | "quotes": [ 273 | "error", 274 | "single" 275 | ], 276 | "radix": "error", 277 | "require-await": "error", 278 | "require-jsdoc": "error", 279 | "rest-spread-spacing": "error", 280 | "semi": "off", 281 | "semi-spacing": [ 282 | "error", 283 | { 284 | "after": false, 285 | "before": false 286 | } 287 | ], 288 | "semi-style": [ 289 | "error", 290 | "last" 291 | ], 292 | "sort-imports": "error", 293 | "sort-keys": "off", 294 | "sort-vars": "off", 295 | "space-before-blocks": "off", 296 | "space-before-function-paren": "off", 297 | "space-in-parens": "off", 298 | "space-infix-ops": "off", 299 | "space-unary-ops": "error", 300 | "spaced-comment": "off", 301 | "strict": "off", 302 | "switch-colon-spacing": "error", 303 | "symbol-description": "error", 304 | "template-curly-spacing": "error", 305 | "template-tag-spacing": "error", 306 | "unicode-bom": [ 307 | "error", 308 | "never" 309 | ], 310 | "valid-jsdoc": "error", 311 | "vars-on-top": "error", 312 | "wrap-iife": "off", 313 | "wrap-regex": "error", 314 | "yield-star-spacing": "error", 315 | "yoda": [ 316 | "error", 317 | "never" 318 | ] 319 | } 320 | }; 321 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDEs and editors 5 | /.idea 6 | .project 7 | .classpath 8 | *.launch 9 | .settings/ 10 | *.idea 11 | *target/ 12 | *.iml 13 | package-lock.json 14 | #System Files 15 | .DS_Store 16 | Thumbs.db 17 | 18 | vendor/* 19 | !vendor/vendor.json 20 | 21 | tmp/* 22 | gorm.db 23 | coverage.txt 24 | 25 | bak.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-huobi-api 2 | Node huobi API is an asynchronous node.js library for the huobi API designed to be easy to use. 3 | -------------------------------------------------------------------------------- /node-huobi-api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cover from Node Binance API 3 | * @module jaggedsoft/node-binance-api 4 | * @return {object} instance to class object 5 | */ 6 | 7 | let api = function Huobi(){ 8 | let Huobi = this; 9 | 10 | 'use strict'; 11 | const WebSocket = require('ws'); 12 | const request = require('request'); 13 | const crypto = require('crypto'); 14 | const file = require('fs'); 15 | const url = require('url'); 16 | const pako = require('pako'); 17 | const HttpsProxyAgent = require('https-proxy-agent'); 18 | const SocksProxyAgent = require('socks-proxy-agent'); 19 | //const stringHash = require('string-hash'); 20 | //const async = require('async'); 21 | const site = 'api.huobi.pro'; 22 | const base = 'https://'+site; 23 | const wapi = 'https://api.binance.com/wapi/'; 24 | const stream = 'wss://'+site+'/ws/'; 25 | //const combineStream = 'wss://api.huobi.pro/ws/'; 26 | const userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36';//'Mozilla/4.0 (compatible; Node Huobi API)'; 27 | const contentType = 'application/x-www-form-urlencoded'; 28 | 29 | Huobi.subscriptions = {}; 30 | Huobi.depthCache = {}; 31 | Huobi.depthCacheContext = {}; 32 | Huobi.ohlcLatest = {}; 33 | Huobi.klineQueue = {}; 34 | Huobi.ohlc = {}; 35 | const default_options = { 36 | recvWindow: 5000, 37 | useServerTime: false, 38 | reconnect: true, 39 | verbose: false, 40 | test: false, 41 | log: function (...args) { 42 | console.log(Array.prototype.slice.call(args)); 43 | } 44 | }; 45 | Huobi.options = default_options; 46 | Huobi.info = { timeOffset: 0 }; 47 | Huobi.socketHeartbeatInterval = null; 48 | /** 49 | * Replaces socks connection uri hostname with IP address 50 | * @param {string} connString - socks connection string 51 | * @return {string} modified string with ip address 52 | */ 53 | const proxyReplacewithIp = function (connString) { 54 | return connString; 55 | } 56 | /** 57 | * Returns an array in the form of [host, port] 58 | * @param {string} connString - connection string 59 | * @return {array} array of host and port 60 | */ 61 | const parseProxy = function (connString) { 62 | let arr = connString.split('/'); 63 | let host = arr[2].split(':')[0]; 64 | let port = arr[2].split(':')[1]; 65 | return [arr[0], host, port]; 66 | } 67 | /** 68 | * Checks to see of the object is iterable 69 | * @param {object} obj - The object check 70 | * @return {boolean} true or false is iterable 71 | */ 72 | const isIterable = function (obj) { 73 | // checks for null and undefined 74 | if (obj === null) { 75 | return false; 76 | } 77 | return typeof obj[Symbol.iterator] === 'function'; 78 | } 79 | const addProxy = opt => { 80 | let socksproxy = process.env.socks_proxy || false; 81 | if (socksproxy === false) return opt; 82 | socksproxy = proxyReplacewithIp(socksproxy); 83 | 84 | if (Huobi.options.verbose) Huobi.options.log('using socks proxy server ' + socksproxy); 85 | 86 | opt.agentClass = SocksProxyAgent; 87 | opt.agentOptions = { 88 | protocol: parseProxy(socksproxy)[0], 89 | host: parseProxy(socksproxy)[1], 90 | port: parseProxy(socksproxy)[2] 91 | } 92 | return opt; 93 | } 94 | const reqHandler = cb => (error, response, body) => { 95 | if (!cb) return; 96 | 97 | if (error) return cb(error, {}); 98 | 99 | if (response && response.statusCode !== 200) return cb(response, {}); 100 | 101 | return cb(null, JSON.parse(body)); 102 | } 103 | 104 | const proxyRequest = (opt, cb) => request(addProxy(opt), reqHandler(cb)); 105 | 106 | const reqObj = (url, data = {}, method = 'GET', key) => ({ 107 | url: url, 108 | qs: data, 109 | method: method, 110 | timeout: Huobi.options.recvWindow, 111 | headers: { 112 | 'User-Agent': userAgent, 113 | 'Content-type': contentType, 114 | 'X-MBX-APIKEY': key || '' 115 | } 116 | }) 117 | const reqObjPOST = (url, data = {}, method = 'POST', key) => ({ 118 | url: url, 119 | form: data, 120 | method: method, 121 | timeout: Huobi.options.recvWindow, 122 | headers: { 123 | 'User-Agent': userAgent, 124 | 'Content-type': contentType, 125 | 'X-MBX-APIKEY': key || '' 126 | } 127 | }) 128 | /** 129 | * Create a http request to the public API 130 | * @param {string} url - The http endpoint 131 | * @param {object} data - The data to send 132 | * @param {function} callback - The callback method to call 133 | * @param {string} method - the http method 134 | * @return {undefined} 135 | */ 136 | const publicRequest = function (url, data = {}, callback, method = 'GET') { 137 | let opt = reqObj(url, data, method); 138 | proxyRequest(opt, callback); 139 | }; 140 | 141 | /** 142 | * Create a http request to the public API 143 | * @param {string} url - The http endpoint 144 | * @param {object} data - The data to send 145 | * @param {function} callback - The callback method to call 146 | * @param {string} method - the http method 147 | * @return {undefined} 148 | */ 149 | const apiRequest = function (url, data = {}, callback, method = 'GET') { 150 | if (!Huobi.options.APIKEY) throw Error('apiRequest: Invalid API Key'); 151 | let opt = reqObj( 152 | url, 153 | data, 154 | method, 155 | Huobi.options.APIKEY 156 | ); 157 | proxyRequest(opt, callback); 158 | }; 159 | 160 | /** 161 | * Used to subscribe to a single websocket endpoint 162 | * @param {string} endpoint - endpoint to connect to 163 | * @param {function} callback - the function to called when information is received 164 | * @param {boolean} reconnect - whether to reconnect on disconnect 165 | * @param {object} opened_callback - the function to called when opened 166 | * @return {WebSocket} - websocket reference 167 | */ 168 | const subscribe = function (symbols, callback, reconnect = false, opened_callback = false) { 169 | 170 | let httpsproxy = process.env.https_proxy || false; 171 | let socksproxy = process.env.socks_proxy || false; 172 | let ws = false; 173 | 174 | if (socksproxy !== false) { 175 | socksproxy = proxyReplacewithIp(socksproxy); 176 | if (Huobi.options.verbose) Huobi.options.log('using socks proxy server ' + socksproxy); 177 | let agent = new SocksProxyAgent({ 178 | protocol: parseProxy(socksproxy)[0], 179 | host: parseProxy(socksproxy)[1], 180 | port: parseProxy(socksproxy)[2] 181 | }); 182 | //ws = new WebSocket(stream + endpoint, { agent: agent }); 183 | ws = new WebSocket(stream , { agent: agent }); 184 | } else if (httpsproxy !== false) { 185 | if (Huobi.options.verbose) Huobi.options.log('using proxy server ' + agent); 186 | let config = url.parse(httpsproxy); 187 | let agent = new HttpsProxyAgent(config); 188 | //ws = new WebSocket(stream + endpoint, { agent: agent }); 189 | ws = new WebSocket(stream , { agent: agent }); 190 | } else { 191 | //ws = new WebSocket(stream + endpoint); 192 | ws = new WebSocket(stream); 193 | } 194 | 195 | let doSymbolsToWebsocket = function () { 196 | if (Array.isArray(symbols)) { 197 | symbols.forEach(s => { 198 | let channel = {}; 199 | channel.sub = "market." + s + ".depth.step0"; 200 | channel.id = new Date().getTime()+s; 201 | Huobi.options.log(channel.sub); 202 | ws.send(JSON.stringify(channel)); 203 | }); 204 | }else{ 205 | let channel = {}; 206 | channel.sub = "market." + symbols + ".depth.step0"; 207 | Huobi.options.log(channel.sub); 208 | channel.id = new Date().getTime()+symbols; 209 | ws.send(JSON.stringify(channel)); 210 | } 211 | 212 | 213 | }; 214 | if (Huobi.options.verbose) Huobi.options.log('Subscribed to ' + stream); 215 | ws.reconnect = Huobi.options.reconnect; 216 | ws.endpoint = new Date().getTime()+"depth"; 217 | ws.isAlive = false; 218 | ws.on('open', handleSocketOpen.bind(ws, opened_callback ? doSymbolsToWebsocket : opened_callback)); 219 | ws.on('pong', handleSocketHeartbeat); 220 | ws.on('error', handleSocketError); 221 | ws.on('close', handleSocketClose.bind(ws, reconnect)); 222 | ws.on('message', function (data) { 223 | data = pako.inflate(data,{ to: 'string' }); 224 | //Huobi.options.log('ws data: ' + data); 225 | //try { 226 | let msg = JSON.parse(data); 227 | if (msg.ping) { 228 | ws.send(JSON.stringify({ pong: msg.ping })); 229 | // Huobi.options.log('ping: '+msg.ping ); 230 | } else if (msg.subbed) { 231 | //Huobi.options.log('subbed: '+msg.id +" status: "+msg.status ); 232 | //options.log('subbed: '+msg.id +" status: "+msg.status ); 233 | } else { 234 | if (msg.status && msg.status == 'error') { 235 | Huobi.options.log('error: '+ data ); 236 | return; 237 | //throw new Error(msg) 238 | } 239 | callback( JSON.parse(data) ); 240 | } 241 | // } catch (error) { 242 | // //options.log('CombinedStream: Parse error: '+error.message +'-> '+ JSON.stringify(data) ); 243 | // Huobi.options.log('Parse error: ' + error.message); 244 | // } 245 | // try { 246 | // callback(JSON.parse(data)); 247 | // } catch (error) { 248 | // Huobi.options.log('Parse error: ' + error.message); 249 | // } 250 | }); 251 | return ws; 252 | }; 253 | /** 254 | * Make market request 255 | * @param {string} url - The http endpoint 256 | * @param {object} data - The data to send 257 | * @param {function} callback - The callback method to call 258 | * @param {string} method - the http method 259 | * @return {undefined} 260 | */ 261 | const marketRequest = function (url, data = {}, callback, method = 'GET') { 262 | if (!Huobi.options.APIKEY) throw Error('apiRequest: Invalid API Key'); 263 | let query = Object.keys(data).reduce(function (a, k) { 264 | a.push(k + '=' + encodeURIComponent(data[k])); 265 | return a; 266 | }, []).join('&'); 267 | 268 | let opt = reqObj( 269 | url + (query ? '?' + query : ''), 270 | data, 271 | method, 272 | Huobi.options.APIKEY 273 | ); 274 | proxyRequest(opt, callback); 275 | }; 276 | /** 277 | * Create a signed http request to the signed API 278 | * @param {string} url - The http endpoint 279 | * @param {object} data - The data to send 280 | * @param {function} callback - The callback method to call 281 | * @param {string} method - the http method 282 | * @return {undefined} 283 | */ 284 | const signedRequest = function (url, data = {}, callback, method = 'GET') { 285 | if (!Huobi.options.APIKEY) throw Error('apiRequest: Invalid API Key'); 286 | if (!Huobi.options.APISECRET) throw Error('signedRequest: Invalid API Secret'); 287 | //if (typeof data.recvWindow === 'undefined') data.recvWindow = Huobi.options.recvWindow; 288 | data.Timestamp = new Date().toISOString().replace(/\..+/, '');//.getTime()+ Huobi.info.timeOffset; 289 | data.SignatureMethod='HmacSHA256'; 290 | data.SignatureVersion=2; 291 | data.AccessKeyId=Huobi.options.APIKEY; 292 | //console.log(data.Timestamp); 293 | let query = Object.keys(data) 294 | .sort( (a,b)=> (a > b) ? 1 : -1 ) 295 | .reduce(function (a, k) { 296 | a.push(k + '=' + encodeURIComponent(data[k])); 297 | return a; 298 | }, []).join('&'); 299 | //console.log("query %s",query); 300 | 301 | let source = method+'\n' + site+'\n'+url.replace(base,'')+'\n'+query; 302 | //console.log("source %s",source); 303 | let signature = crypto.createHmac('sha256', Huobi.options.APISECRET).update(source).digest('base64');//digest('hex'); // set the HMAC hash header 304 | signature = encodeURIComponent(signature); 305 | //console.log("Signature %s",signature); 306 | if (method === 'POST') { 307 | let opt = reqObjPOST( 308 | url + '?Signature=' + signature, 309 | data, 310 | method, 311 | Huobi.options.APIKEY 312 | ); 313 | proxyRequest(opt, callback); 314 | } else { 315 | let opt = reqObj( 316 | url + '?' + query + '&Signature=' + signature, 317 | data, 318 | method, 319 | Huobi.options.APIKEY 320 | ); 321 | proxyRequest(opt, callback); 322 | } 323 | }; 324 | 325 | const parseSymbol = function (depth) { 326 | //Huobi.options.log("parseSymbol = ",depth) 327 | return depth.ch.split('.')[1]; 328 | }; 329 | 330 | /** 331 | * Used for /depth endpoint 332 | * @param {object} depth - information 333 | * @return {undefined} 334 | */ 335 | const depthHandler = function (depth) { 336 | let symbol = parseSymbol(depth), obj; 337 | let context = Huobi.depthCacheContext[symbol]; 338 | //Huobi.options.log(depth); 339 | //Huobi.options.log("context is" + (context != null)); 340 | let updateDepthCache = function () { 341 | Huobi.depthCache[symbol].eventTime = depth.ts; 342 | for (obj of depth.tick.bids) { //bids 343 | Huobi.depthCache[symbol].bids[obj[0]] = parseFloat(obj[1]); 344 | if (obj[1] === '0.00000000') { 345 | delete Huobi.depthCache[symbol].bids[obj[0]]; 346 | } 347 | } 348 | for (obj of depth.tick.asks) { //asks 349 | Huobi.depthCache[symbol].asks[obj[0]] = parseFloat(obj[1]); 350 | if (obj[1] === '0.00000000') { 351 | delete Huobi.depthCache[symbol].asks[obj[0]]; 352 | } 353 | } 354 | context.skipCount = 0; 355 | context.lastEventUpdateId = depth.ts; 356 | context.lastEventUpdateTime = depth.ts; 357 | }; 358 | 359 | // This is our first legal update from the stream data 360 | updateDepthCache(); 361 | 362 | }; 363 | /** 364 | * Create a signed http request to the signed API 365 | * @param {string} side - BUY or SELL 366 | * @param {string} symbol - The symbol to buy or sell 367 | * @param {string} quantity - The quantity to buy or sell 368 | * @param {string} price - The price per unit to transact each unit at 369 | * @param {object} flags - additional order settings 370 | * @param {function} callback - the callback function 371 | * @return {undefined} 372 | */ 373 | const order = function (side, symbol, quantity, price, flags = {}, callback = false) { 374 | let endpoint = 'v3/order'; 375 | if (Huobi.options.test) endpoint += '/test'; 376 | let opt = { 377 | symbol: symbol, 378 | side: side, 379 | type: 'LIMIT', 380 | quantity: quantity 381 | }; 382 | if (typeof flags.type !== 'undefined') opt.type = flags.type; 383 | if (opt.type.includes('LIMIT')) { 384 | opt.price = price; 385 | opt.timeInForce = 'GTC'; 386 | } 387 | if (typeof flags.timeInForce !== 'undefined') opt.timeInForce = flags.timeInForce; 388 | if (typeof flags.newOrderRespType !== 'undefined') opt.newOrderRespType = flags.newOrderRespType; 389 | if (typeof flags.newClientOrderId !== 'undefined') opt.newClientOrderId = flags.newClientOrderId; 390 | 391 | /* 392 | * STOP_LOSS 393 | * STOP_LOSS_LIMIT 394 | * TAKE_PROFIT 395 | * TAKE_PROFIT_LIMIT 396 | * LIMIT_MAKER 397 | */ 398 | if (typeof flags.icebergQty !== 'undefined') opt.icebergQty = flags.icebergQty; 399 | if (typeof flags.stopPrice !== 'undefined') { 400 | opt.stopPrice = flags.stopPrice; 401 | if (opt.type === 'LIMIT') throw Error('stopPrice: Must set "type" to one of the following: STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT'); 402 | } 403 | signedRequest(base + endpoint, opt, function (error, response) { 404 | if (!response) { 405 | if (callback) callback(error, response); 406 | else Huobi.options.log('Order() error:', error); 407 | return; 408 | } 409 | if (typeof response.msg !== 'undefined' && response.msg === 'Filter failure: MIN_NOTIONAL') { 410 | Huobi.options.log('Order quantity too small. See exchangeInfo() for minimum amounts'); 411 | } 412 | if (callback) callback(error, response); 413 | else Huobi.options.log(side + '(' + symbol + ',' + quantity + ',' + price + ') ', response); 414 | }, 'POST'); 415 | }; 416 | /** 417 | * No-operation function 418 | * @return {undefined} 419 | */ 420 | const noop = function () { 421 | // do nothing 422 | }; 423 | /** 424 | * Gets depth cache for given symbol 425 | * @param {string} symbol - the symbol to fetch 426 | * @return {object} - the depth cache object 427 | */ 428 | const getDepthCache = function (symbol) { 429 | if (typeof Huobi.depthCache[symbol] === 'undefined') return { bids: {}, asks: {} }; 430 | return Huobi.depthCache[symbol]; 431 | }; 432 | /** 433 | * Calculate Buy/Sell volume from DepthCache 434 | * @param {string} symbol - the symbol to fetch 435 | * @return {object} - the depth volume cache object 436 | */ 437 | const depthVolume = function (symbol) { 438 | let cache = getDepthCache(symbol), quantity, price; 439 | let bidbase = 0, askbase = 0, bidqty = 0, askqty = 0; 440 | for (price in cache.bids) { 441 | quantity = cache.bids[price]; 442 | bidbase += parseFloat((quantity * parseFloat(price)).toFixed(8)); 443 | bidqty += quantity; 444 | } 445 | for (price in cache.asks) { 446 | quantity = cache.asks[price]; 447 | askbase += parseFloat((quantity * parseFloat(price)).toFixed(8)); 448 | askqty += quantity; 449 | } 450 | return { bids: bidbase, asks: askbase, bidQty: bidqty, askQty: askqty }; 451 | }; 452 | /** 453 | * Reworked Tuitio's heartbeat code into a shared single interval tick 454 | * @return {undefined} 455 | */ 456 | const socketHeartbeat = function () { 457 | 458 | /* sockets removed from `subscriptions` during a manual terminate() 459 | will no longer be at risk of having functions called on them */ 460 | for (let endpointId in Huobi.subscriptions) { 461 | const ws = Huobi.subscriptions[endpointId]; 462 | if (ws.isAlive) { 463 | ws.isAlive = false; 464 | if (ws.readyState === WebSocket.OPEN) ws.ping(noop); 465 | } else { 466 | if (Huobi.options.verbose) Huobi.options.log('Terminating inactive/broken WebSocket: ' + ws.endpoint); 467 | if (ws.readyState === WebSocket.OPEN) ws.terminate(); 468 | } 469 | } 470 | }; 471 | 472 | /** 473 | * Called when socket is opened, subscriptions are registered for later reference 474 | * @param {function} opened_callback - a callback function 475 | * @return {undefined} 476 | */ 477 | const handleSocketOpen = function (opened_callback) { 478 | this.isAlive = true; 479 | if (Object.keys(Huobi.subscriptions).length === 0) { 480 | Huobi.socketHeartbeatInterval = setInterval(socketHeartbeat, 30000); 481 | } 482 | Huobi.subscriptions[this.endpoint] = this; 483 | if (typeof opened_callback === 'function') opened_callback(this.endpoint); 484 | }; 485 | 486 | /** 487 | * Called when socket is closed, subscriptions are de-registered for later reference 488 | * @param {boolean} reconnect - true or false to reconnect the socket 489 | * @param {string} code - code associated with the socket 490 | * @param {string} reason - string with the response 491 | * @return {undefined} 492 | */ 493 | const handleSocketClose = function (reconnect, code, reason) { 494 | delete Huobi.subscriptions[this.endpoint]; 495 | if (Huobi.subscriptions && Object.keys(Huobi.subscriptions).length === 0) { 496 | clearInterval(Huobi.socketHeartbeatInterval); 497 | } 498 | Huobi.options.log('WebSocket closed: ' + this.endpoint + 499 | (code ? ' (' + code + ')' : '') + 500 | (reason ? ' ' + reason : '')); 501 | if (Huobi.options.reconnect && this.reconnect && reconnect) { 502 | if (this.endpoint && parseInt(this.endpoint.length, 10) === 60) Huobi.options.log('Account data WebSocket reconnecting...'); 503 | else Huobi.options.log('WebSocket reconnecting: ' + this.endpoint + '...'); 504 | try { 505 | reconnect(); 506 | } catch (error) { 507 | Huobi.options.log('WebSocket reconnect error: ' + error.message); 508 | } 509 | } 510 | }; 511 | 512 | /** 513 | * Used by balance to get the balance data 514 | * @param {array} data - account info object 515 | * @return {object} - balances hel with available, onorder amounts 516 | */ 517 | const balanceData = function (data) { 518 | let balances = {}; 519 | if (typeof data === 'undefined') return {}; 520 | if (typeof data.data === 'undefined') { 521 | Huobi.options.log('balanceData error', data); 522 | return {}; 523 | } 524 | 525 | for (let obj of data.data.list) { 526 | //balances[obj.asset] = { available: obj.free, onOrder: obj.locked }; 527 | if (balances[obj.currency] == null){ 528 | balances[obj.currency] = {}; 529 | } 530 | if(obj.type === 'trade'){ 531 | balances[obj.currency].available = obj.balance; 532 | }else if(obj.type === 'frozen'){ 533 | balances[obj.currency].frozen = obj.balance; 534 | } 535 | } 536 | 537 | return balances; 538 | }; 539 | 540 | /** 541 | * Called when socket errors 542 | * @param {object} error - error object message 543 | * @return {undefined} 544 | */ 545 | const handleSocketError = function (error) { 546 | /* Errors ultimately result in a `close` event. 547 | see: https://github.com/websockets/ws/blob/828194044bf247af852b31c49e2800d557fedeff/lib/websocket.js#L126 */ 548 | Huobi.options.log('WebSocket error: ' + this.endpoint + 549 | (error.code ? ' (' + error.code + ')' : '') + 550 | (error.message ? ' ' + error.message : '')); 551 | }; 552 | 553 | /** 554 | * Called on each socket heartbeat 555 | * @return {undefined} 556 | */ 557 | const handleSocketHeartbeat = function () { 558 | this.isAlive = true; 559 | }; 560 | 561 | /** 562 | * Used to terminate a web socket 563 | * @param {string} endpoint - endpoint identifier associated with the web socket 564 | * @param {boolean} reconnect - auto reconnect after termination 565 | * @return {undefined} 566 | */ 567 | const terminate = function (endpoint, reconnect = false) { 568 | let ws = Huobi.subscriptions[endpoint]; 569 | if (!ws) return; 570 | ws.removeAllListeners('message'); 571 | ws.reconnect = reconnect; 572 | ws.terminate(); 573 | } 574 | 575 | 576 | /** 577 | * Checks whether or not an array contains any duplicate elements 578 | * Note(keith1024): at the moment this only works for primitive types, 579 | * will require modification to work with objects 580 | * @param {array} array - the array to check 581 | * @return {boolean} - true or false 582 | */ 583 | const isArrayUnique = function (array) { 584 | let s = new Set(array); 585 | return s.size === array.length; 586 | }; 587 | return { 588 | /** 589 | * Gets depth cache for given symbol 590 | * @param {symbol} symbol - get depch cache for this symbol 591 | * @return {object} - object 592 | */ 593 | depthCache: function (symbol) { 594 | return getDepthCache(symbol); 595 | }, 596 | 597 | /** 598 | * Gets depth volume for given symbol 599 | * @param {symbol} symbol - get depch volume for this symbol 600 | * @return {object} - object 601 | */ 602 | depthVolume: function (symbol) { 603 | return depthVolume(symbol); 604 | }, 605 | // 606 | // /** 607 | // * Count decimal places 608 | // * @param {float} float - get the price precision point 609 | // * @return {int} - number of place 610 | // */ 611 | // getPrecision: function (float) { 612 | // if ( !float || Number.isInteger( float ) ) return 0; 613 | // return float.toString().split('.')[1].length || 0; 614 | // }, 615 | // 616 | // /** 617 | // * rounds number with given step 618 | // * @param {float} qty - quantity to round 619 | // * @param {float} stepSize - stepSize as specified by exchangeInfo 620 | // * @return {float} - number 621 | // */ 622 | // roundStep: function (qty, stepSize) { 623 | // // Integers do not require rounding 624 | // if (Number.isInteger(qty)) return qty; 625 | // const qtyString = qty.toFixed(16); 626 | // const desiredDecimals = Math.max(stepSize.indexOf('1') - 1, 0); 627 | // const decimalIndex = qtyString.indexOf('.'); 628 | // return parseFloat(qtyString.slice(0, decimalIndex + desiredDecimals + 1)); 629 | // }, 630 | // 631 | // /** 632 | // * rounds price to required precision 633 | // * @param {float} price - price to round 634 | // * @param {float} tickSize - tickSize as specified by exchangeInfo 635 | // * @return {float} - number 636 | // */ 637 | // roundTicks: function (price, tickSize) { 638 | // const formatter = new Intl.NumberFormat('en-US', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 8 }); 639 | // const precision = formatter.format(tickSize).split('.')[1].length || 0; 640 | // if (typeof price === 'string') price = parseFloat(price); 641 | // return price.toFixed(precision); 642 | // }, 643 | // 644 | // /** 645 | // * Gets percentage of given numbers 646 | // * @param {float} min - the smaller number 647 | // * @param {float} max - the bigger number 648 | // * @param {int} width - percentage width 649 | // * @return {float} - percentage 650 | // */ 651 | // percent: function (min, max, width = 100) { 652 | // return (min * 0.01) / (max * 0.01) * width; 653 | // }, 654 | // 655 | // /** 656 | // * Gets the sum of an array of numbers 657 | // * @param {array} array - the number to add 658 | // * @return {float} - sum 659 | // */ 660 | // sum: function (array) { 661 | // return array.reduce((a, b) => a + b, 0); 662 | // }, 663 | // 664 | // /** 665 | // * Reverses the keys of an object 666 | // * @param {object} object - the object 667 | // * @return {object} - the object 668 | // */ 669 | // reverse: function (object) { 670 | // let range = Object.keys(object).reverse(), output = {}; 671 | // for (let price of range) { 672 | // output[price] = object[price]; 673 | // } 674 | // return output; 675 | // }, 676 | // 677 | // /** 678 | // * Converts an object to an array 679 | // * @param {object} obj - the object 680 | // * @return {array} - the array 681 | // */ 682 | // array: function (obj) { 683 | // return Object.keys(obj).map(function (key) { 684 | // return [Number(key), obj[key]]; 685 | // }); 686 | // }, 687 | // 688 | /** 689 | * Sorts bids 690 | * @param {string} symbol - the object 691 | * @param {int} max - the max number of bids 692 | * @param {string} baseValue - the object 693 | * @return {object} - the object 694 | */ 695 | sortBids: function (symbol, max = Infinity, baseValue = false) { 696 | let object = {}, count = 0, cache; 697 | if (typeof symbol === 'object') cache = symbol; 698 | else cache = getDepthCache(symbol).bids; 699 | let sorted = Object.keys(cache).sort(function (a, b) { 700 | return parseFloat(b) - parseFloat(a) 701 | }); 702 | let cumulative = 0; 703 | for (let price of sorted) { 704 | if (baseValue === 'cumulative') { 705 | cumulative += parseFloat(cache[price]); 706 | object[price] = cumulative; 707 | } else if (!baseValue) object[price] = parseFloat(cache[price]); 708 | else object[price] = parseFloat((cache[price] * parseFloat(price)).toFixed(8)); 709 | if (++count >= max) break; 710 | } 711 | return object; 712 | }, 713 | 714 | /** 715 | * Sorts asks 716 | * @param {string} symbol - the object 717 | * @param {int} max - the max number of bids 718 | * @param {string} baseValue - the object 719 | * @return {object} - the object 720 | */ 721 | sortAsks: function (symbol, max = Infinity, baseValue = false) { 722 | let object = {}, count = 0, cache; 723 | if (typeof symbol === 'object') cache = symbol; 724 | else cache = getDepthCache(symbol).asks; 725 | let sorted = Object.keys(cache).sort(function (a, b) { 726 | return parseFloat(a) - parseFloat(b); 727 | }); 728 | let cumulative = 0; 729 | for (let price of sorted) { 730 | if (baseValue === 'cumulative') { 731 | cumulative += parseFloat(cache[price]); 732 | object[price] = cumulative; 733 | } else if (!baseValue) object[price] = parseFloat(cache[price]); 734 | else object[price] = parseFloat((cache[price] * parseFloat(price)).toFixed(8)); 735 | if (++count >= max) break; 736 | } 737 | return object; 738 | }, 739 | // 740 | // /** 741 | // * Returns the first property of an object 742 | // * @param {object} object - the object to get the first member 743 | // * @return {string} - the object key 744 | // */ 745 | // first: function (object) { 746 | // return Object.keys(object).shift(); 747 | // }, 748 | // 749 | // /** 750 | // * Returns the last property of an object 751 | // * @param {object} object - the object to get the first member 752 | // * @return {string} - the object key 753 | // */ 754 | // last: function (object) { 755 | // return Object.keys(object).pop(); 756 | // }, 757 | // 758 | // /** 759 | // * Returns an array of properties starting at start 760 | // * @param {object} object - the object to get the properties form 761 | // * @param {int} start - the starting index 762 | // * @return {array} - the array of entires 763 | // */ 764 | // slice: function (object, start = 0) { 765 | // return Object.entries(object).slice(start).map(entry => entry[0]); 766 | // }, 767 | // 768 | // /** 769 | // * Gets the minimum key form object 770 | // * @param {object} object - the object to get the properties form 771 | // * @return {string} - the minimum key 772 | // */ 773 | // min: function (object) { 774 | // return Math.min.apply(Math, Object.keys(object)); 775 | // }, 776 | // 777 | // /** 778 | // * Gets the maximum key form object 779 | // * @param {object} object - the object to get the properties form 780 | // * @return {string} - the minimum key 781 | // */ 782 | // max: function (object) { 783 | // return Math.max.apply(Math, Object.keys(object)); 784 | // }, 785 | // 786 | // /** 787 | // * Sets an option given a key and value 788 | // * @param {string} key - the key to set 789 | // * @param {object} value - the value of the key 790 | // * @return {undefined} 791 | // */ 792 | // setOption: function (key, value) { 793 | // Binance.options[key] = value; 794 | // }, 795 | // 796 | // /** 797 | // * Gets an option given a key 798 | // * @param {string} key - the key to set 799 | // * @return {undefined} 800 | // */ 801 | // getOption: function (key) { 802 | // return Binance.options[key]; 803 | // }, 804 | // 805 | // /** 806 | // * Returns the entire info object 807 | // * @return {object} - the info object 808 | // */ 809 | // getInfo: function () { 810 | // return Binance.info; 811 | // }, 812 | 813 | /** 814 | * Returns the entire options object 815 | * @return {object} - the options object 816 | */ 817 | getOptions: function () { 818 | return Binance.options; 819 | }, 820 | 821 | /** 822 | * Gets an option given a key 823 | * @param {object} opt - the object with the class configuration 824 | * @param {function} callback - the callback function 825 | * @return {undefined} 826 | */ 827 | options: function (opt, callback = false) { 828 | if (typeof opt === 'string') { // Pass json config filename 829 | Huobi.options = JSON.parse(file.readFileSync(opt)); 830 | } else Huobi.options = opt; 831 | if (typeof Huobi.options.recvWindow === 'undefined') Huobi.options.recvWindow = default_options.recvWindow; 832 | if (typeof Huobi.options.useServerTime === 'undefined') Huobi.options.useServerTime = default_options.useServerTime; 833 | if (typeof Huobi.options.reconnect === 'undefined') Huobi.options.reconnect = default_options.reconnect; 834 | if (typeof Huobi.options.test === 'undefined') Huobi.options.test = default_options.test; 835 | if (typeof Huobi.options.log === 'undefined') Huobi.options.log = default_options.log; 836 | if (typeof Huobi.options.verbose === 'undefined') Huobi.options.verbose = default_options.verbose; 837 | if (Huobi.options.useServerTime) { 838 | apiRequest(base + 'v1/time', {}, function (error, response) { 839 | Huobi.info.timeOffset = response.serverTime - new Date().getTime(); 840 | //Binance.options.log("server time set: ", response.serverTime, Binance.info.timeOffset); 841 | if (callback) callback(); 842 | }); 843 | } else if (callback) callback(); 844 | return this; 845 | }, 846 | 847 | 848 | /** 849 | * Creates an order 850 | * @param {string} side - BUY or SELL 851 | * @param {string} symbol - the symbol to buy 852 | * @param {numeric} quantity - the quantity required 853 | * @param {numeric} price - the price to pay for each unit 854 | * @param {object} flags - aadditionalbuy order flags 855 | * @param {function} callback - the callback function 856 | * @return {undefined} 857 | */ 858 | order: function (side, symbol, quantity, price, flags = {}, callback = false) { 859 | order(side, symbol, quantity, price, flags, callback); 860 | }, 861 | 862 | /** 863 | * Creates a buy order 864 | * @param {string} symbol - the symbol to buy 865 | * @param {numeric} quantity - the quantity required 866 | * @param {numeric} price - the price to pay for each unit 867 | * @param {object} flags - additional buy order flags 868 | * @param {function} callback - the callback function 869 | * @return {undefined} 870 | */ 871 | buy: function (symbol, quantity, price, flags = {}, callback = false) { 872 | order('BUY', symbol, quantity, price, flags, callback); 873 | }, 874 | 875 | /** 876 | * Creates a sell order 877 | * @param {string} symbol - the symbol to sell 878 | * @param {numeric} quantity - the quantity required 879 | * @param {numeric} price - the price to sell each unit for 880 | * @param {object} flags - additional order flags 881 | * @param {function} callback - the callback function 882 | * @return {undefined} 883 | */ 884 | sell: function (symbol, quantity, price, flags = {}, callback = false) { 885 | order('SELL', symbol, quantity, price, flags, callback); 886 | }, 887 | 888 | /** 889 | * Creates a market buy order 890 | * @param {string} symbol - the symbol to buy 891 | * @param {numeric} quantity - the quantity required 892 | * @param {object} flags - additional buy order flags 893 | * @param {function} callback - the callback function 894 | * @return {undefined} 895 | */ 896 | marketBuy: function (symbol, quantity, flags = { type: 'MARKET' }, callback = false) { 897 | if (typeof flags === 'function') { // Accept callback as third parameter 898 | callback = flags; 899 | flags = { type: 'MARKET' }; 900 | } 901 | if (typeof flags.type === 'undefined') flags.type = 'MARKET'; 902 | order('BUY', symbol, quantity, 0, flags, callback); 903 | }, 904 | 905 | /** 906 | * Creates a market sell order 907 | * @param {string} symbol - the symbol to sell 908 | * @param {numeric} quantity - the quantity required 909 | * @param {object} flags - additional sell order flags 910 | * @param {function} callback - the callback function 911 | * @return {undefined} 912 | */ 913 | marketSell: function (symbol, quantity, flags = { type: 'MARKET' }, callback = false) { 914 | if (typeof flags === 'function') { // Accept callback as third parameter 915 | callback = flags; 916 | flags = { type: 'MARKET' }; 917 | } 918 | if (typeof flags.type === 'undefined') flags.type = 'MARKET'; 919 | order('SELL', symbol, quantity, 0, flags, callback); 920 | }, 921 | 922 | /** 923 | * Cancels an order 924 | * @param {string} symbol - the symbol to cancel 925 | * @param {string} orderid - the orderid to cancel 926 | * @param {function} callback - the callback function 927 | * @return {undefined} 928 | */ 929 | cancel: function (symbol, orderid, callback = false) { 930 | signedRequest(base + 'v3/order', { symbol: symbol, orderId: orderid }, function (error, data) { 931 | if (callback) return callback.call(this, error, data, symbol); 932 | }, 'DELETE'); 933 | }, 934 | 935 | /** 936 | * Gets the status of an order 937 | * @param {string} symbol - the symbol to check 938 | * @param {string} orderid - the orderid to check 939 | * @param {function} callback - the callback function 940 | * @param {object} flags - any additional flags 941 | * @return {undefined} 942 | */ 943 | orderStatus: function (symbol, orderid, callback, flags = {}) { 944 | let parameters = Object.assign({ symbol: symbol, orderId: orderid }, flags); 945 | signedRequest(base + 'v3/order', parameters, function (error, data) { 946 | if (callback) return callback.call(this, error, data, symbol); 947 | }); 948 | }, 949 | 950 | /** 951 | * Gets open orders 952 | * @param {string} symbol - the symbol to get 953 | * @param {function} callback - the callback function 954 | * @return {undefined} 955 | */ 956 | openOrders: function (symbol, callback) { 957 | let parameters = symbol ? { symbol: symbol } : {}; 958 | signedRequest(base + 'v3/openOrders', parameters, function (error, data) { 959 | return callback.call(this, error, data, symbol); 960 | }); 961 | }, 962 | 963 | /** 964 | * Cancels all order of a given symbol 965 | * @param {string} symbol - the symbol to cancel all orders for 966 | * @param {function} callback - the callback function 967 | * @return {undefined} 968 | */ 969 | cancelOrders: function (symbol, callback = false) { 970 | signedRequest(base + 'v3/openOrders', { symbol: symbol }, function (error, json) { 971 | if (json.length === 0) { 972 | if (callback) return callback.call(this, 'No orders present for this symbol', {}, symbol); 973 | } 974 | for (let obj of json) { 975 | let quantity = obj.origQty - obj.executedQty; 976 | Binance.options.log('cancel order: ' + obj.side + ' ' + symbol + ' ' + quantity + ' @ ' + obj.price + ' #' + obj.orderId); 977 | signedRequest(base + 'v3/order', { symbol: symbol, orderId: obj.orderId }, function (error, data) { 978 | if (callback) return callback.call(this, error, data, symbol); 979 | }, 'DELETE'); 980 | } 981 | }); 982 | }, 983 | 984 | /** 985 | * Gets all order of a given symbol 986 | * @param {string} symbol - the symbol 987 | * @param {function} callback - the callback function 988 | * @param {object} options - additional options 989 | * @return {undefined} 990 | */ 991 | allOrders: function (symbol, callback, options = {}) { 992 | let parameters = Object.assign({ symbol: symbol }, options); 993 | signedRequest(base + 'v3/allOrders', parameters, function (error, data) { 994 | if (callback) return callback.call(this, error, data, symbol); 995 | }); 996 | }, 997 | 998 | /** 999 | * Gets the depth information for a given symbol 1000 | * @param {string} symbol - the symbol 1001 | * @param {function} callback - the callback function 1002 | * @param {int} limit - limit the number of returned orders 1003 | * @return {undefined} 1004 | */ 1005 | depth: function (symbol, callback, limit = 100) { 1006 | publicRequest(base + 'v1/depth', { symbol: symbol, limit: limit }, function (error, data) { 1007 | return callback.call(this, error, depthData(data), symbol); 1008 | }); 1009 | }, 1010 | 1011 | /** 1012 | * Gets the average prices of a given symbol 1013 | * @param {string} symbol - the symbol 1014 | * @param {function} callback - the callback function 1015 | * @return {undefined} 1016 | */ 1017 | avgPrice: function (symbol, callback = false) { 1018 | let socksproxy = process.env.socks_proxy || false; 1019 | 1020 | let opt = { 1021 | url: base + 'v3/avgPrice?symbol=' + symbol, 1022 | timeout: Binance.options.recvWindow 1023 | }; 1024 | 1025 | if (socksproxy !== false) { 1026 | socksproxy = proxyReplacewithIp(socksproxy); 1027 | if (Binance.options.verbose) Binance.options.log('using socks proxy server ' + socksproxy); 1028 | opt.agentClass = SocksProxyAgent; 1029 | opt.agentOptions = { 1030 | protocol: parseProxy(socksproxy)[0], 1031 | host: parseProxy(socksproxy)[1], 1032 | port: parseProxy(socksproxy)[2] 1033 | } 1034 | } 1035 | 1036 | request(opt, function (error, response, body) { 1037 | if (!callback) return; 1038 | 1039 | if (error) return callback(error); 1040 | 1041 | if (response && response.statusCode !== 200) return callback(response); 1042 | 1043 | if (callback) return callback(null, priceData(JSON.parse(body))); 1044 | }); 1045 | }, 1046 | 1047 | /** 1048 | * Gets the prices of a given symbol(s) 1049 | * @param {string} symbol - the symbol 1050 | * @param {function} callback - the callback function 1051 | * @return {undefined} 1052 | */ 1053 | prices: function (symbol, callback = false) { 1054 | const params = typeof symbol === 'string' ? '?symbol=' + symbol : ''; 1055 | if (typeof symbol === 'function') callback = symbol; // backwards compatibility 1056 | 1057 | let socksproxy = process.env.socks_proxy || false; 1058 | 1059 | let opt = { 1060 | url: base + 'v3/ticker/price' + params, 1061 | timeout: Binance.options.recvWindow 1062 | }; 1063 | 1064 | if (socksproxy !== false) { 1065 | socksproxy = proxyReplacewithIp(socksproxy); 1066 | if (Binance.options.verbose) Binance.options.log('using socks proxy server ' + socksproxy); 1067 | opt.agentClass = SocksProxyAgent; 1068 | opt.agentOptions = { 1069 | protocol: parseProxy(socksproxy)[0], 1070 | host: parseProxy(socksproxy)[1], 1071 | port: parseProxy(socksproxy)[2] 1072 | } 1073 | } 1074 | 1075 | request(opt, function (error, response, body) { 1076 | if (!callback) return; 1077 | 1078 | if (error) return callback(error); 1079 | 1080 | if (response && response.statusCode !== 200) return callback(response); 1081 | 1082 | if (callback) return callback(null, priceData(JSON.parse(body))); 1083 | }); 1084 | }, 1085 | 1086 | /** 1087 | * Gets the book tickers of given symbol(s) 1088 | * @param {string} symbol - the symbol 1089 | * @param {function} callback - the callback function 1090 | * @return {undefined} 1091 | */ 1092 | bookTickers: function (symbol, callback) { 1093 | const params = typeof symbol === 'string' ? '?symbol=' + symbol : ''; 1094 | if (typeof symbol === 'function') callback = symbol; // backwards compatibility 1095 | 1096 | let socksproxy = process.env.socks_proxy || false; 1097 | 1098 | let opt = { 1099 | url: base + 'v3/ticker/bookTicker' + params, 1100 | timeout: Binance.options.recvWindow 1101 | }; 1102 | 1103 | if (socksproxy !== false) { 1104 | socksproxy = proxyReplacewithIp(socksproxy); 1105 | if (Binance.options.verbose) Binance.options.log('using socks proxy server ' + socksproxy); 1106 | opt.agentClass = SocksProxyAgent; 1107 | opt.agentOptions = { 1108 | protocol: parseProxy(socksproxy)[0], 1109 | host: parseProxy(socksproxy)[1], 1110 | port: parseProxy(socksproxy)[2] 1111 | } 1112 | } 1113 | 1114 | request(opt, function (error, response, body) { 1115 | if (!callback) return; 1116 | 1117 | if (error) return callback(error); 1118 | 1119 | if (response && response.statusCode !== 200) return callback(response); 1120 | 1121 | if (callback) { 1122 | const result = symbol ? JSON.parse(body) : bookPriceData(JSON.parse(body)); 1123 | return callback(null, result); 1124 | } 1125 | }); 1126 | }, 1127 | 1128 | /** 1129 | * Gets the prevday percentage change 1130 | * @param {string} symbol - the symbol or symbols 1131 | * @param {function} callback - the callback function 1132 | * @return {undefined} 1133 | */ 1134 | prevDay: function (symbol, callback) { 1135 | let input = symbol ? { symbol: symbol } : {}; 1136 | publicRequest(base + 'v1/ticker/24hr', input, function (error, data) { 1137 | if (callback) return callback.call(this, error, data, symbol); 1138 | }); 1139 | }, 1140 | 1141 | /** 1142 | * Gets the the exchange info 1143 | * @param {function} callback - the callback function 1144 | * @return {undefined} 1145 | */ 1146 | exchangeInfo: function (callback) { 1147 | publicRequest(base + '/v1/common/symbols', {}, callback); 1148 | }, 1149 | /** 1150 | * Gets the dust log for user 1151 | * @param {function} callback - the callback function 1152 | * @return {undefined} 1153 | */ 1154 | dustLog: function (callback) { 1155 | signedRequest(wapi + '/v3/userAssetDribbletLog.html', {}, callback); 1156 | }, 1157 | /** 1158 | * Gets the the system status 1159 | * @param {function} callback - the callback function 1160 | * @return {undefined} 1161 | */ 1162 | systemStatus: function (callback) { 1163 | publicRequest(wapi + 'v3/systemStatus.html', {}, callback); 1164 | }, 1165 | 1166 | /** 1167 | * Withdraws asset to given wallet id 1168 | * @param {string} asset - the asset symbol 1169 | * @param {string} address - the wallet to transfer it to 1170 | * @param {number} amount - the amount to transfer 1171 | * @param {string} addressTag - and addtional address tag 1172 | * @param {function} callback - the callback function 1173 | * @return {undefined} 1174 | */ 1175 | withdraw: function (asset, address, amount, addressTag = false, callback = false) { 1176 | let params = { asset, address, amount }; 1177 | params.name = 'API Withdraw'; 1178 | if (addressTag) params.addressTag = addressTag; 1179 | signedRequest(wapi + 'v3/withdraw.html', params, callback, 'POST'); 1180 | }, 1181 | 1182 | /** 1183 | * Get the Withdraws history for a given asset 1184 | * @param {function} callback - the callback function 1185 | * @param {object} params - supports limit and fromId parameters 1186 | * @return {undefined} 1187 | */ 1188 | withdrawHistory: function (callback, params = {}) { 1189 | if (typeof params === 'string') params = { asset: params }; 1190 | signedRequest(wapi + 'v3/withdrawHistory.html', params, callback); 1191 | }, 1192 | 1193 | /** 1194 | * Get the deposit history 1195 | * @param {function} callback - the callback function 1196 | * @param {object} params - additional params 1197 | * @return {undefined} 1198 | */ 1199 | depositHistory: function (callback, params = {}) { 1200 | if (typeof params === 'string') params = { asset: params }; // Support 'asset' (string) or optional parameters (object) 1201 | signedRequest(wapi + 'v3/depositHistory.html', params, callback); 1202 | }, 1203 | 1204 | /** 1205 | * Get the deposit history for given asset 1206 | * @param {string} asset - the asset 1207 | * @param {function} callback - the callback function 1208 | * @return {undefined} 1209 | */ 1210 | depositAddress: function (asset, callback) { 1211 | signedRequest(wapi + 'v3/depositAddress.html', { asset: asset }, callback); 1212 | }, 1213 | 1214 | /** 1215 | * Get the account status 1216 | * @param {function} callback - the callback function 1217 | * @return {undefined} 1218 | */ 1219 | accountStatus: function (callback) { 1220 | signedRequest(wapi + 'v3/accountStatus.html', {}, callback); 1221 | }, 1222 | 1223 | /** 1224 | * Get the trade fee 1225 | * @param {function} callback - the callback function 1226 | * @param {string} symbol (optional) 1227 | * @return {undefined} 1228 | */ 1229 | tradeFee: function (callback, symbol = false) { 1230 | let params = symbol ? { symbol: symbol } : {}; 1231 | signedRequest(wapi + 'v3/tradeFee.html', params, callback); 1232 | }, 1233 | 1234 | /** 1235 | * Fetch asset detail (minWithdrawAmount, depositStatus, withdrawFee, withdrawStatus, depositTip) 1236 | * @param {function} callback - the callback function 1237 | * @return {undefined} 1238 | */ 1239 | assetDetail: function (callback) { 1240 | signedRequest(wapi + 'v3/assetDetail.html', {}, callback); 1241 | }, 1242 | 1243 | /** 1244 | * Get the account 1245 | * @param {function} callback - the callback function 1246 | * @return {undefined} 1247 | */ 1248 | account: function (callback) { 1249 | signedRequest(base + '/v1/account/accounts', {}, callback); 1250 | }, 1251 | 1252 | /** 1253 | * Get the balance data 1254 | * @param {string} accountId - the account 1255 | * @param {function} callback - the callback function 1256 | * @return {undefined} 1257 | */ 1258 | balance: function (accountId,callback) { 1259 | signedRequest(base + '/v1/account/accounts/'+accountId+'/balance', {}, function (error, data) { 1260 | if (callback) callback(error, balanceData(data)); 1261 | }); 1262 | }, 1263 | 1264 | /** 1265 | * Get trades for a given symbol 1266 | * @param {string} symbol - the symbol 1267 | * @param {function} callback - the callback function 1268 | * @param {object} options - additional options 1269 | * @return {undefined} 1270 | */ 1271 | trades: function (symbol, callback, options = {}) { 1272 | let parameters = Object.assign({ symbol: symbol }, options); 1273 | signedRequest(base + 'v3/myTrades', parameters, function (error, data) { 1274 | if (callback) return callback.call(this, error, data, symbol); 1275 | }); 1276 | }, 1277 | 1278 | /** 1279 | * Tell api to use the server time to offset time indexes 1280 | * @param {function} callback - the callback function 1281 | * @return {undefined} 1282 | */ 1283 | useServerTime: function (callback = false) { 1284 | apiRequest(base + 'v1/time', {}, function (error, response) { 1285 | Huobi.info.timeOffset = response.serverTime - new Date().getTime(); 1286 | //Binance.options.log("server time set: ", response.serverTime, Binance.info.timeOffset); 1287 | if (callback) callback(); 1288 | }); 1289 | }, 1290 | 1291 | /** 1292 | * Gets the time 1293 | * @param {function} callback - the callback function 1294 | * @return {undefined} 1295 | */ 1296 | time: function (callback) { 1297 | apiRequest(base + '/v1/common/timestamp', {}, callback); 1298 | }, 1299 | 1300 | /** 1301 | * Get agg trades for given symbol 1302 | * @param {string} symbol - the symbol 1303 | * @param {object} options - addtional optoins 1304 | * @param {function} callback - the callback function 1305 | * @return {undefined} 1306 | */ 1307 | aggTrades: function (symbol, options = {}, callback = false) { //fromId startTime endTime limit 1308 | let parameters = Object.assign({ symbol }, options); 1309 | marketRequest(base + 'v1/aggTrades', parameters, callback); 1310 | }, 1311 | 1312 | /** 1313 | * Get the recent trades 1314 | * @param {string} symbol - the symbol 1315 | * @param {function} callback - the callback function 1316 | * @param {int} limit - limit the number of items returned 1317 | * @return {undefined} 1318 | */ 1319 | recentTrades: function (symbol, callback, limit = 500) { 1320 | marketRequest(base + 'v1/trades', { symbol: symbol, limit: limit }, callback); 1321 | }, 1322 | 1323 | /** 1324 | * Get the historical trade info 1325 | * @param {string} symbol - the symbol 1326 | * @param {function} callback - the callback function 1327 | * @param {int} limit - limit the number of items returned 1328 | * @param {int} fromId - from this id 1329 | * @return {undefined} 1330 | */ 1331 | historicalTrades: function (symbol, callback, limit = 500, fromId = false) { 1332 | let parameters = { symbol: symbol, limit: limit }; 1333 | if (fromId) parameters.fromId = fromId; 1334 | marketRequest(base + 'v1/historicalTrades', parameters, callback); 1335 | }, 1336 | 1337 | /** 1338 | * Convert chart data to highstock array [timestamp,open,high,low,close] 1339 | * @param {object} chart - the chart 1340 | * @param {boolean} include_volume - to include the volume or not 1341 | * @return {array} - an array 1342 | */ 1343 | highstock: function (chart, include_volume = false) { 1344 | let array = []; 1345 | for (let timestamp in chart) { 1346 | let obj = chart[timestamp]; 1347 | let line = [ 1348 | Number(timestamp), 1349 | parseFloat(obj.open), 1350 | parseFloat(obj.high), 1351 | parseFloat(obj.low), 1352 | parseFloat(obj.close) 1353 | ]; 1354 | if (include_volume) line.push(parseFloat(obj.volume)); 1355 | array.push(line); 1356 | } 1357 | return array; 1358 | }, 1359 | 1360 | /** 1361 | * Populates hte OHLC information 1362 | * @param {object} chart - the chart 1363 | * @return {object} - object with candle information 1364 | */ 1365 | ohlc: function (chart) { 1366 | let open = [], high = [], low = [], close = [], volume = []; 1367 | for (let timestamp in chart) { //Binance.ohlc[symbol][interval] 1368 | let obj = chart[timestamp]; 1369 | open.push(parseFloat(obj.open)); 1370 | high.push(parseFloat(obj.high)); 1371 | low.push(parseFloat(obj.low)); 1372 | close.push(parseFloat(obj.close)); 1373 | volume.push(parseFloat(obj.volume)); 1374 | } 1375 | return { open: open, high: high, low: low, close: close, volume: volume }; 1376 | }, 1377 | 1378 | /** 1379 | * Gets the candles information for a given symbol 1380 | * intervals: 1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M 1381 | * @param {string} symbol - the symbol 1382 | * @param {function} interval - the callback function 1383 | * @param {function} callback - the callback function 1384 | * @param {object} options - additional options 1385 | * @return {undefined} 1386 | */ 1387 | candlesticks: function (symbol, interval = '5m', callback = false, options = { limit: 500 }) { 1388 | if (!callback) return; 1389 | let params = Object.assign({ symbol: symbol, interval: interval }, options); 1390 | publicRequest(base + 'v1/klines', params, function (error, data) { 1391 | return callback.call(this, error, data, symbol); 1392 | }); 1393 | }, 1394 | 1395 | /** 1396 | * Queries the public api 1397 | * @param {string} url - the public api endpoint 1398 | * @param {object} data - the data to send 1399 | * @param {function} callback - the callback function 1400 | * @param {string} method - the http method 1401 | * @return {undefined} 1402 | */ 1403 | publicRequest: function (url, data, callback, method = 'GET') { 1404 | publicRequest(url, data, callback, method) 1405 | }, 1406 | 1407 | /** 1408 | * Queries the signed api 1409 | * @param {string} url - the signed api endpoint 1410 | * @param {object} data - the data to send 1411 | * @param {function} callback - the callback function 1412 | * @param {string} method - the http method 1413 | * @return {undefined} 1414 | */ 1415 | signedRequest: function (url, data, callback, method = 'GET') { 1416 | signedRequest(url, data, callback, method); 1417 | }, 1418 | 1419 | /** 1420 | * Gets the market asset of given symbol 1421 | * @param {string} symbol - the public api endpoint 1422 | * @return {undefined} 1423 | */ 1424 | getMarket: function (symbol) { 1425 | const substring = symbol.substr(-3); 1426 | if (substring === 'BTC') return 'BTC'; 1427 | else if (substring === 'ETH') return 'ETH'; 1428 | else if (substring === 'BNB') return 'BNB'; 1429 | else if (symbol.substr(-4) === 'USDT') return 'USDT'; 1430 | }, 1431 | websockets: { 1432 | /** 1433 | * Userdata websockets function 1434 | * @param {function} callback - the callback function 1435 | * @param {function} execution_callback - optional execution callback 1436 | * @param {function} subscribed_callback - subscription callback 1437 | * @return {undefined} 1438 | */ 1439 | userData: function userData(callback, execution_callback = false, subscribed_callback = false) { 1440 | let reconnect = function () { 1441 | if (Huobi.options.reconnect) userData(callback, execution_callback, subscribed_callback); 1442 | }; 1443 | apiRequest(base + 'v1/userDataStream', {}, function (error, response) { 1444 | Huobi.options.listenKey = response.listenKey; 1445 | setTimeout(function userDataKeepAlive() { // keepalive 1446 | try { 1447 | apiRequest(base + 'v1/userDataStream?listenKey=' + Huobi.options.listenKey, {}, function (err) { 1448 | if (err) setTimeout(userDataKeepAlive, 60000); // retry in 1 minute 1449 | else setTimeout(userDataKeepAlive, 60 * 30 * 1000); // 30 minute keepalive 1450 | }, 'PUT'); 1451 | } catch (error) { 1452 | setTimeout(userDataKeepAlive, 60000); // retry in 1 minute 1453 | } 1454 | }, 60 * 30 * 1000); // 30 minute keepalive 1455 | Huobi.options.balance_callback = callback; 1456 | Huobi.options.execution_callback = execution_callback; 1457 | const subscription = subscribe(Huobi.options.listenKey, userDataHandler, reconnect); 1458 | if (subscribed_callback) subscribed_callback(subscription.endpoint); 1459 | }, 'POST'); 1460 | }, 1461 | 1462 | /** 1463 | * Subscribe to a generic websocket 1464 | * @param {string} url - the websocket endpoint 1465 | * @param {function} callback - optional execution callback 1466 | * @param {boolean} reconnect - subscription callback 1467 | * @return {WebSocket} the websocket reference 1468 | */ 1469 | subscribe: function (url, callback, reconnect = false) { 1470 | return subscribe(url, callback, reconnect); 1471 | }, 1472 | 1473 | /** 1474 | * Subscribe to a generic combined websocket 1475 | * @param {string} url - the websocket endpoint 1476 | * @param {function} callback - optional execution callback 1477 | * @param {boolean} reconnect - subscription callback 1478 | * @return {WebSocket} the websocket reference 1479 | */ 1480 | subscribeCombined: function (url, callback, reconnect = false) { 1481 | return subscribeCombined(url, callback, reconnect); 1482 | }, 1483 | 1484 | /** 1485 | * Returns the known websockets subscriptions 1486 | * @return {array} array of web socket subscriptions 1487 | */ 1488 | subscriptions: function () { 1489 | return Huobi.subscriptions; 1490 | }, 1491 | 1492 | /** 1493 | * Terminates a web socket 1494 | * @param {string} endpoint - the string associated with the endpoint 1495 | * @return {undefined} 1496 | */ 1497 | terminate: function (endpoint) { 1498 | if (Huobi.options.verbose) Huobi.options.log('WebSocket terminating:', endpoint); 1499 | return terminate(endpoint); 1500 | }, 1501 | 1502 | /** 1503 | * Websocket depth chart 1504 | * @param {array/string} symbols - an array or string of symbols to query 1505 | * @param {function} callback - callback function 1506 | * @return {string} the websocket endpoint 1507 | */ 1508 | depth: function depth(symbols, callback) { 1509 | let reconnect = function () { 1510 | if (Huobi.options.reconnect) depth(symbols, callback); 1511 | }; 1512 | 1513 | let subscription; 1514 | if (Array.isArray(symbols)) { 1515 | // if (!isArrayUnique(symbols)) throw Error('depth: "symbols" cannot contain duplicate elements.'); 1516 | // let streams = symbols.map(function (symbol) { 1517 | // return symbol.toLowerCase() + '@depth'; 1518 | // }); 1519 | // subscription = subscribeCombined(streams, callback, reconnect); 1520 | } else { 1521 | let symbol = symbols; 1522 | subscription = subscribe(symbol, callback, reconnect); 1523 | } 1524 | return subscription.endpoint; 1525 | }, 1526 | 1527 | /** 1528 | * Websocket depth cache 1529 | * @param {array/string} symbols - an array or string of symbols to query 1530 | * @param {function} callback - callback function 1531 | * @param {int} limit - the number of entries 1532 | * @return {string} the websocket endpoint 1533 | */ 1534 | depthCache: function depthCacheFunction(symbols, callback, limit = 500) { 1535 | let reconnect = function () { 1536 | if (Huobi.options.reconnect) depthCacheFunction(symbols, callback, limit); 1537 | }; 1538 | 1539 | let symbolDepthInit = function (symbol) { 1540 | //Huobi.options.log("symbolDepthInit ====="+symbol); 1541 | if (typeof Huobi.depthCacheContext[symbol] === 'undefined') Huobi.depthCacheContext[symbol] = {}; 1542 | 1543 | let context = Huobi.depthCacheContext[symbol]; 1544 | context.snapshotUpdateId = null; 1545 | context.lastEventUpdateId = null; 1546 | context.messageQueue = []; 1547 | 1548 | Huobi.depthCache[symbol] = { bids: {}, asks: {} }; 1549 | }; 1550 | 1551 | let assignEndpointIdToContext = function (symbol, endpointId) { 1552 | if (Huobi.depthCacheContext[symbol]) { 1553 | let context = Huobi.depthCacheContext[symbol]; 1554 | context.endpointId = endpointId; 1555 | //Huobi.options.log("symbol "+symbol+" endpointId "+endpointId); 1556 | } 1557 | }; 1558 | 1559 | let handleDepthStreamData = function (depth) { 1560 | let symbol = parseSymbol(depth); 1561 | let context = Huobi.depthCacheContext[symbol]; 1562 | // try { 1563 | depthHandler(depth); 1564 | // } catch (err) { 1565 | // return terminate(context.endpointId, true); 1566 | // } 1567 | if (callback) callback(symbol, Huobi.depthCache[symbol], context); 1568 | 1569 | }; 1570 | 1571 | // let getSymbolDepthSnapshot = function (symbol, cb) { 1572 | // 1573 | // publicRequest(base + 'v1/depth', { symbol: symbol, limit: limit }, function (error, json) { 1574 | // if (error) { 1575 | // return cb(error, null); 1576 | // } 1577 | // // Store symbol next use 1578 | // json.symb = symbol; 1579 | // cb(null, json) 1580 | // }); 1581 | // }; 1582 | 1583 | let updateSymbolDepthCache = function (json) { 1584 | // Get previous store symbol 1585 | let symbol = json.symb; 1586 | // Initialize depth cache from snapshot 1587 | Huobi.depthCache[symbol] = depthData(json); 1588 | // Prepare depth cache context 1589 | let context = Huobi.depthCacheContext[symbol]; 1590 | context.snapshotUpdateId = json.lastUpdateId; 1591 | context.messageQueue = context.messageQueue.filter(depth => depth.u > context.snapshotUpdateId); 1592 | // Process any pending depth messages 1593 | for (let depth of context.messageQueue) { 1594 | 1595 | /* Although sync errors shouldn't ever happen here, we catch and swallow them anyway 1596 | just in case. The stream handler function above will deal with broken caches. */ 1597 | try { 1598 | depthHandler(depth); 1599 | } catch (err) { 1600 | // do nothing 1601 | } 1602 | } 1603 | delete context.messageQueue; 1604 | if (callback) callback(symbol, Huobi.depthCache[symbol]); 1605 | }; 1606 | 1607 | /* If an array of symbols are sent we use a combined stream connection rather. 1608 | This is transparent to the developer, and results in a single socket connection. 1609 | This essentially eliminates "unexpected response" errors when subscribing to a lot of data. */ 1610 | let subscription; 1611 | if (Array.isArray(symbols)) { 1612 | if (!isArrayUnique(symbols)) throw Error('depthCache: "symbols" cannot contain duplicate elements.'); 1613 | 1614 | symbols.forEach(symbolDepthInit); 1615 | let streams = symbols.map(function (symbol) { 1616 | return symbol.toLowerCase(); 1617 | }); 1618 | subscription = subscribe(streams, handleDepthStreamData, reconnect, function () { 1619 | }); 1620 | 1621 | symbols.forEach(s => assignEndpointIdToContext(s, subscription.endpoint)); 1622 | } else { 1623 | let symbol = symbols; 1624 | symbolDepthInit(symbol); 1625 | subscription = subscribe(symbol, handleDepthStreamData, reconnect, function () { 1626 | }); 1627 | assignEndpointIdToContext(symbol, subscription.endpoint); 1628 | } 1629 | return subscription.endpoint; 1630 | }, 1631 | 1632 | /** 1633 | * Websocket staggered depth cache 1634 | * @param {array/string} symbols - an array of symbols to query 1635 | * @param {function} callback - callback function 1636 | * @param {int} limit - the number of entries 1637 | * @param {int} stagger - ms between each depth cache 1638 | * @return {Promise} the websocket endpoint 1639 | */ 1640 | depthCacheStaggered: function (symbols, callback, limit = 100, stagger = 200) { 1641 | if (!Array.isArray(symbols)) symbols = [symbols]; 1642 | let chain = null; 1643 | 1644 | // symbols.forEach(symbol => { 1645 | // let promise = () => new Promise(resolve => { 1646 | // this.depthCache(symbol, callback, limit); 1647 | // setTimeout(resolve, stagger); 1648 | // }); 1649 | // chain = chain ? chain.then(promise) : promise(); 1650 | // }); 1651 | let promise = () => new Promise(resolve => { 1652 | this.depthCache(symbols, callback, limit); 1653 | setTimeout(resolve, stagger); 1654 | }); 1655 | chain = chain ? chain.then(promise) : promise(); 1656 | return chain; 1657 | }, 1658 | 1659 | /** 1660 | * Websocket aggregated trades 1661 | * @param {array/string} symbols - an array or string of symbols to query 1662 | * @param {function} callback - callback function 1663 | * @return {string} the websocket endpoint 1664 | */ 1665 | aggTrades: function trades(symbols, callback) { 1666 | let reconnect = function () { 1667 | if (Huobi.options.reconnect) trades(symbols, callback); 1668 | }; 1669 | 1670 | let subscription; 1671 | if (Array.isArray(symbols)) { 1672 | if (!isArrayUnique(symbols)) throw Error('trades: "symbols" cannot contain duplicate elements.'); 1673 | let streams = symbols.map(function (symbol) { 1674 | return symbol.toLowerCase() + '@aggTrade'; 1675 | }); 1676 | subscription = subscribeCombined(streams, callback, reconnect); 1677 | } else { 1678 | let symbol = symbols; 1679 | subscription = subscribe(symbol.toLowerCase() + '@aggTrade', callback, reconnect); 1680 | } 1681 | return subscription.endpoint; 1682 | }, 1683 | 1684 | /** 1685 | * Websocket raw trades 1686 | * @param {array/string} symbols - an array or string of symbols to query 1687 | * @param {function} callback - callback function 1688 | * @return {string} the websocket endpoint 1689 | */ 1690 | trades: function trades(symbols, callback) { 1691 | let reconnect = function () { 1692 | if (Huobi.options.reconnect) trades(symbols, callback); 1693 | }; 1694 | 1695 | let subscription; 1696 | if (Array.isArray(symbols)) { 1697 | if (!isArrayUnique(symbols)) throw Error('trades: "symbols" cannot contain duplicate elements.'); 1698 | let streams = symbols.map(function (symbol) { 1699 | return symbol.toLowerCase() + '@trade'; 1700 | }); 1701 | subscription = subscribeCombined(streams, callback, reconnect); 1702 | } else { 1703 | let symbol = symbols; 1704 | subscription = subscribe(symbol.toLowerCase() + '@trade', callback, reconnect); 1705 | } 1706 | return subscription.endpoint; 1707 | }, 1708 | 1709 | /** 1710 | * Websocket klines 1711 | * @param {array/string} symbols - an array or string of symbols to query 1712 | * @param {string} interval - the time interval 1713 | * @param {function} callback - callback function 1714 | * @param {int} limit - maximum results, no more than 1000 1715 | * @return {string} the websocket endpoint 1716 | */ 1717 | chart: function chart(symbols, interval, callback, limit = 500) { 1718 | let reconnect = function () { 1719 | if (Huobi.options.reconnect) chart(symbols, interval, callback, limit); 1720 | }; 1721 | 1722 | let symbolChartInit = function (symbol) { 1723 | if (typeof Huobi.info[symbol] === 'undefined') Huobi.info[symbol] = {}; 1724 | if (typeof Huobi.info[symbol][interval] === 'undefined') Huobi.info[symbol][interval] = {}; 1725 | if (typeof Huobi.ohlc[symbol] === 'undefined') Huobi.ohlc[symbol] = {}; 1726 | if (typeof Huobi.ohlc[symbol][interval] === 'undefined') Huobi.ohlc[symbol][interval] = {}; 1727 | if (typeof Huobi.ohlcLatest[symbol] === 'undefined') Huobi.ohlcLatest[symbol] = {}; 1728 | if (typeof Huobi.ohlcLatest[symbol][interval] === 'undefined') Huobi.ohlcLatest[symbol][interval] = {}; 1729 | if (typeof Huobi.klineQueue[symbol] === 'undefined') Huobi.klineQueue[symbol] = {}; 1730 | if (typeof Huobi.klineQueue[symbol][interval] === 'undefined') Huobi.klineQueue[symbol][interval] = []; 1731 | Huobi.info[symbol][interval].timestamp = 0; 1732 | } 1733 | 1734 | let handleKlineStreamData = function (kline) { 1735 | let symbol = kline.s; 1736 | if (!Huobi.info[symbol][interval].timestamp) { 1737 | if (typeof (Huobi.klineQueue[symbol][interval]) !== 'undefined' && kline !== null) { 1738 | Huobi.klineQueue[symbol][interval].push(kline); 1739 | } 1740 | } else { 1741 | //Binance.options.log('@klines at ' + kline.k.t); 1742 | klineHandler(symbol, kline); 1743 | if (callback) callback(symbol, interval, klineConcat(symbol, interval)); 1744 | } 1745 | }; 1746 | 1747 | let getSymbolKlineSnapshot = function (symbol, limit = 500) { 1748 | publicRequest(base + 'v1/klines', { symbol: symbol, interval: interval, limit: limit }, function (error, data) { 1749 | klineData(symbol, interval, data); 1750 | //Binance.options.log('/klines at ' + Binance.info[symbol][interval].timestamp); 1751 | if (typeof Huobi.klineQueue[symbol][interval] !== 'undefined') { 1752 | for (let kline of Huobi.klineQueue[symbol][interval]) klineHandler(symbol, kline, Huobi.info[symbol][interval].timestamp); 1753 | delete Huobi.klineQueue[symbol][interval]; 1754 | } 1755 | if (callback) callback(symbol, interval, klineConcat(symbol, interval)); 1756 | }); 1757 | }; 1758 | 1759 | let subscription; 1760 | if (Array.isArray(symbols)) { 1761 | if (!isArrayUnique(symbols)) throw Error('chart: "symbols" cannot contain duplicate elements.'); 1762 | symbols.forEach(symbolChartInit); 1763 | let streams = symbols.map(function (symbol) { 1764 | return symbol.toLowerCase() + '@kline_' + interval; 1765 | }); 1766 | subscription = subscribeCombined(streams, handleKlineStreamData, reconnect); 1767 | symbols.forEach(element => getSymbolKlineSnapshot(element, limit)); 1768 | } else { 1769 | let symbol = symbols; 1770 | symbolChartInit(symbol); 1771 | subscription = subscribe(symbol.toLowerCase() + '@kline_' + interval, handleKlineStreamData, reconnect); 1772 | getSymbolKlineSnapshot(symbol, limit); 1773 | } 1774 | return subscription.endpoint; 1775 | }, 1776 | 1777 | /** 1778 | * Websocket candle sticks 1779 | * @param {array/string} symbols - an array or string of symbols to query 1780 | * @param {string} interval - the time interval 1781 | * @param {function} callback - callback function 1782 | * @return {string} the websocket endpoint 1783 | */ 1784 | candlesticks: function candlesticks(symbols, interval, callback) { 1785 | let reconnect = function () { 1786 | if (Huobi.options.reconnect) candlesticks(symbols, interval, callback); 1787 | }; 1788 | 1789 | /* If an array of symbols are sent we use a combined stream connection rather. 1790 | This is transparent to the developer, and results in a single socket connection. 1791 | This essentially eliminates "unexpected response" errors when subscribing to a lot of data. */ 1792 | let subscription; 1793 | if (Array.isArray(symbols)) { 1794 | if (!isArrayUnique(symbols)) throw Error('candlesticks: "symbols" cannot contain duplicate elements.'); 1795 | let streams = symbols.map(function (symbol) { 1796 | return symbol.toLowerCase() + '@kline_' + interval; 1797 | }); 1798 | subscription = subscribeCombined(streams, callback, reconnect); 1799 | } else { 1800 | let symbol = symbols.toLowerCase(); 1801 | subscription = subscribe(symbol + '@kline_' + interval, callback, reconnect); 1802 | } 1803 | return subscription.endpoint; 1804 | }, 1805 | 1806 | /** 1807 | * Websocket mini ticker 1808 | * @param {function} callback - callback function 1809 | * @return {string} the websocket endpoint 1810 | */ 1811 | miniTicker: function miniTicker(callback) { 1812 | let reconnect = function () { 1813 | if (Huobi.options.reconnect) miniTicker(callback); 1814 | }; 1815 | let subscription = subscribe('!miniTicker@arr', function (data) { 1816 | let markets = {}; 1817 | for (let obj of data) { 1818 | markets[obj.s] = { 1819 | close: obj.c, 1820 | open: obj.o, 1821 | high: obj.h, 1822 | low: obj.l, 1823 | volume: obj.v, 1824 | quoteVolume: obj.q, 1825 | eventTime: obj.E 1826 | }; 1827 | } 1828 | callback(markets); 1829 | }, reconnect); 1830 | return subscription.endpoint; 1831 | }, 1832 | 1833 | /** 1834 | * Websocket prevday percentage 1835 | * @param {array/string} symbols - an array or string of symbols to query 1836 | * @param {function} callback - callback function 1837 | * @return {string} the websocket endpoint 1838 | */ 1839 | prevDay: function prevDay(symbols, callback) { 1840 | let reconnect = function () { 1841 | if (Huobi.options.reconnect) prevDay(symbols, callback); 1842 | }; 1843 | 1844 | let subscription; 1845 | // Combine stream for array of symbols 1846 | if (Array.isArray(symbols)) { 1847 | if (!isArrayUnique(symbols)) throw Error('prevDay: "symbols" cannot contain duplicate elements.'); 1848 | let streams = symbols.map(function (symbol) { 1849 | return symbol.toLowerCase() + '@ticker'; 1850 | }); 1851 | subscription = subscribeCombined(streams, function (data) { 1852 | prevDayStreamHandler(data, callback); 1853 | }, reconnect); 1854 | // Raw stream for a single symbol 1855 | } else if (symbols) { 1856 | let symbol = symbols; 1857 | subscription = subscribe(symbol.toLowerCase() + '@ticker', function (data) { 1858 | prevDayStreamHandler(data, callback); 1859 | }, reconnect); 1860 | // Raw stream of all listed symbols 1861 | } else { 1862 | subscription = subscribe('!ticker@arr', function (data) { 1863 | for (let line of data) { 1864 | prevDayStreamHandler(line, callback); 1865 | } 1866 | }, reconnect); 1867 | } 1868 | return subscription.endpoint; 1869 | } 1870 | } 1871 | }; 1872 | 1873 | }; 1874 | module.exports = api; 1875 | 1876 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-huobi-api", 3 | "version": "1.0.15", 4 | "description": "Node huobi API is an asynchronous node.js library for the huobi API designed to be easy to use.", 5 | "main": "node-huobi-api.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "test-debug": "mocha --inspect-brk", 9 | "lint": "eslint -c .eslintrc.js node-huobi-api.js test.js " 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/davewang/node-huobi-api.git" 14 | }, 15 | "keywords": [ 16 | "huobi", 17 | "api" 18 | ], 19 | "author": "NPM DaveWang User (http://www.iapploft.net)", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/davewang/node-huobi-api/issues" 23 | }, 24 | "homepage": "https://github.com/davewang/node-huobi-api#readme", 25 | "dependencies": { 26 | "dns-sync": "^0.1.3", 27 | "fs": "0.0.1-security", 28 | "https-proxy-agent": "^2.2.1", 29 | "pako": "^1.0.6", 30 | "request": "^2.85.0", 31 | "socks-proxy-agent": "^4.0.1", 32 | "string-hash": "^1.1.3", 33 | "url": "^0.11.0", 34 | "ws": "^4.1.0" 35 | }, 36 | "devDependencies": { 37 | "chai": "^4.1.2", 38 | "chai-counter": "^1.0.0", 39 | "codacy-coverage": "^2.1.1", 40 | "codecov": "^3.0.0", 41 | "coveralls": "^3.0.0", 42 | "eslint": "^4.19.1", 43 | "istanbul": "^0.4.5", 44 | "jsdoc": "^3.5.5", 45 | "mocha": "^5.2.0", 46 | "mocha-lcov-reporter": "^1.3.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const TIMEOUT = 10000; 2 | 3 | let chai = require( 'chai' ); 4 | let assert = chai.assert; 5 | let path = require( 'path' ); 6 | let huobi = require( path.resolve( __dirname, 'node-huobi-api.js' ) )(); 7 | let util = require( 'util' ); 8 | 9 | 10 | let logger = { 11 | log: function (msg){ 12 | let logLineDetails = ((new Error().stack).split('at ')[3]).trim(); 13 | let logLineNum = logLineDetails.split(':'); 14 | console.log('DEBUG', logLineNum[1] + ':' + logLineNum[2], msg); 15 | } 16 | } 17 | 18 | let debug = function( x ) { 19 | // if ( typeof ( process.env.node_huobi_api ) === 'undefined' ) { 20 | // return; 21 | // } 22 | logger.log( typeof ( x ) ); 23 | logger.log( util.inspect( x ) ); 24 | } 25 | 26 | debug('Begin'); 27 | 28 | /*global describe*/ 29 | /*eslint no-undef: "error"*/ 30 | describe( 'Construct', function() { 31 | /*global it*/ 32 | /*eslint no-undef: "error"*/ 33 | it( 'Construct the huobi object', function( done ) { 34 | huobi.options( { 35 | APIKEY: process.env.APIKEY, 36 | APISECRET:process.env.APISECRET, 37 | useServerTime: true, 38 | reconnect: true, 39 | verbose: true, 40 | log: debug 41 | } ); 42 | assert( typeof ( huobi ) === 'object', 'Huobi is not an object' ); 43 | done(); 44 | } ).timeout( TIMEOUT ); 45 | } ); 46 | 47 | describe( 'exchangeInfo', function() { 48 | it( 'Call exchangeInfo', function( done ) { 49 | huobi.exchangeInfo((error, data) => { 50 | assert(error === null); 51 | debug( error ); 52 | debug( data ); 53 | done(); 54 | }); 55 | }).timeout( TIMEOUT ); 56 | }); 57 | 58 | describe( 'account', function() { 59 | it( 'Call account', function( done ) { 60 | huobi.account((error, data) => { 61 | assert(error === null); 62 | debug( error ); 63 | debug( data ); 64 | done(); 65 | }); 66 | }).timeout( TIMEOUT ); 67 | }); 68 | 69 | describe( 'balance', function() { 70 | it( 'Call balance', function( done ) { 71 | huobi.account((error, data) => { 72 | assert(error === null); 73 | debug( error ); 74 | debug( data ); 75 | if (error == null){ 76 | huobi.balance(data.data[0].id,(error1,data1)=>{ 77 | debug( error ); 78 | debug( data ); 79 | done(); 80 | }); 81 | } 82 | }); 83 | }).timeout( TIMEOUT ); 84 | }); 85 | 86 | 87 | describe( 'time', function() { 88 | it( 'Call time', function( done ) { 89 | huobi.time((error, data) => { 90 | assert(error === null); 91 | debug( error ); 92 | debug( data ); 93 | done(); 94 | }); 95 | }).timeout( TIMEOUT ); 96 | }); 97 | 98 | 99 | describe( 'depthCache', function() { 100 | it( 'Call depthCache', function( done ) { 101 | const tickers=['xrpbtc', 'bchusdt']; 102 | huobi.websockets.depthCache( tickers,(symbol, depth) => { 103 | debug(symbol+'=='+ JSON.stringify( depth) ); 104 | 105 | },10); 106 | //done(); 107 | }).timeout( TIMEOUT ); 108 | }); 109 | 110 | describe( 'depthCacheStaggered', function() { 111 | it( 'Call depthCacheStaggered', function( done ) { 112 | const tickers=['xrpbtc', 'bchusdt']; 113 | huobi.websockets.depthCacheStaggered( tickers,(symbol, depth) => { 114 | debug(symbol+'=='+ JSON.stringify( depth) ); 115 | done(); 116 | },10); 117 | //done(); 118 | }).timeout( TIMEOUT ); 119 | }); 120 | --------------------------------------------------------------------------------