UNPKG

cossmmbot

Version:

Market Making bot for the coss.io exchange

1,909 lines (1,204 loc) 87.4 kB
const $tls = require("tls"); const $url = require("url"); const $https = require("https"); const $util = require("util"); const $crypto = require("crypto"); const $fs = require("fs"); const $events = require("events"); const $assert = require("assert").strict; var $private = ""; var $public = ""; class MMBot { /** * The main and unique class for market making on * the COSS plateform for a complete documentation * see : https://github.com/cyrus1996/CossMMBot */ constructor($spec,priv,pub){ $private = priv; $public = pub; /** * @var _spec <Object> * * * an Object that contains * every specification for every * pair that we want to trade on */ this._spec = {} /** * @var _ob_list <Object> * * created in => this.createOrderbook() * * an Object that stores all the * order books of the pairs we are * trading on */ this._ob_list = {} /** * @var _amount_min <Map> * * The map pair => min_amount that represents * the minimum amount to trade on the base pairs * (eg ETH: 0.02, COSS: 25) */ this._amount_min = new Map(); /** * @var _decimal <Map> * * the specification given by * the exchange for every pair * formed like this : * Pair => {price_decimal,amount_decimal} */ this._decimal = new Map() /** * @var _wallet <Object> * * created in => this.getWallet * * Stores your wallet specifications and amount */ this._wallet = {} /** * @var _callcounter <Int> * * the number of call done to the API * this variable allows us to prevent * reaching the API limit calls */ this._callcounter = 15; /** * @var _cookie <String> * * this variable is used to make the call * to the specific exchange url to cancel * all of open orders regardless of the pair * for more details see spec [] */ this._cookie = ""; /** * @var _kill <String> * * this variable is used to prevent * from opening new orders when the * exiting process started. */ this._kill = false; /** * @var $spec <String> * * this variable is used to store * our pair details for the order * opening process */ this._spec = $spec; /** * @var _error <Bool> * * this variable is used to * display a warning message if something * went wrong when cancelling the orders. */ this._error = false; this._exception = 0; this._exit = 0; this._interval = { "pair": [], "index": 0, "side": true, "lock": true } process.on("SIGINT", async () => { this._exit++; console.log("LOG: " + new Date().toUTCString() + " received SIGINT, starting the cancelling process after 12 sec (please wait for 12 sec)") if (this._exit >= 5) { console.log("\x1B[91;1mFATAL ERROR: " + new Date().toUTCString() + " >=5 ^C token received, process got killed, please check right now " + "the exchange to close any remaining order\x1B[0m"); process.exit(); } else if (this._exit > 1) { return true; } /** * if the ctrl-C token is sent to the * program we need to cancel all the open orders * to prevent any possible loss. */ /** * we set the _kill variable to * true, doing so will prevent new orders * to be opened */ this._kill = true; /** * before starting the * cancelling process we need to wait for at * least 10 sec because this is the minimum * lifetime of an opened order, cancelling an order * before 10 sec after opened it result in a soft ban * of the API for 5min. */ setTimeout(async () => { console.log("LOG: " + new Date().toUTCString() + " 12 sec elapsed starting cancelling process"); /** * if the cancelOnce method * worked we can safely exit the program */ console.log("LOG: " + new Date().toUTCString() + " trying to cancel all orders at once with cancelOnce method") if (await this.cancelOnce()) { console.log("\x1B[1mFINISH\x1B[0m: " + new Date().toUTCString() + " all orders have succefully been cancelled. Thanks for using me :)") process.exit(); } else { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " CancelOnce method returned false and didn't worked properly" + " starting to cancel every pair one at a time") /** * if the cancel once method didn't * worked we need to cancel orders on each * pair separately. */ for(var pair in this._spec){ /** * if the auto_kill attribute of the pair * is set to false we dont close the orders */ if(!this._spec[pair]["created"]) continue; if (!this._spec[pair]["auto_kill"]) { console.log("LOG: " + new Date().toUTCString() + " didn't cancel on " + pair + " cause on "+ pair +" : auto_kill = false"); continue; } /** * if the cancelAllOrders method returned false * this means we reached our api limits calls * so we need to wait for 1 minute in order to * reset our api limit calls. */ console.log("LOG: " + new Date().toUTCString() + " start using cancelAllOrders on " + pair); if(!await this.cancelAllOrders(pair)){ var error = await this.timerCancelAll(pair); !error ? this._error = true : false; } } if (this._error) { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " All orders weren't cancelled properly log " + "in the plateform and manually close them"); process.exit() } else { console.log("\x1B[1mFINISH\x1B[0m: " + new Date().toUTCString() + " all orders have succefully been cancelled. Thanks for using me :)") process.exit(); } } },12000) }) process.on("uncaughtException", async (err) => { /** * Similar to what's above. */ this._kill = true; this._exception++; console.log("\x1B[91;1mERROR\x1B[0m:" + new Date().toUTCString() + " the error which stopped the program : ", err); if (this._exception > 1) { console.log("\x1B[91;1mFATAL ERROR: " + new Date().toUTCString() + " An error occured during error handling, process got killed, please check right now " + "the exchange to close any remaining order\x1B[0m"); process.exit(); } setTimeout(async () => { console.log("LOG: " + new Date().toUTCString() + " trying to cancel all orders at once with cancelOnce method"); if (await this.cancelOnce()) { console.log("\x1B[1mFINISH\x1B[0m: " + new Date().toUTCString() + " all orders have succefully been cancelled. Thanks for using me :)") process.exit(); } else { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " CancelOnce method returned false and didn't worked properly" + " starting to cancel every pair one at a time"); for(var pair in this._spec){ if(!this._spec[pair]["created"]) continue; console.log("LOG: " + new Date().toUTCString() + " start using cancelAllOrders on " + pair); if(!await this.cancelAllOrders(pair)){ var error = await this.timerCancelAll(pair); !error ? this._error = true : false; } } if (this._error) { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " All orders weren't cancelled properly log " + "in the plateform and manually close them"); process.exit() } else { console.log("\x1B[1mFINISH\x1B[0m: " + new Date().toUTCString() + " all orders have succefully been cancelled. Thanks for using me :)") process.exit(); } } },12000) }) process.on("unhandledRejection", async (err) => { /** * Similar to what's above. */ this._kill = true; this._exception++; console.log("\x1B[91;1mERROR\x1B[0m:" + new Date().toUTCString() + " the error which stopped the program : ", err); if (this._exception > 1) { console.log("\x1B[91;1mFATAL ERROR: " + new Date().toUTCString() + " An error occured during error handling, process got killed, please check right now " + "the exchange to close any remaining order\x1B[0m"); process.exit(); } setTimeout(async () => { console.log("LOG: " + new Date().toUTCString() + " trying to cancel all orders at once with cancelOnce method"); if (await this.cancelOnce()) { console.log("\x1B[1mFINISH\x1B[0m: " + new Date().toUTCString() + " all orders have succefully been cancelled. Thanks for using me :)") process.exit(); } else { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " CancelOnce method returned false and didn't worked properly" + " starting to cancel every pair one at a time"); for(var pair in this._spec){ if(!this._spec[pair]["created"]) continue; if(!await this.cancelAllOrders(pair)){ console.log("LOG: " + new Date().toUTCString() + " start using cancelAllOrders on " + pair); var error = await this.timerCancelAll(pair); !error ? this._error = true : false; } } if (this._error) { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " All orders weren't cancelled properly log " + "in the plateform and manually close them"); process.exit() } else { console.log("\x1B[1mFINISH\x1B[0m: " + new Date().toUTCString() + " all orders have succefully been cancelled. Thanks for using me :)") process.exit(); } } },12000) }) process.stdin.resume(); /** * this section allows us to use * a "secret" api call in order to close * all the orders at once. */ process.stdin.on("data", async (data) => { data = data.toString().replace("\n","").replace("\r",""); if (data.includes("->")) { data = data.split("->"); if (data[0] == "open") { var json = JSON.parse(data[1]); await this.getWallet(1); for (var pair in json){ if (!this._spec[pair]) { this._spec[pair] = json[pair]; await this.createSocket(pair); this._interval["pair"].push(pair); } } } else if (data[0] == "close") { this._interval["pair"].splice(this._interval["pair"].indexOf(data[1]),1); clearInterval(this._spec[data[1]]['pong']); clearInterval(this._spec[data[1]]['timer']); this._spec[data[1]]['socket'].removeAllListeners("data").removeAllListeners("error"); setTimeout(async () => { await this.cancelAllOrders(data[1]); delete this._spec[data[1]]; console.log("LOG: " + new Date().toUTCString() + " orders cancelled on " + data[1]); },12000) } } else { this._cookie = data; } }); setInterval(async() => { await this.getWallet(1); },300000); /** * this interval is used in order * to keep our calls below 1000 calls/minute */ setInterval(async() => { this._callcounter -= 15; this._callcounter < 0 ? this._callcounter = 0 : false; },900) setInterval(async () => { if (this._interval["pair"].length && this._interval["lock"]) { var champ = this._interval["side"] ? "asks" : "bids"; this._interval["lock"] = false; if(this._interval["pair"][this._interval["index"]] && this._spec[this._interval["pair"][this._interval["index"]]]["orderbook"]) await this.checkOne(this._interval["pair"][this._interval["index"]],this._interval["side"]); this._interval["side"] = !this._interval["side"]; this._interval["side"] ? this._interval["index"]++ : false; this._interval["index"] = this._interval["index"] >= this._interval["pair"].length ? 0 : this._interval["index"]; this._interval["lock"] = true; } },20000) /** * Populates our wallet * and make the rest of the * function asychronously */ this.getWallet().then(async(value) => { await this.getExchangeInfo(); for(var pair in this._spec){ await this.createSocket(pair); this._interval["pair"].push(pair); } this._start = true; }) } /** * @param ref <Int> * * this function is used if _cookie * was provided to cancel all the orders * at once */ async cancelOnce(ref = 0){ if (!this._cookie) { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " _cookie variable is set to false, " + "can't use cancelOnce method returning false"); return false; } else { return new Promise((resolve,reject) => { var options = { method: "DELETE", port: 443, header: { "Host": "coss.io", "Cookie": this._cookie } } var req = $https.request("https://coss.io/c/order/cancel_all",options, async res => { if (res.statusCode == 302) { resolve(false); } else { resolve(true); } }) req.on("error", async (err) => { console.log("\x1B[91mNOTICE\x1B[0m: " + new Date().toUTCString() + " an error occured during CancelOnce method err : " + err); resolve(false); }) req.end(); }) } } /** * @param pair <String> * * this function return the list * of the open orders for the given pair */ async getOpenOrders($pair,ref = 0){ this._callcounter++; return new Promise(async (resolve,reject) => { var $date = new Date().getTime() - 3000; var $response = ""; var $data = '{"symbol": '+ $pair +',"timestamp": '+ $date +',"recvWindow": 5000}'; var hmac = $crypto.createHmac("sha256",$private); hmac.update($data); var options = { "hostname": "trade.coss.io", "port": 443, "path": "/c/api/v1/order/list/open", "method": "POST", "headers": { "Host": "trade.coss.io", "Content-Type": "application/json", "Content-Length": $data.length, "Authorization": $public, "Signature": hmac.digest("hex") } } var req = $https.request(options, async (res) => { res.on("data", async (chunk) => {$response += chunk.toString(); }); res.on("end", async () => { try{ $response = JSON.parse($response); if (!$response["list"]) { throw new Error("unexpected response"); } $response = $response["list"].map(async(value) => { return value["order_id"]; }) $response = await Promise.all($response); console.log("LOG: " + new Date().toUTCString() + " list of open orders OK "); resolve($response); } catch(e){ var wait = Math.round(Math.random() * 800 + 600) var timer = setTimeout(async () => { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " something caused an error while getting " + " open orders on: " + pair + " trying to get the list again, Error: " + e); resolve(await this.getOpenOrders($pair, ++ref)); },wait); if (ref > 4) { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + "maximum number of try reached on " + pair + " orders wont be claused please close them manually. Error" + e); clearTimeout(timer); resolve(false); } } }); }) req.on("error", async(e) => { var wait = Math.round(Math.random() * 800 + 600) if (ref > 4) { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + "maximum number of try reached on " + pair + " orders wont be claused please close them manually. Error" + e); resolve(false) } setTimeout(async () => { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " an error occured on " + pair + " while getting the list of open orders. Error: " + e); resolve(await this.getOpenOrders($pair, ++ref)); },wait); }); req.end($data); }); } /** * @param pair <String> * * this is the main function for * cancelling all the orders for the given * pair, it uses several sub-functions. */ async cancelAllOrders(pair){ return new Promise(async(resolve,reject) =>{ console.log("LOG: " + new Date().toUTCString() + " in cancelAllOrders waiting for getOpenOrders on pair: " + pair); var liste = await this.getOpenOrders(pair); if (!liste) { this._error = true; resolve(false) } if (this._callcounter + liste.length < 700) { console.log("LOG: " + new Date().toUTCString() + " Ok for our API limit calls requests "); for(var x of liste){ await this.cancelOne(x,pair); } resolve(true); } else { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " broke our API limit requests waiting for 1 minute "); resolve(false); } }) } /** * @param pair <String> * * this function waits for one minute before * starting to cancel new orders. */ async timerCancelAll(pair){ return new Promise((resolve,reject) => { setTimeout(async() => { console.log("LOG: " + new Date().toUTCString() + "timer of 1 minute elapsed " + pair); await this.cancelAllOrders(pair); resolve(true); },65000) }) } /** * @param id <String> the id of the pair * @param pair <String> */ async cancelOne(id,pair,ref = 0){ this._callcounter++; return new Promise(async (resolve,reject) => { var $response = ""; var $date = new Date().getTime() - 3000; var $data = '{"order_symbol": '+ pair +',"timestamp": '+ $date +',"recvWindow": 5000,"order_id": ' + id + '}'; var hmac = $crypto.createHmac("sha256",$private); hmac.update($data); var options = { "hostname": "trade.coss.io", "method": "DELETE", "port": 443, "path": "/c/api/v1/order/cancel", "headers": { "Content-Type": "application/json", "Content-Length": $data.length, "Authorization": $public, "Signature": hmac.digest("hex") } } var req = $https.request(options,async(res) => { res.on("data", async (chunk) => {$response += chunk.toString(); }); res.on("end", async () => { try{ $response = JSON.parse($response); if (!$response["order_id"]) { throw new Error("unexpected response"); } else { resolve($response); } } catch(e){ var wait = Math.round(Math.random() * 800 + 600) var timer = setTimeout(async () => { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " an order cancelling went wrong, retrying on: " + pair + " if no warning follows this message the order got cancelled " + ". Error: " + e); resolve(await this.cancelOne(id,pair, ++ref)); },wait); if (ref > 4) { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " an order didn't got cancelled please " + " cancel it manually on the exchange on " + pair + ". Error: " + e); this._error = true; clearTimeout(timer); resolve(false); } } }); }); req.on("error", async(e) => { var wait = Math.round(Math.random() * 800 + 600) if (ref > 4) { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " an order didn't got cancelled please " + " cancel it manually on the exchange on " + pair + ". Error: " + e); resolve(false) } setTimeout(async () => { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " an error occured during the request " + " for cancelling an order on " + pair + ". Error: " + e); resolve(await this.cancelOne(id,pair, ++ref)); },wait); }); req.end($data); }) } /** * @params Ø * * @return Promise * * This function returns our wallet * and stores the results in the _wallet variable */ async getWallet(ref = 0){ console.log("LOG: " + new Date().toUTCString() + " getting our wallet "); return new Promise((resolve,reject) => { var $time = new Date().getTime() - 3000; var hmac = $crypto.createHmac("sha256",$private) var $payload = "recvWindow=5000&timestamp=" + $time; hmac.update($payload); var options = { host: "trade.coss.io", path: "/c/api/v1/account/balances?" + $payload, headers: { "Host": "trade.coss.io", "Authorization": $public, "Signature": hmac.digest("hex") } } $https.get(options,async (res) => { var $data = ""; res.on("data", async (chunk) => { $data += chunk.toString(); }); res.on("end", async () => { try{ let data = JSON.parse($data); data.forEach((value) => { this._wallet[value['currency_code']] = parseFloat(value['available']); }); console.log("LOG: " + new Date().toUTCString() + " succeed getting wallet "); resolve(true); } catch(err){ if (ref == 0) { console.log("\x1B[91;1mFATAL ERROR: " + new Date().toUTCString() + " An error occured while getting the wallet amounts " + "this cant allow us to continue exiting\x1B[0m ", err); process.exit(); } else { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " Unable to retrive Wallet informations try again"); resolve(true); } } }); res.on("error", async err => { if (ref == 0) { console.log("\x1B[91;1mFATAL ERROR: " + new Date().toUTCString() + " An error occured while getting the wallet amounts " + "this cant allow us to continue exiting\x1B[0m ", err); process.exit(); } else { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " Unable to retrive Wallet informations try again"); resolve(true); } }) }); }); } /** * @params none * * This function is used to retrieve * infomations about the pairs on the * exchange * * @return Promise */ async getExchangeInfo(){ return new Promise((resolve,rej) => { $https.get("https://trade.coss.io/c/api/v1/exchange-info", (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { try{ data = JSON.parse(data); data['base_currencies'].forEach((value) => { this._amount_min.set(value['currency_code'],value['minimum_total_order']); }); data['symbols'].forEach((value) => { this._decimal.set(value['symbol'],{amount_decimal: value['amount_limit_decimal'],price_decimal: value['price_limit_decimal']}); }); resolve(true); }catch (err){ console.log("\x1B[91;1mFATAL ERROR: " + new Date().toUTCString() + " An error occured while getting exchange specification " + "this cant allow us to continue, exiting\x1B[0m ", err); process.exit(); } }); res.on("error", async err =>{ console.log("\x1B[91;1mFATAL ERROR: " + new Date().toUTCString() + " An error occured while getting exchange specification " + "this can't allow us to continue, exiting\x1B[0m ", err); process.exit(); }) }); }); } /** * @param $pair <string> * * Creates a new connection on a specified * websocket on the exchange * in order to listen for data and update our * internal orderbook */ async createSocket($pair,ref = 0){ if (!this._spec[$pair]["created"]) { this._spec[$pair]["poll"] = []; this._spec[$pair]["a_id"] = {}; this._spec[$pair]["b_id"] = {}; this._spec[$pair]["a_time"] = {}; this._spec[$pair]["b_time"] = {}; /** * thoses are variables used if we set overflow * to false and reach our order price bourdary */ this._spec[$pair]["reminder_asks"] = false; this._spec[$pair]["reminder_bids"] = false; } this._spec[$pair]["lock"] = true; if(this._kill) return true; this._spec[$pair]["events"] = new $events; this._spec[$pair]["events"].once("finish", async() => { var list = Array.from(this._spec[$pair]["poll"]); this._spec[$pair]["poll"] = []; await this.updateStripes(list,$pair); }) this._spec[$pair]['pong'] = 0; this._spec[$pair]['timer'] = 0; if(!this._decimal.has($pair)){ console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " " + $pair + " doesnt exists"); resolve(false); } console.log("LOG: " + new Date().toUTCString() + " opening socket on " + $pair); return new Promise((resolve,reject) => { this._spec[$pair]['socket'] = $tls.connect(443,"engine.coss.io", async ()=>{ this._spec[$pair]['socket'].write("GET /ws/v1/dp/" + $pair +" HTTP/1.1\r\n" + "Host: engine.coss.io\r\n" + "Accept: */*\r\nConnection: Upgrade\r\n" + "Upgrade: websocket\r\nSec-WebSocket-Version: 13\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"); /** * If we receive a wrong response we reopen * a new socket on this pair until the connection * is ready. */ this._spec[$pair]['socket'].once("data",async (data) => { if(this._kill) return true; if (data.toString().indexOf("101") != -1) { console.log("LOG: " + new Date().toUTCString() + " connected on : " + $pair); /** * Pong frames according to RFC 6455 */ this._spec[$pair]['pong'] = setInterval(() => { this._spec[$pair]["socket"].write(Buffer.from([0x8A,0x80,0x77,0x77,0x77,0x77])); },20000); this._spec[$pair]['timer'] = setInterval(async() => { if (this._spec[$pair]["created"] && this._spec[$pair]["lock"] && this._spec[$pair]["poll"].length) { var tempo = Array.from(this._spec[$pair]["poll"]); this._spec[$pair]["poll"] = []; await this.updateStripes(tempo,$pair); } },250) /** * Before listening to the data * we have to create the order book in * order to prevent missing any data frame * from the websocket. */ var $ob = await this.createOrderbook($pair); /** * If we failed to open the order book * we do not do any further actions * because this would result in errors */ if (!$ob) { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " no orders will be opened on " + $pair + " cause a problem occured while creating order book"); clearInterval(this._spec[$pair]['pong']); clearInterval(this._spec[$pair]['timer']); this._spec[$pair]['socket'].removeAllListeners("data").removeAllListeners("error"); resolve(false); } /** * Once We've created our order book and our socket is * connected we add our * orders for the market making. If the orders have already been * created created would be set to true so it would be useless to create stripes * once again. */ var response = this._spec[$pair]["created"] ? false : await this.createStripes($pair); console.log("LOG: " + new Date().toUTCString() + " stripes created on: " + $pair); this._spec[$pair]["events"].emit("finish"); this._spec[$pair]["created"] = true; resolve(response); } else { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " trying to open socket again on : " + $pair); this.createSocket($pair).then((value) => { resolve(value); }); } }) /** * If an error occurs on our websocket * we just reopen it. But we need to clear * every thing before. */ this._spec[$pair]['socket'].on("error", async (err) => { if (ref > 3) { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " too many errors on "+ $pair + " socket we will disable it. [Error] : " + err); clearInterval(this._spec[$pair]['pong']); clearInterval(this._spec[$pair]['timer']); this._spec[$pair]['socket'].removeAllListeners("data").removeAllListeners("error"); } else { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " an error occured on socket "+ $pair +" we will try "+ "we will try to open it again error : " + err); clearInterval(this._spec[$pair]['pong']); clearInterval(this._spec[$pair]['timer']); this._spec[$pair]['socket'].removeAllListeners("data").removeAllListeners("error"); await this.createSocket($pair,++ref); } }) this._spec[$pair]['socket'].on("data",async (data) => { if (this._kill) { return false} /** * We parse our data and update our orderbooks */ var $data = await this.parseData(data); if ($data) { for(var x of $data){ if (x["a"].length) { console.log("LOG: " + new Date().toUTCString() + " data received on " + $pair + " asks[price]: " + x['a'][0] + " asks[quantity]: " + x['a'][1] + " frame timestamp: " + x['t']); } else if (x["b"].length) { console.log("LOG: " + new Date().toUTCString() + " data received on " + $pair + " bids[price]: " + x['b'][0] + " bids[quantity]: " + x['b'][1] + " frame timestamp: " + x['t']); } if (x["a"][1] == 0) { this._spec[$pair]["poll"].push(x); } else if (x["b"][1] == 0) { this._spec[$pair]["poll"].push(x); } else if (this._spec[$pair]["orderbook"] && this._spec[$pair]["orderbook"]["asks"].includes(parseFloat(x["a"][0]))) { await this.checkTest($pair,parseFloat(x["a"][0]),"a_id") } else if (this._spec[$pair]["orderbook"] && this._spec[$pair]["orderbook"]["bids"].includes(parseFloat(x["b"][0]))) { await this.checkTest($pair,parseFloat(x["b"][0]),"b_id") } } } }); }); }); } /** * @param pair <String> * @param ref <Int> * @return Promise * * this function creates and store the order book for the * specified pair, the ref variable is used to count if * too much calls fail the opening on the pair is aborted */ async createOrderbook(pair,ref = 0){ this._callcounter++; if (this._kill) { return false} return new Promise((resolve,rej) => { $https.get("https://engine.coss.io/api/v1/dp?symbol="+pair, (res) => { var donnes = ""; res.on("data",async (chunk) => { donnes += chunk; }); res.on("end",async () => { try { donnes = JSON.parse(donnes); $assert.ok(donnes["asks"] && donnes["bids"],"We must have at least one bid and one ask on the order book"); this._ob_list[pair] = donnes; console.log("LOG: " + new Date().toUTCString() + " orderbook created on " + pair); resolve(donnes); } catch(e){ ref > 3 ? resolve(false): false; setTimeout(async() => { console.log("\x1B[38;5;226mNOTICE\x1B[0m: " + new Date().toUTCString() + " trying to open orderbook again on: " + pair + " the error we caught: " + err); resolve(await this.createOrderbook(pair,++ref)) },750); } }); }).on("error", async err => { ref > 3 ? resolve(false): false; setTimeout(async() => { console.log("\x1B[91mWARNING\x1B[0m: " + new Date().toUTCString() + " trying to open orderbook again on: " + pair + " the error we caught: " + err); resolve(await this.createOrderbook(pair,++ref)); },750); }); }); } /** * @param data <String> * * Get the raw data as input and returns * a Promise containing the data in JSON */ async parseData(data){ if (data.length > 50 && data.indexOf("Server") == -1) { data = data.toString("ascii").match(/\{.+?\}/g); var $data = data.map(async (value) => { return JSON.parse(value.replace("[[","[").replace("]]","]")); }); return Promise.all($data); } else { return false; } } /** * @params data <array> A Json array containing the formatted * data to analyse * * @params pair <string> The pair corresponding to those data * * Check the data that are received if a quantity is set to 0 * we check if this quantity were part of our stripes * if it is the case we have to update our stripes */ async updateStripes(valuee,pair){ this._spec[pair]["lock"] = false; var $pairs = pair.split("_"); var final = []; var changes = false; /** * Once we receive data, we check * every frame from the websocket * to check if we received data * that includes orders that we have * opened. */ for(var data of valuee){ changes = false; if (data['a'].length) { /** * if the data is about an ask order * that we receive, we check if this a 0 quantity * update which could mean some orders have been bought. */ if (data['a'][1] == 0 && this._spec[pair]["orderbook"]["asks"].includes(parseFloat(data['a'][0]))) { for (var val of this._spec[pair]["orderbook"]["asks"]){ /** * if we receive a 0 quantity data update, we have to check * weather we have an order at such price, if this is the case, * this means an order have been excecuted. */ if (data['a'][0] == val && parseFloat(data['t']) >= this._spec[pair]["a_time"][parseFloat(val)]) { console.log("LOG: " + new Date().toUTCString() + " usefull data received on " + pair + " asks[price]: " + data['a'][0] + " asks[quantity]: " + data['a'][1] + " frame timestamp: " + data['t']); /** * if one of our asks order has been excuted we have * to rise our best bid order in order to still cover the * order book. so we calculate our new bid by multiply our highest * bid by the profit we want to take. */ changes = true; var price = this._spec[pair]["orderbook"]["bids"][0] * (this._spec[pair]["profit"] / 100 + 1); var price_ceil = await this._ceil(price,this._decimal.get(pair)["price_decimal"]); price = val / price_ceil >= (this._spec[pair]["profit"] / 100 + 1) ? price_ceil : await this._floor(price,this._decimal.get(pair)["price_decimal"]); while (val / price < (this._spec[pair]["profit"] / 100 + 1)) { price = (price * 10 ** this._decimal.get(pair)["price_decimal"] - 1) / 10 ** this._decimal.get(pair)["price_decimal"]; } /** * we get the quantity that we should put for this new order. */ var amount = await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],price,pair); if (this._spec[pair]["allow_overflow"]) { /** * In the case we allowed overflowding, we keep * the same number of order opened at each time, * so if one order got excecuted we have to add one * above our highest ask in this case, see docs for more details. * spec []; */ /** * what ever happens if we have one order exceuted * we have to update our wallet. */ if (this._spec[pair]["orderbook"]["asks"].length <= 1) { /** * if we are over our range limit we just update our wallet * but nothing else. */ if (!this._spec[pair]["reminder_asks"]) { this._wallet[$pairs[1]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],val,pair) * val; } this._spec[pair]["reminder_asks"] = val } else { /** * if we are in the range we specified, we get the quantity and * the price of the order we want to open and we open it. */ this._wallet[$pairs[1]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],val,pair) * val; this._spec[pair]["orderbook"]["asks"].splice(this._spec[pair]["orderbook"]["asks"].indexOf(val),1); delete this._spec[pair]["a_time"][parseFloat(val)]; delete this._spec[pair]["a_id"][parseFloat(val)]; if(!this._spec[pair]["orderbook"]["bids"].includes(price) && this._spec[pair]["orderbook"]["asks"][0] / price >= (this._spec[pair]["profit"] / 100 * 2 + 1) && await this.openOrder(price,amount,"BUY",pair)) this._spec[pair]["orderbook"]["bids"].unshift(price); } if (this._spec[pair]["orderbook"]["asks"].length <= this._spec[pair]["orderbook"]["asks_length"] && this._spec[pair]["orderbook"]["asks"].length > 1) { /** * we only want to have the same number of order as the moment * we set up our stripes, so if we have enough opened orders * it is not necessary to open more orders. */ /** * price_new, give us our new highest ask in order to still cover all the order book. */ var price_new = await this._ceil(this._spec[pair]["orderbook"]["asks"][this._spec[pair]["orderbook"]["asks"].length - 1] * (this._spec[pair]["profit"] / 100 + 1),this._decimal.get(pair)["price_decimal"]); var amount_new = await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],price_new,pair); if(await this.openOrder(price_new,amount_new,"SELL",pair)) this._spec[pair]["orderbook"]["asks"].push(price_new); } break; } else { /** * in the case we don't allow overfloating, we dont want to open * more orders, we just want to cover the range we gave to the * algorythm. */ if (this._spec[pair]["orderbook"]["asks"].length <= 1) { /** * if we are over our range limit we just update our wallet * but nothing else. */ if (!this._spec[pair]["reminder_asks"]) { this._wallet[$pairs[1]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],val,pair) * val; } this._spec[pair]["reminder_asks"] = val break; } else { /** * if we are in the range we specified, we get the quantity and * the price of the order we want to open and we open it. */ this._wallet[$pairs[1]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],val,pair) * val; delete this._spec[pair]["a_time"][parseFloat(val)]; delete this._spec[pair]["a_id"][parseFloat(val)]; this._spec[pair]["orderbook"]["asks"].splice(this._spec[pair]["orderbook"]["asks"].indexOf(val),1); if(!this._spec[pair]["orderbook"]["bids"].includes(price) && this._spec[pair]["orderbook"]["asks"][0] / price >= (this._spec[pair]["profit"] / 100 * 2 + 1) && await this.openOrder(price,amount,"BUY",pair)) this._spec[pair]["orderbook"]["bids"].unshift(price); break; } } } } } /** * the below for loop, will most of the time, * remain usefull, however, in really rare occasion, * it will keep your orders on track, especially * when we receive multiples frames in a really short * period of time, or if we get the frames in the wrong order * but this should not happen, according to the TCP * protocol. */ if (changes && this._spec[pair]["force_liquidity"]) { await this.updateWhileAsk(pair,data); } if (changes == true) { console.log("LOG: " + new Date().toUTCString() + " pair: ",pair," order asks",this._spec[pair]["orderbook"]["asks"]); console.log("LOG: " + new Date().toUTCString() + " pair: ",pair," order bids",this._spec[pair]["orderbook"]["bids"]); console.log("LOG: " + new Date().toUTCString() + " wallet on " + $pairs[0] + ": ",this._wallet[$pairs[0]]); console.log("LOG: " + new Date().toUTCString() + " wallet on " + $pairs[1] + ": ",this._wallet[$pairs[1]]); } } else if (data['b'].length) { if (data['b'][1] == 0 && this._spec[pair]["orderbook"]["bids"].includes(parseFloat(data['b'][0]))) { for(var valeur of this._spec[pair]["orderbook"]["bids"]){ if (data['b'][0] == valeur && parseFloat(data['t']) >= this._spec[pair]["b_time"][parseFloat(valeur)]) { console.log("LOG: " + new Date().toUTCString() + " usefull data received on " + pair + " bids[price]: " + data['b'][0] + " bids[quantity]: " + data['b'][1] + " frame timestamp: " + data['t']); changes = true; var price = this._spec[pair]["orderbook"]["asks"][0] / (this._spec[pair]["profit"] / 100 + 1); var price_floor = await this._floor(price,this._decimal.get(pair)["price_decimal"]); price = price_floor / valeur >= (this._spec[pair]["profit"] / 100 + 1) ? price_floor : await this._ceil(price,this._decimal.get(pair)["price_decimal"]); while (price / valeur < (this._spec[pair]["profit"] / 100 + 1)) { price = (price * 10 ** this._decimal.get(pair)["price_decimal"] + 1) / 10 ** this._decimal.get(pair)["price_decimal"]; } var amount = await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],price,pair); if (this._spec[pair]["allow_overflow"]) { if (this._spec[pair]["orderbook"]["bids"].length <= 1) { if (!this._spec[pair]["reminder_bids"]) { this._wallet[$pairs[0]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],valeur,pair); } this._spec[pair]["reminder_bids"] = valeur } else { this._wallet[$pairs[0]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],valeur,pair); this._spec[pair]["orderbook"]["bids"].splice(this._spec[pair]["orderbook"]["bids"].indexOf(valeur),1); delete this._spec[pair]["b_time"][parseFloat(valeur)]; delete this._spec[pair]["b_id"][parseFloat(val)]; if(!this._spec[pair]["orderbook"]["asks"].includes(price) && price / this._spec[pair]["orderbook"]["bids"][0] >= (this._spec[pair]["profit"] / 100 * 2 + 1) && await this.openOrder(price,amount,"SELL",pair))this._spec[pair]["orderbook"]["asks"].unshift(price); } if (this._spec[pair]["orderbook"]["bids"].length <= this._spec[pair]["orderbook"]["bids_length"] && this._spec[pair]["orderbook"]["bids"].length > 1) { var price_new = await this._floor(this._spec[pair]["orderbook"]["bids"][this._spec[pair]["orderbook"]["bids"].length - 1] / (this._spec[pair]["profit"] / 100 + 1),this._decimal.get(pair)["price_decimal"]); var amount_new = await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],price_new,pair); if(await this.openOrder(price_new,amount_new,"BUY",pair))this._spec[pair]["orderbook"]["bids"].push(price_new); } break; } else { if (this._spec[pair]["orderbook"]["bids"].length <= 1) { if (!this._spec[pair]["reminder_bids"]) { this._wallet[$pairs[0]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],valeur,pair); } this._spec[pair]["reminder_bids"] = valeur; break; } else { this._wallet[$pairs[0]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],valeur,pair); this._spec[pair]["orderbook"]["bids"].splice(this._spec[pair]["orderbook"]["bids"].indexOf(valeur),1); delete this._spec[pair]["b_time"][parseFloat(valeur)]; delete this._spec[pair]["b_id"][parseFloat(val)]; if(!this._spec[pair]["orderbook"]["asks"].includes(price) && price / this._spec[pair]["orderbook"]["bids"][0] >= (this._spec[pair]["profit"] / 100 * 2 + 1) && await this.openOrder(price,amount,"SELL",pair))this._spec[pair]["orderbook"]["asks"].unshift(price); break; } } } } } if (changes && this._spec[pair]["force_liquidity"]) { await this.updateWhileBid(pair,data); } if (changes == true) { console.log("LOG: " + new Date().toUTCString() + " pair: ",pair," order asks",this._spec[pair]["orderbook"]["asks"]); console.log("LOG: " + new Date().toUTCString() + " pair: ",pair," order bids",this._spec[pair]["orderbook"]["bids"]); console.log("LOG: " + new Date().toUTCString() + " wallet on " + $pairs[0] + ": ",this._wallet[$pairs[0]]); console.log("LOG: " + new Date().toUTCString() + " wallet on " + $pairs[1] + ": ",this._wallet[$pairs[1]]); } } } this._spec[pair]["lock"] = true; return true; } async updateSubAsks(data,pair,$pairs){ return new Promise(async (resolve,reject) => { if (Math.max(...this._spec[pair]["orderbook"]["bids"]) < data['a'][0]) { resolve(false); } else { var final = []; for(var value of this._spec[pair]["orderbook"]["bids"]){ /** * if the frames specifies an ask that is * below our highest bid this mean we had * orders excecuted but we didn't got those frames. * so we have to do a similar work as above. */ if (data['a'][0] <= value) { console.log("LOG: " + new Date().toUTCString() + " weird usefull data received on " + pair + " asks[price]: " + data['a'][0] + " asks[quantity]: " + data['a'][1]); /** * if the ask is below one of our * bids we have to open a new ask order. * so we get the quantity and the price. */ var price = await this._floor(this._spec[pair]["orderbook"]["asks"][0] / (this._spec[pair]["profit"] / 100 + 1),this._decimal.get(pair)["price_decimal"]); var amount = await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],price,pair); if (this._spec[pair]["allow_overflow"]) { if (this._spec[pair]["orderbook"]["bids"].length <= 1) { if (!this._spec[pair]["reminder_bids"]) { this._wallet[$pairs[0]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],value,pair); } this._spec[pair]["reminder_bids"] = value; final.push(value); } else { this._wallet[$pairs[0]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],value,pair); if(await this.openOrder(price,amount,"SELL",pair))this._spec[pair]["orderbook"]["asks"].unshift(price); } var price_new = await this._ceil(this._spec[pair]["orderbook"]["bids"][this._spec[pair]["orderbook"]["bids"].length - 1] / (this._spec[pair]["profit"] / 100 + 1),this._decimal.get(pair)["price_decimal"]); if (this._spec[pair]["orderbook"]["bids"].length <= this._spec[pair]["orderbook"]["bids_length"]) { var amount_new = await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],price_new,pair); if(await this.openOrder(price_new,amount_new,"BUY",pair))this._spec[pair]["orderbook"]["bids"].push(price_new); } } else { if (this._spec[pair]["orderbook"]["bids"].length <= 1) { if (!this._spec[pair]["reminder_bids"]) { this._wallet[$pairs[0]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],value,pair); } this._spec[pair]["reminder_bids"] = value; final.push(value); } else { this._wallet[$pairs[0]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],value,pair); if(await this.openOrder(price,amount,"SELL",pair))this._spec[pair]["orderbook"]["asks"].unshift(price); } } } else { final.push(value); } } this._spec[pair]["orderbook"]["bids"] = final; resolve(true); } }) } async updateSubBids(data,pair,$pairs){ return new Promise(async (resolve,reject) => { if (Math.min(...this._spec[pair]["orderbook"]["asks"]) > data['b'][0]) { resolve(false); } else { var final = []; for(var value of this._spec[pair]["orderbook"]["asks"]){ if (data['b'][0] >= value) { var price = await this._ceil(this._spec[pair]["orderbook"]["bids"][0] * (this._spec[pair]["profit"] / 100 + 1),this._decimal.get(pair)["price_decimal"]); var amount = await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],price,pair); if (this._spec[pair]["allow_overflow"]) { if (this._spec[pair]["orderbook"]["asks"].length <= 1) { if (!this._spec[pair]["reminder_asks"]) { this._wallet[$pairs[1]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],value,pair) * value; } final.push(value); this._spec[pair]["reminder_asks"] = value } else { this._wallet[$pairs[1]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],value,pair) * value; if(await this.openOrder(price,amount,"BUY",pair))this._spec[pair]["orderbook"]["bids"].unshift(price); } var price_new = await this._ceil(this._spec[pair]["orderbook"]["asks"][this._spec[pair]["orderbook"]["asks"].length - 1] * (this._spec[pair]["profit"] / 100 + 1),this._decimal.get(pair)["price_decimal"]); // ceil if (this._spec[pair]["orderbook"]["asks"].length <= this._spec[pair]["orderbook"]["asks_length"]) { var amount_new = await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],price_new,pair); if(await this.openOrder(price_new,amount_new,"SELL",pair))this._spec[pair]["orderbook"]["asks"].push(price_new); } } else { if (this._spec[pair]["orderbook"]["asks"].length <= 1) { if (!this._spec[pair]["reminder_asks"]) { this._wallet[$pairs[1]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],value,pair) * value; } final.push(value); this._spec[pair]["reminder_asks"] = value; } else { this._wallet[$pairs[1]] += await this.quantity(this._spec[pair]["amount"],this._spec[pair]["ref"],value,pair) * value; if(await this.openOrder(price,amount,"BUY",pair))this._spec[pair]["orderbook"]["bids"].unshift(price); } } }