cossmmbot
Version:
Market Making bot for the coss.io exchange
1,909 lines (1,204 loc) • 87.4 kB
JavaScript
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×tamp=" + $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);
}
}
}