From 6d54540fd0767fea5707f8300c9ebced3b70f2cd Mon Sep 17 00:00:00 2001 From: Aleksei Lebedev Date: Thu, 12 Mar 2020 10:24:01 +0300 Subject: [PATCH] v0.7.0 --- .editorconfig | 15 + .eslintrc.js | 44 ++ .gitignore | 6 + README.md | 91 ++++ app.js | 42 ++ config.json | 75 +++ helpers/const.js | 3 + helpers/dbModel.js | 70 +++ helpers/log.js | 61 +++ helpers/notify.js | 57 +++ helpers/utils/adm_utils.js | 69 +++ helpers/utils/index.js | 123 +++++ modules/DB.js | 18 + modules/Store.js | 100 ++++ modules/api.js | 3 + modules/checkerTransactions.js | 27 ++ modules/commandTxs.js | 840 +++++++++++++++++++++++++++++++++ modules/configReader.js | 118 +++++ modules/incomingTxsParser.js | 114 +++++ modules/tradeapi.js | 2 + modules/transferTxs.js | 47 ++ modules/unknownTxs.js | 201 ++++++++ package.json | 49 ++ server.js | 43 ++ trade/mm_trader.js | 253 ++++++++++ trade/orderCollector.js | 38 ++ trade/orderStats.js | 111 +++++ trade/tradeParams_idcm.js | 8 + trade/trader_idcm.js | 203 ++++++++ 29 files changed, 2831 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.js create mode 100644 config.json create mode 100644 helpers/const.js create mode 100644 helpers/dbModel.js create mode 100644 helpers/log.js create mode 100644 helpers/notify.js create mode 100644 helpers/utils/adm_utils.js create mode 100644 helpers/utils/index.js create mode 100644 modules/DB.js create mode 100644 modules/Store.js create mode 100644 modules/api.js create mode 100644 modules/checkerTransactions.js create mode 100644 modules/commandTxs.js create mode 100644 modules/configReader.js create mode 100644 modules/incomingTxsParser.js create mode 100644 modules/tradeapi.js create mode 100644 modules/transferTxs.js create mode 100644 modules/unknownTxs.js create mode 100644 package.json create mode 100644 server.js create mode 100644 trade/mm_trader.js create mode 100644 trade/orderCollector.js create mode 100644 trade/orderStats.js create mode 100644 trade/tradeParams_idcm.js create mode 100644 trade/trader_idcm.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6f445f1b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# Install EditorConfig Plugin on your IDE for one coding style +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +tab_width = 4 +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..c0293ada --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,44 @@ +module.exports = { + "parserOptions": { + "ecmaVersion": 8, + "sourceType": "module" + }, + "rules": { + quotes: ["error", "single"], + "semi": "warn", // обязательно ; + "semi-spacing": ["error", {"before": false, "after": true}], + "indent": ["error", "tab"], + "space-infix-ops": "error",// отступы вокруг + - * / = и тд + "eqeqeq": "error", // обязательно === и !== (нельзя == и !=) + // "no-eq-null": "error", // обязательно === и !== (нельзя == и !=) но тоько в отношении null + "curly": "error", // проверка шаблонов `${name}` + // "space-before-function-paren": [ // отступ до и после function + // "error", { + // "anonymous": "always", + // "named": "always", + // "asyncArrow": "ignore" + // } + // ], + "key-spacing": ["error", { "mode": "strict" }], // оформление обЪекта + "space-in-parens": ["error", "never"], // запрет отступов ( a,b) + "computed-property-spacing": ["error", "never"], // запрет лишних отступов в выражениях a[ i] + "array-bracket-spacing": ["error", "never"], + "no-multi-spaces": "error", // запрет лишних пробелов var a = 2 + "no-sparse-arrays": "warn", // предупреждение при дырке в массиве + "no-mixed-spaces-and-tabs": "error", // нельзя миксовать табы и пробелы + "keyword-spacing": ["error", { "after": true }], + "comma-spacing": ["error", { "before": false, "after": true }], // отступ после запятой, а перед нельзя + "no-undef":"error", + "array-callback-return": "error" // коллбек методов массива типа arr.map arr.filter должны иметь return в коллбеке + }, + "env": { + "browser": true, + "node": true + }, + "globals": { + "Vue":true, + "Symbol":true, + "Promise":true, + }, + "plugins": [] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c87e89c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +logs/ +.vscode/ +package-lock.json +tests.js +config.test diff --git a/README.md b/README.md new file mode 100644 index 00000000..23e63505 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +ADAMANT Trading & Market making bot is a software that allows to run trades on crypto exchanges or make fake volume. Trading is a mode when bot run orders according to some strategy. It can be profitable or not. +In Market making mode, the bot places orders and execute them by himself, making a trade volume. +Trade bots work in ADAMANT Messenger chats directly. + +Features: + +* Managed with your commands using ADAMANT Messenger +* Easy to install and configure +* Free and open source +* Fill order books +* Market making +* Stores and displays statistics + +Supported exchanges (more in progress): + +* [IDCM](https://idcm.io/invitation/receive?code=LM5510&lang=en) + +Available commands: ask a bot with `/help` command. Read more: [M](). + +# Installation + +## Requirements + +* Ubuntu 16 / Ubuntu 18 (other OS had not been tested) +* NodeJS v 8+ +* MongoDB ([installation instructions](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/)) + +## Setup + +``` +su - adamant +git clone https://github.com/Adamant-im/adamant-tradebot +cd ./adamant-tradebot +npm i +``` + +## Pre-launch tuning + +``` +nano config.json +``` + +Parameters: + +* `exchange` Exchange to work with. Available values see above. Case insensitive, obligatory. +* `pair` Pair to with on the exchange. Obligatory. +* `apikey` Exchange's account API key for connection. Obligatory. +* `apisecret` Exchange's account API secret for connection. Obligatory. +* `apipassword` Exchange's account trade password. If needed for exchange. +* `passPhrase` The bot's secret phrase for accepting commands. Obligatory. Bot's ADM address will correspond this passPhrase. +* `admin_accounts` ADAMANT accounts to accept commands from. Commands from other accounts will not be executed. At lease one account. +* `notify_non_admins` Notify non-admins that they are not admins. If false, bot will be silent. +* `node_ADM` List of nodes for API work, obligatorily +* `infoservice` List of [ADAMANT InfoServices](https://github.com/Adamant-im/adamant-currencyinfo-services) for catching exchange rates, recommended +* `slack` Token for Slack alerts for the bot’s administrator. No alerts if not set. +* `adamant_notify` ADM address for the bot’s administrator. Recommended. +* `socket` If to use WebSocket connection. Recommended for better user experience. +* `ws_type` Choose socket connection, "ws" or "wss" depending on your server. +* `bot_name` Bot's name for notifications. +* `welcome_string` How to reply user in-chat, if unknown command received. + +## Launching + +You can start the Bot with the `node app` command, but it is recommended to use the process manager for this purpose. + +``` +pm2 start --name tradebot app.js +``` + +## Add Bot to cron + +``` +crontab -e +``` + +Add string: + +``` +@reboot cd /home/adamant/adamant-tradebot && pm2 start --name tradebot app.js +``` + +## Updating + +``` +su - adamant +cd ./adamant-tradebot +pm2 stop tradebot +mv config.json config_bup.json && git pull && mv config_bup.json config.json +npm i +pm2 start --name tradebot app.js +``` diff --git a/app.js b/app.js new file mode 100644 index 00000000..d506c5dc --- /dev/null +++ b/app.js @@ -0,0 +1,42 @@ +const notify = require('./helpers/notify'); +const db = require('./modules/DB'); +const Store = require('./modules/Store'); +const checker = require('./modules/checkerTransactions'); +const doClearDB = process.argv.includes('clear_db'); +const config = require('./modules/configReader'); +const txParser = require('./modules/incomingTxsParser'); +const log = require('./helpers/log'); + +// Socket connection +const api = require('./modules/api'); +api.socket.initSocket({socket: config.socket, wsType: config.ws_type, onNewMessage: txParser, admAddress: Store.user.ADM.address}); + +setTimeout(init, 5000); + +function init() { + require('./server'); + try { + if (doClearDB) { + console.log('Clearing database..'); + db.systemDb.db.drop(); + db.incomingTxsDb.db.drop(); + notify(`*${config.notifyName}: database cleared*. Manually stop the Bot now.`, 'info'); + } else { + + db.systemDb.findOne().then(system => { + if (system) { + Store.lastBlock = system.lastBlock; + } else { // if 1st start + Store.updateLastBlock(); + } + checker(); + require('./trade/mm_trader').run(); + notify(`*${config.notifyName} started* for address _${Store.user.ADM.address}_ (ver. ${Store.version}).`, 'info'); + }); + } + + } catch (e) { + notify(`${config.notifyName} is not started. Error: ${e}`, 'error'); + process.exit(1); + } +} diff --git a/config.json b/config.json new file mode 100644 index 00000000..29d12d74 --- /dev/null +++ b/config.json @@ -0,0 +1,75 @@ +{ + + /** The bot's secret phrase for accepting commands. + Bot's ADM address will correspond this passPhrase. + **/ + "passPhrase": "distance expect praise frequent..", + + /** List of nodes to fetch transactions. + If one become unavailable, bot will choose live one. + **/ + + "node_ADM": [ + "http://localhost:36666", + "https://endless.adamant.im", + "https://clown.adamant.im", + "https://bid.adamant.im", + "https://unusual.adamant.im", + "https://debate.adamant.im", + "http://185.231.245.26:36666", + "https://lake.adamant.im" + ], + + /** Socket connection is recommended for better user experience **/ + "socket": true, + + /** Choose socket connection, "ws" or "wss" depending on your server **/ + "ws_type": "ws", + + /** List of ADAMANT InfoServices for catching exchange rates **/ + "infoservice": [ + "https://info.adamant.im" + ], + + /** ADAMANT accounts to accept commands from. Commands from other accounts will not be executed. **/ + "admin_accounts": [ + "U123.." + ], + + /** Notify non-admins that they are not admins. If false, bot will be silent. **/ + "notify_non_admins": true, + + /** Exchange to work with. Available values: "idcm". Case insensitive. **/ + "exchange": "IDCM", + + /** Pair to trade **/ + "pair": "ADM/BTC", + + /** Exchange's account API key for connection **/ + "apikey": "YOUR-KEY..", + + /** Exchange's account API secret for connection **/ + "apisecret": "YOUR-SECRET..", + + /** Exchange's account trade password. If needed. **/ + "apipassword": "YOUR-TRADE-PASS", + + /** How to reply user in-chat, if first unknown command received. **/ + "welcome_string": "Hi! 😊 I'm a trade and market making bot. ℹ️ Learn more about me on ADAMANT’s blog or type **/help** to see what I can.", + + /** Bot's name for notifications **/ + "bot_name": "Lovely Trade Bot", + + /** ADAMANT address for notifications and monitoring (if needed, recommended) **/ + "adamant_notify": "", + + /** Slack key for notifications and monitoring (if needed) **/ + "slack": "https://hooks.slack.com/services/...", + + /** Port for getting debug info. + Do not set for live bots, use only for debugging. + Allows to get DBs records like http://ip:port/db?tb=incomingTxsDb + **/ + "api": false + +} diff --git a/helpers/const.js b/helpers/const.js new file mode 100644 index 00000000..b7bb11b0 --- /dev/null +++ b/helpers/const.js @@ -0,0 +1,3 @@ +module.exports = { + SAT: 100000000 +}; \ No newline at end of file diff --git a/helpers/dbModel.js b/helpers/dbModel.js new file mode 100644 index 00000000..e011d3e5 --- /dev/null +++ b/helpers/dbModel.js @@ -0,0 +1,70 @@ +module.exports = (db) => { + return class { + constructor(data = {}, isSave) { + this.db = db; + Object.assign(this, data); + if (isSave){ + this.save(); + } + } + static get db() { + return db; + } + static find(a) { // return Array + return new Promise((resolve, reject) => { + this.db.find(a).toArray((err, data) => { + resolve(data.map(d=>new this(d))); + }); + }); + } + static aggregate(a) { // return Array + return new Promise((resolve, reject) => { + this.db.aggregate(a).toArray((err, data) => { + resolve(data.map(d=>new this(d))); + }); + }); + } + static findOne(a) { + return new Promise((resolve, reject) => { + db.findOne(a).then((doc, b) => { + if (!doc) { + resolve(doc); + } else { + resolve(new this(doc)); + } + }); + }); + } + _data() { + const data = {}; + for (let field in this){ + if (!['db', '_id'].includes(field)){ + data[field] = this[field]; + } + } + return data; + } + async update(obj, isSave){ + Object.assign(this, obj); + if (isSave){ + await this.save(); + } + } + save() { + return new Promise((resolve, reject) => { + if (!this._id) { + db.insertOne(this._data(), (err, res) => { + this._id = res.insertedId; + resolve(this._id); + }); + } else { + db.updateOne({_id: this._id}, { + $set: this._data() + }, {upsert: true}).then(() => { + resolve(this._id); + }); + } + }); + } + }; +}; \ No newline at end of file diff --git a/helpers/log.js b/helpers/log.js new file mode 100644 index 00000000..c1dc4dac --- /dev/null +++ b/helpers/log.js @@ -0,0 +1,61 @@ +let fs = require('fs'); +if (!fs.existsSync('./logs')) { + fs.mkdirSync('./logs'); +} + +let infoStr = fs.createWriteStream('./logs/' + date() + '.log', { + flags: 'a' +}); + +infoStr.write(` +_________________${fullTime()}_________________ +`); + +module.exports = { + error(str) { + infoStr.write(` + ` + 'Bot error|' + time() + '|' + str); + console.log('\x1b[31m', 'error|' + time(), '\x1b[0m', str); + }, + info(str) { + console.log('\x1b[32m', 'info|' + time(), '\x1b[0m', str); + + infoStr.write(` + ` + 'Bot info|' + time() + '|' + str); + }, + warn(str) { + console.log('\x1b[33m', 'warn|' + time(), '\x1b[0m', str); + + infoStr.write(` + ` + 'Bot warn|' + time() + '|' + str); + }, + log(str) { + console.log('\x1b[34m', 'log|' + time(), '\x1b[0m', str); + + infoStr.write(` + ` + 'Bot log|[' + time() + '|' + str); + } +}; + +function time() { + var options = { + hour: 'numeric', + minute: 'numeric', + second: 'numeric' + }; + + return new Date().toLocaleString('en', options); +} + +function date() { + var options = { + day: 'numeric', + month: 'numeric', + year: 'numeric' + }; + return (new Date().toLocaleString('en', options)).replace(/\//g, '-'); +} + +function fullTime() { + return date() + ' ' + time(); +} diff --git a/helpers/notify.js b/helpers/notify.js new file mode 100644 index 00000000..5a8365cb --- /dev/null +++ b/helpers/notify.js @@ -0,0 +1,57 @@ +const request = require('request'); +const config = require('../modules/configReader'); +const log = require('./log'); +const api = require('../modules/api'); +const { + adamant_notify, + slack +} = config; + + +module.exports = (message, type) => { + try { + log[type](message.replace(/\*/g, '').replace(/_/g, '')); + + if (!slack && !adamant_notify) { + return; + } + let color; + switch (type) { + case ('error'): + color = '#FF0000'; + break; + + case ('warn'): + color = '#FFFF00'; + break; + + case ('info'): + color = '#00FF00'; + break; + case ('log'): + color = '#FFFFFF'; + break; + } + const opts = { + uri: slack, + method: 'POST', + json: true, + body: { + 'attachments': [{ + 'fallback': message, + 'color': color, + 'text': message, + 'mrkdwn_in': ['text'] + }] + } + }; + if (slack && slack.length > 34) { + request(opts); + } + if (adamant_notify && adamant_notify.length > 5 && adamant_notify.startsWith('U') && config.passPhrase && config.passPhrase.length > 30) { + api.send(config.passPhrase, adamant_notify, `${type}| ${message.replace(/\*/g, '**')}`, 'message'); + } + } catch (e) { + log.error('Notifier error: ' + e); + } +}; diff --git a/helpers/utils/adm_utils.js b/helpers/utils/adm_utils.js new file mode 100644 index 00000000..1ec7bd93 --- /dev/null +++ b/helpers/utils/adm_utils.js @@ -0,0 +1,69 @@ +const Store = require('../../modules/Store'); +const api = require('../../modules/api'); +const log = require('../../helpers/log'); +const {SAT} = require('../const'); +const User = Store.user.ADM; + +module.exports = { + get FEE() { + return Store.comissions.ADM; + }, + syncGetTransaction(hash, tx){ + return { + blockNumber: tx.blockId, + hash: tx.id, + sender: tx.senderId, + recipient: tx.recipientId, + amount: +(tx.amount / SAT).toFixed(8) + }; + }, + async getLastBlockNumber(){ + try { + return (await api.get('uri', 'blocks?limit=1')).blocks[0].height; + } catch (e){ + return null; + } + }, + async getTransactionStatus(txid){ + try { + const tx = (await api.get('uri', 'transactions/get?id=' + txid)).transaction; + return { + blockNumber: tx.height, + status: true + }; + } catch (e){ + return null; + } + }, + async send(params) { + try { + const {address, value, comment} = params; + console.log(`Send ${value} ADM: `, comment); + let res; + if (comment){ + res = api.send(User.passPhrase, address, comment, 'message', null, value); + } else { + res = api.send(User.passPhrase, address, value, null, comment); + } + + if (!res) { + return { + success: false + }; + } + return { + success: res.success, + hash: res.transactionId + }; + } catch (e) { + log.error('Error while sending ADM in Utils module: ' + e); + } + }, + async updateBalance() { + try { + User.balance = (await api.get('uri', 'accounts?address=' + User.address)).account.balance / SAT; + } catch (e) { + log.error('Error while getting ADM balance in Utils module: ' + e); + } + } +}; diff --git a/helpers/utils/index.js b/helpers/utils/index.js new file mode 100644 index 00000000..218aca82 --- /dev/null +++ b/helpers/utils/index.js @@ -0,0 +1,123 @@ +const api = require('../../modules/api'); +const config = require('../../modules/configReader'); +const adm_utils = require('./adm_utils'); +const log = require('../log'); +const db = require('../../modules/DB'); +const Store = require('../../modules/Store'); + +module.exports = { + unix() { + return new Date().getTime(); + }, + sendAdmMsg(address, msg, type = 'message') { + if (msg) { + try { + return api.send(config.passPhrase, address, msg, type).success || false; + } catch (e) { + return false; + } + } + }, + thousandSeparator(num, doBold) { + var parts = (num + '').split('.'), + main = parts[0], + len = main.length, + output = '', + i = len - 1; + + while (i >= 0) { + output = main.charAt(i) + output; + if ((len - i) % 3 === 0 && i > 0) { + output = ' ' + output; + } + --i; + } + + if (parts.length > 1) { + if (doBold) { + output = `**${output}**.${parts[1]}`; + } else { + output = `${output}.${parts[1]}`; + } + } + return output; + }, + async getAddressCryptoFromAdmAddressADM(coin, admAddress) { + try { + if (this.isERC20(coin)) { + coin = 'ETH'; + } + const resp = await api.syncGet(`/api/states/get?senderId=${admAddress}&key=${coin.toLowerCase()}:address`); + if (resp && resp.success) { + if (resp.transactions.length) { + return resp.transactions[0].asset.state.value; + } else { + return 'none'; + }; + }; + } catch (e) { + log.error(' in getAddressCryptoFromAdmAddressADM(): ' + e); + return null; + } + }, + async userDailiValue(senderId){ + return (await db.paymentsDb.find({ + transactionIsValid: true, + senderId: senderId, + needToSendBack: false, + inAmountMessageUsd: {$ne: null}, + date: {$gt: (this.unix() - 24 * 3600 * 1000)} // last 24h + })).reduce((r, c) => { + return r + c.inAmountMessageUsd; + }, 0); + }, + async updateAllBalances(){ + try { + await this.ADM.updateBalance(); + } catch (e){} + }, + async getLastBlocksNumbers() { + const data = { + ADM: await this.ADM.getLastBlockNumber(), + }; + return data; + }, + isKnown(coin){ + return config.known_crypto.includes(coin); + }, + isAccepted(coin){ + return config.accepted_crypto.includes(coin); + }, + isExchanged(coin){ + return config.exchange_crypto.includes(coin); + }, + isFiat(coin){ + return ['USD', 'RUB', 'EUR', 'CNY', 'JPY'].includes(coin); + }, + isHasTicker(coin){ // if coin has ticker like COIN/OTHERCOIN or OTHERCOIN/COIN + const pairs = Object.keys(Store.currencies).toString(); + return pairs.includes(',' + coin + '/') || pairs.includes('/' + coin); + }, + isERC20(coin){ + return config.erc20.includes(coin.toUpperCase()); + }, + getCoinsFromPair(pair) { + pair = pair.trim().toUpperCase(); + let coin, coin2; + if (pair.indexOf('/') > -1) { + coin = pair.substr(0, pair.indexOf('/')); + coin2 = pair.substr(pair.indexOf('/') + 1, coin.length); + } + return { + coin: coin, + coin2: coin2, + pair: pair + } + }, + randomDeviation(number, deviation) { + const min = number - number * deviation; + const max = number + number * deviation; + return Math.random() * (max - min) + min; + }, + ADM: adm_utils +}; diff --git a/modules/DB.js b/modules/DB.js new file mode 100644 index 00000000..7b992a4a --- /dev/null +++ b/modules/DB.js @@ -0,0 +1,18 @@ +const MongoClient = require("mongodb").MongoClient; +const mongoClient = new MongoClient("mongodb://localhost:27017/", {useNewUrlParser: true, useUnifiedTopology: true}); +const model = require('../helpers/dbModel'); + +const collections = {}; + +mongoClient.connect((err, client) => { + if (err) { + throw (err); + } + const db = client.db("tradebotdb"); + collections.db = db; + collections.systemDb = model(db.collection("systems")); + collections.incomingTxsDb = model(db.collection("incomingtxs")); + collections.ordersDb = model(db.collection("orders")); +}); + +module.exports = collections; diff --git a/modules/Store.js b/modules/Store.js new file mode 100644 index 00000000..044fbb9f --- /dev/null +++ b/modules/Store.js @@ -0,0 +1,100 @@ +const db = require('./DB'); +const log = require('../helpers/log'); +const keys = require('adamant-api/helpers/keys'); +const api = require('./api'); +const {version} = require('../package.json'); +const config = require('./configReader'); + +// ADM data +const AdmKeysPair = keys.createKeypairFromPassPhrase(config.passPhrase); +const AdmAddress = keys.createAddressFromPublicKey(AdmKeysPair.publicKey); +// ETH data +const ethData = api.eth.keys(config.passPhrase); + +module.exports = { + version, + botName: AdmAddress, + user: { + ADM: { + passPhrase: config.passPhrase, + keysPair: AdmKeysPair, + address: AdmAddress + }, + ETH: { + address: ethData.address, + privateKey: ethData.privateKey, + } + }, + comissions: { + ADM: 0.5 // This is a stub. Ether fee returned with FEE() method in separate module + }, + lastBlock: null, + get lastHeight() { + return this.lastBlock && this.lastBlock.height || false; + }, + updateSystem(field, data) { + const $set = {}; + $set[field] = data; + db.systemDb.db.updateOne({}, {$set}, {upsert: true}); + this[field] = data; + }, + async updateLastBlock() { + try { + const lastBlock = (await api.get('uri', 'blocks')).blocks[0]; + this.updateSystem('lastBlock', lastBlock); + } catch (e) { + log.error('Error while updating lastBlock: ' + e); + } + }, + async updateCurrencies(){ + try { + const data = await api.syncGet(config.infoservice + '/get', true); + if (data.success){ + this.currencies = data.result; + } + } catch (e){ + log.error('Error while updating currencies: ' + e); + }; + }, + getPrice(from, to){ + try { + from = from.toUpperCase(); + to = to.toUpperCase(); + let price = + (this.currencies[from + '/' + to] || 1 / this.currencies[to + '/' + from] || 0).toFixed(8); + if (price){ + return price; + } + const priceFrom = +(this.currencies[from + '/USD']); + const priceTo = +(this.currencies[to + '/USD']); + return +(priceFrom / priceTo || 1).toFixed(8); + } catch (e){ + log.error('Error while calculating getPrice(): ', e); + return 0; + } + }, + mathEqual(from, to, amount, doNotAccountFees){ + let price = this.getPrice(from, to); + if (!doNotAccountFees){ + price *= (100 - config['exchange_fee_' + from]) / 100; + }; + if (!price){ + return { + outAmount: 0, + exchangePrice: 0 + }; + } + price = +price.toFixed(8); + return { + outAmount: +(price * amount).toFixed(8), + exchangePrice: price + }; + } +}; + +config.notifyName = `${config.bot_name} (${module.exports.botName})`; +module.exports.updateCurrencies(); + +setInterval(() => { + module.exports.updateCurrencies(); +}, 60 * 1000); + diff --git a/modules/api.js b/modules/api.js new file mode 100644 index 00000000..4dabf632 --- /dev/null +++ b/modules/api.js @@ -0,0 +1,3 @@ +const log = require('../helpers/log'); +const config = require('./configReader'); +module.exports = require('adamant-api')({passPhrase: config.passPhrase, node: config.node_ADM, logLevel: 'warn'}, log); diff --git a/modules/checkerTransactions.js b/modules/checkerTransactions.js new file mode 100644 index 00000000..69039809 --- /dev/null +++ b/modules/checkerTransactions.js @@ -0,0 +1,27 @@ +const Store = require('./Store'); +const api = require('./api'); +const txParser = require('./incomingTxsParser'); +const log = require('../helpers/log'); + +async function check() { + try { + if (!Store.lastHeight){ + return; + } + const txChat = (await api.get('uri', 'chats/get/?recipientId=' + Store.user.ADM.address + '&orderBy=timestamp:desc&fromHeight=' + (Store.lastHeight - 5))).transactions; + + const txTrx = (await api.get('transactions', 'fromHeight=' + (Store.lastHeight - 5) + '&and:recipientId=' + Store.user.ADM.address + '&and:type=0')).transactions; + + txChat + .concat(txTrx) + .forEach(t => { + txParser(t); + }); + Store.updateLastBlock(); + } catch (e) { + log.error('Error while checking new transactions: ' + e); + } +} +module.exports = () => { + setInterval(check, 2500); +}; diff --git a/modules/commandTxs.js b/modules/commandTxs.js new file mode 100644 index 00000000..49a969a6 --- /dev/null +++ b/modules/commandTxs.js @@ -0,0 +1,840 @@ +const fs = require('fs'); +const Store = require('../modules/Store'); +const $u = require('../helpers/utils'); +const config = require('./configReader'); +const log = require('../helpers/log'); +const notify = require('../helpers/notify'); +const tradeParams = require('../trade/tradeParams_' + config.exchange); +const traderapi = require('../trade/trader_' + config.exchange)(config.apikey, config.apisecret, config.apipassword, log); +const orderCollector = require('../trade/orderCollector'); +const getStats = require('../trade/orderStats'); + +module.exports = async (cmd, tx, itx) => { + + if (itx.isProcessed) return; + log.info('Got new command Tx to process: ' + cmd); + try { + let res = []; + const group = cmd + .trim() + .replace(/ /g, ' ') + .replace(/ /g, ' ') + .replace(/ /g, ' ') + .split(' '); + const methodName = group.shift().trim().toLowerCase().replace('\/', ''); + const m = commands[methodName]; + if (m) { + res = await m(group, tx); + } else { + res.msgSendBack = `I don’t know */${methodName}* command. ℹ️ You can start with **/help**.`; + } + if (!tx) { + return res.msgSendBack; + } + if (tx) { + itx.update({isProcessed: true}, true); + if (res.msgNotify) + notify(res.msgNotify, res.notifyType); + if (res.msgSendBack) + $u.sendAdmMsg(tx.senderId, res.msgSendBack); + saveConfig(); + } + } catch (e) { + tx = tx || {}; + log.error('Error while processing command ' + cmd + ' from senderId ' + tx.senderId + '. Tx Id: ' + tx.id + '. Error: ' + e); + } +} + +function start(params) { + const type = (params[0] || '').trim(); + if (!type || !type.length || !["mm"].includes(type)) { + return { + msgNotify: '', + msgSendBack: `Indicate trade type, _mm_ for market making. Example: */start mm*.`, + notifyType: 'log' + } + } + if (type === "mm") { + if (!tradeParams.mm_isActive) { + tradeParams.mm_isActive = true; + return { + msgNotify: `${config.notifyName} set to start market making for ${config.pair}.`, + msgSendBack: `Starting market making for ${config.pair} pair..`, + notifyType: 'log' + } + } else { + tradeParams.mm_isActive = true; + return { + msgNotify: '', + msgSendBack: `Market making for ${config.pair} pair is active already.`, + notifyType: 'log' + } + } + } +} + +function stop(params) { + const type = (params[0] || '').trim(); + if (!type || !type.length || !["mm"].includes(type)) { + return { + msgNotify: '', + msgSendBack: `Indicate trade type, _mm_ for market making. Example: */stop mm*.`, + notifyType: 'log' + } + } + if (type === "mm") { + if (tradeParams.mm_isActive) { + tradeParams.mm_isActive = false; + return { + msgNotify: `${config.notifyName} stopped market making for ${config.pair} pair.`, + msgSendBack: `Market making for ${config.pair} pair is disabled now.`, + notifyType: 'log' + } + } else { + tradeParams.mm_isActive = false; + return { + msgNotify: '', + msgSendBack: `Market making for ${config.pair} pair is disabled already.`, + notifyType: 'log' + } + } + } +} + +function buypercent(param) { + const val = +((param[0] || '').trim()); + if (!val || val === Infinity || val < 0 || val > 100) { + return { + msgNotify: '', + msgSendBack: `Invalid percentage of buy orders. Example: */buyPercent 85*.`, + notifyType: 'log' + } + } + + tradeParams.mm_buyPercent = val / 100; + return { + msgNotify: `${config.notifyName} is set to make market with ${val}% of buy orders for ${config.pair} pair.`, + msgSendBack: `Set to make market with ${val}% of buy orders for ${config.pair} pair.`, + notifyType: 'log' + } +} + +function amount(param) { + const val = (param[0] || '').trim(); + if (!val || !val.length || (val.indexOf('-') === -1)) { + return { + msgNotify: '', + msgSendBack: `Invalid values for market making of ${config.pair}. Example: */amount 0.01-20*.`, + notifyType: 'log' + } + } + const [minStr, maxStr] = val.split('-'); + const min = +minStr; + const max = +maxStr; + if (!min || min === Infinity || !max || max === Infinity){ + return { + msgNotify: '', + msgSendBack: `Invalid values for market making of ${config.pair}. Example: */amount 0.01-20*.`, + notifyType: 'log' + } + } + tradeParams.mm_minAmount = min; + tradeParams.mm_maxAmount = max; + return { + msgNotify: `${config.notifyName} is set to make market with amounts from ${min} to ${max} ${config.coin1} for ${config.pair} pair.`, + msgSendBack: `Set to make market with amounts from ${min} to ${max} ${config.coin1} for ${config.pair} pair.`, + notifyType: 'log' + } +} + +function interval(param) { + const val = (param[0] || '').trim(); + if (!val || !val.length || (val.indexOf('-') === -1)) { + return { + msgNotify: '', + msgSendBack: `Invalid intervals for market making of ${config.pair}. Example: */interval 1-5 min*.`, + notifyType: 'log' + } + } + + let time = (param[1] || '').trim(); + let multiplier; + + switch (time) { + case 'sec': + multiplier = 1000; + break; + case 'min': + multiplier = 1000*60; + break; + case 'hour': + multiplier = 1000*60*60; + break; + default: + multiplier = 1000*60; + time = 'min'; + } + + const [minStr, maxStr] = val.split('-'); + const min = +minStr; + const max = +maxStr; + if (!min || min === Infinity || !max || max === Infinity) { + return { + msgNotify: '', + msgSendBack: `Invalid intervals for market making of ${config.pair}. Example: */interval 1-5 min*.`, + notifyType: 'log' + } + } + tradeParams.mm_minInterval = min * multiplier; + tradeParams.mm_maxInterval = max * multiplier; + return { + msgNotify: `${config.notifyName} is set to make market in intervals from ${min} to ${max} ${time} for ${config.pair} pair.`, + msgSendBack: `Set to make market in intervals from ${min} to ${max} ${time} for ${config.pair} pair.`, + notifyType: 'log' + } +} + +async function clear(params) { + + param = params[0]; + if (!param || param.indexOf('/') === -1) { + param = config.pair; + } + let coin = (param || '').toUpperCase().trim(); + let output = ''; + let pair; + if (coin.indexOf('/') > -1) { + pair = coin; + coin = coin.substr(0, coin.indexOf('/')); + } + + if (!pair || !pair.length) { + output = 'Please specify market to clear orders in. F. e., */clear DOGE/BTC*.'; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + + let purposes; + let count = 0; + let purposeString; + params.forEach(param => { + if (['all'].includes(param)) { + purposes = ['mm', 'tb']; + } + if (['tb'].includes(param)) { + purposes = ['tb']; + purposeString = `trade bot`; + } + }); + if (!purposes) { + purposes = ['mm']; + purposeString = `market making`; + } + + // console.log(purposes, count, output); + count = await orderCollector(purposes, pair); + if (purposeString) { + if (count > 0) { + output = `Clearing ${count} **${purposeString}** orders for ${pair} pair on ${config.exchangeName}..`; + } else { + output = `No open **${purposeString}** orders on ${config.exchangeName} for ${pair}.`; + } + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + + if (pair) { + const openOrders = await traderapi.getOpenOrders(pair); + if (openOrders) { + if ((count + openOrders.length) > 0) { + output = `Clearing **all** ${count + openOrders.length} orders for ${pair} pair on ${config.exchangeName}..`; + openOrders.forEach(order => { + traderapi.cancelOrder(order.orderid, order.side, order.symbol); + }); + } else { + output = `No open orders on ${config.exchangeName} for ${pair}.`; + } + } else { + output = `Unable to get ${config.exchangeName} orders for ${pair}.`; + } + } + + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + +} + +async function fill(params) { + + // default: low=bid, count=5, pair=ADM/BTC + // fill ADM/BTC buy amount=0.00002000 low=0.00000102 high=0.00000132 count=7 + + // default: high=ask, count=5, ADM/BTC + // fill ADM/BTC sell amount=300 low=0.00000224 high=0.00000380 count=7 + + let count, amount, low, high; + params.forEach(param => { + try { + if (param.startsWith('count')) { + count = +param.split('=')[1].trim(); + } + if (param.startsWith('amount')) { + amount = +param.split('=')[1].trim(); + } + if (param.startsWith('low')) { + low = +param.split('=')[1].trim(); + } + if (param.startsWith('high')) { + high = +param.split('=')[1].trim(); + } + } catch (e) { + return { + msgNotify: ``, + msgSendBack: 'Wrong arguments. Command works like this: */fill ADM/BTC buy amount=0.00002000 low=0.00000100 high=0.00000132 count=7*.', + notifyType: 'log' + } + } + }); + + if (params.length < 3) { + return { + msgNotify: ``, + msgSendBack: 'Wrong arguments. Command works like this: */fill ADM/BTC buy amount=0.00002000 low=0.00000100 high=0.00000132 count=7*.', + notifyType: 'log' + } + } + + let firstParam = 'pair'; + param = params[0]; + if (!param || param.indexOf('/') === -1) { + firstParam = 'type'; + param = config.pair; + } + let coin = (param || '').toUpperCase().trim(); + let coin2 = ''; + let output = ''; + let pair; + if (coin.indexOf('/') > -1) { + pair = coin; + coin = coin.substr(0, coin.indexOf('/')); + coin2 = pair.substr(pair.indexOf('/') + 1, coin.length); + } + + if (!pair || !pair.length) { + output = 'Please specify market to fill orders in.'; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + + let type; + if (firstParam === 'type') { + type = params[0]; + } else { + type = params[1]; + } + + if (!count || count === Infinity || count < 1) + count = 5; // default + + if (!high || high === Infinity || !low || low === Infinity) { + const exchangeRates = await traderapi.getRates(pair); + if (exchangeRates) { + if (!low || low === Infinity) + low = exchangeRates.bid; + if (!high || high === Infinity) + high = exchangeRates.ask; + } else { + output = `Unable to get ${config.exchangeName} rates for ${pair} to fill orders.`; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + } + + // console.log(pair, type, count, amount, low, high); + + if (low > high) { + output = `To fill orders _high_ should be greater than _low_.`; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + + const balances = await traderapi.getBalances(); + let balance; + let isBalanceEnough = true; + if (balances) { + try { + if (type === 'buy') { + balance = balances.filter(crypto => crypto.code === coin2)[0].free; + output = `Not enough ${coin2} to fill orders. Check balances.`; + } else { + balance = balances.filter(crypto => crypto.code === coin)[0].free; + output = `Not enough ${coin} to fill orders. Check balances.`; + } + isBalanceEnough = balance >= amount; + } catch (e) { + output = `Unable to process balances. Check parameters.`; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + } else { + output = `Unable to get ${config.exchangeName} balances. Try again.`; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + + if (!isBalanceEnough) { + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + + // Make order list + let orderList = []; + const delta = high - low; + const step = delta / count; + const orderAmount = amount / count; + const deviation = 0.9; + + let price = low; + let total = 0, coin1Amount = 0, coin2Amount = 0; + for (let i=0; i < count; i++) { + price += $u.randomDeviation(step, deviation); + coin1Amount = $u.randomDeviation(orderAmount, deviation); + total += coin1Amount; + if (type === 'buy') { + // console.log(price, coin1Amount, total); + coin2Amount = coin1Amount; + coin1Amount = coin1Amount / price; + } else { + coin1Amount = coin1Amount; + coin2Amount = coin1Amount * price; + } + if (price > high || total > amount) + break; + orderList.push({ + price: price, + amount: coin1Amount, + altAmount: coin2Amount + }); + } + + // console.log(orderList, orderList.length); + + // Place orders + let total1 = 0, total2 = 0; + items = 0; + let id; + for (i=0; i < orderList.length; i++) { + id = await traderapi.placeOrder(type, pair, orderList[i].price, orderList[i].amount, 1); + if (id) { + items += 1; + total1 += +orderList[i].amount; + total2 += +orderList[i].altAmount; + } + } + + output = `${items} orders to ${type} ${$u.thousandSeparator(+total1.toFixed(8), false)} ${coin} for ${$u.thousandSeparator(+total2.toFixed(8), false)} ${coin2}.`; + + return { + msgNotify: `${config.notifyName} placed ${output}`, + msgSendBack: `Placed ${output}`, + notifyType: 'log' + } + +} + +/* +function buy_sell(params) { + + /buy 10 ADM at 0.00000123 on ADM/BTC + /buy 10 ADM on ADM/BTC + /buy ADM for 0.1 BTC on ADM/BTC + + /sell 10 ADM at 0.00000123 on ADM/BTC + /sell 10 ADM on ADM/BTC + /sell ADM for 0.1 BTC on ADM/BTC + +} +*/ + +function help() { + + let output = `I am **online** and ready to trade. I can do trading and market making, and also can give you market info.`; + + output += ` + +Commands: + +**/rates**: Find out the market price of the coin and/or the ask and bid prices on the exchange for the trading pair. F. e., */rates ADM* or */rates ADM/BTC*. + +**/stats**: Show information on the trading pair on the exchange. Prices, trading volumes and market making stats. Like */stats* or */stats ETH/BTC*. + +**/orders**: Display the number of active orders for the trading pair. Example: */orders ADM/BTC*. + +**/calc**: Calculate the price of one cryptocurrency in another at the market price and at the exchange prices. Works like this: */calc 2.05 BTC in USDT*. + +**/balances**: Display your balances on the exchange + +**/start**: Start trading (td) or market making (mm). F. e., /*start mm*. + +**/stop**: Stop trading (td) or market making (mm). F. e., /*stop mm*. + +**/amount**: Set the amount range for market making orders. Example: */amount 0.1-20*. + +**/interval**: Set the frequency in [sec, *min*, hour] of transactions for market making. Example: */interval 1-5 min*. + +**/buyPercent**: Set the percentage of buy orders for market making. Try */buyPercent 85*. + +**/fill**: Fill sell or buy order book. Works like this: */fill ADM/BTC buy amount=0.00200000 low=0.00000050 high=0.00000182 count=7*. + +**/clear**: Cancel [*mm*, td, all] active orders. F. e., */clear ETH/BTC all* or just */clear* for mm-orders of default pair. + +**/version**: Show bot’s software version + +Happy trading! +`; + +return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' +} + +} + +async function rates(params) { + + param = params[0]; + if (!param) { + param = config.pair; + } + let coin = (param || '').toUpperCase().trim(); + let output = ''; + let pair; + if (coin.indexOf('/') > -1) { + pair = coin; + coin = coin.substr(0, coin.indexOf('/')); + } + + if (!coin || !coin.length) { + output = 'Please specify coin ticker or specific market you are interested in. F. e., */rates ADM* or */rates ETH/BTC*.'; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + const currencies = Store.currencies; + const res = Object + .keys(Store.currencies) + .filter(t => t.startsWith(coin + '/')) + .map(t => { + let p = `${coin}/**${t.replace(coin + '/', '')}**`; + return `${p}: ${currencies[t]}`; + }) + .join(', '); + + if (!res.length) { + if (!pair) { + output = `I can’t get rates for *${coin}*. Made a typo? Try */rates ADM*.`; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + } else { + output = `Global market rates for ${coin}: +${res}.`; + } + + if (pair) { + const exchangeRates = await traderapi.getRates(pair); + if (output) + output += "\n\n"; + if (exchangeRates) { + output += `${config.exchangeName} rates for ${pair} pair: +Ask: ${exchangeRates.ask.toFixed(8)}, bid: ${exchangeRates.bid.toFixed(8)}`; + } else { + output += `Unable to get ${config.exchangeName} rates for ${pair}.`; + } + } + + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + +} + +async function stats(params) { + + param = params[0]; + if (!param || param.indexOf('/') === -1) { + param = config.pair; + } + let coin = (param || '').toUpperCase().trim(); + let coin2 = ''; + let output = ''; + let pair; + if (coin.indexOf('/') > -1) { + pair = coin; + coin = pair.substr(0, pair.indexOf('/')); + coin2 = pair.substr(pair.indexOf('/') + 1, coin.length); + } + + if (!pair || !pair.length) { + output = 'Please specify market you are interested in. F. e., */stats ETH/BTC*.'; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + + if (pair) { + const exchangeRates = await traderapi.getRates(pair); + if (exchangeRates) { + output += `${config.exchangeName} 24h stats for ${pair} pair: +Vol: ${$u.thousandSeparator(+exchangeRates.volume, true)} ${coin}. High: ${exchangeRates.high.toFixed(8)}, low: ${exchangeRates.low.toFixed(8)}, delta: _${(exchangeRates.high-exchangeRates.low).toFixed(8)}_ ${coin2}. +Ask: ${exchangeRates.ask.toFixed(8)}, bid: ${exchangeRates.bid.toFixed(8)}, spread: _${(exchangeRates.ask-exchangeRates.bid).toFixed(8)}_ ${coin2}.`; + } else { + output += `Unable to get ${config.exchangeName} stats for ${pair}.`; + } + } + + let orderStats = await getStats(true, true, false, "mm", pair); + + if (orderStats) { + if (orderStats.coin1AmountTotalAllCount === 0) { + output += "\n\n" + `There were no Market making orders for ${pair} all time.` + } else { + output += "\n\n" + `Market making stats for ${pair} pair:` + "\n"; + if (orderStats.coin1AmountTotalDayCount != 0) { + output += `24h: ${orderStats.coin1AmountTotalDayCount} orders with ${$u.thousandSeparator(+orderStats.coin1AmountTotalDay.toFixed(0), true)} ${coin} and ${$u.thousandSeparator(+orderStats.coin2AmountTotalDay.toFixed(8), true)} ${coin2}` + } + if (orderStats.coin1AmountTotalMonthCount > orderStats.coin1AmountTotalDayCount) { + output += `, 30d: ${orderStats.coin1AmountTotalMonthCount} orders with ${$u.thousandSeparator(+orderStats.coin1AmountTotalMonth.toFixed(0), true)} ${coin} and ${$u.thousandSeparator(+orderStats.coin2AmountTotalMonth.toFixed(8), true)} ${coin2}` + } + if (orderStats.coin1AmountTotalAllCount > orderStats.coin1AmountTotalMonthCount) { + output += `, all time: ${orderStats.coin1AmountTotalAllCount} orders with ${$u.thousandSeparator(+orderStats.coin1AmountTotalAll.toFixed(0), true)} ${coin} and ${$u.thousandSeparator(+orderStats.coin2AmountTotalAll.toFixed(8), true)} ${coin2}` + } + output += '.'; + } + } else { + output += `Unable to get Market making stats for ${pair}.`; + } + + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + +} + +async function orders(params) { + + param = params[0]; + if (!param || param.indexOf('/') === -1) { + param = config.pair; + } + let coin = (param || '').toUpperCase().trim(); + let output = ''; + let pair; + if (coin.indexOf('/') > -1) { + pair = coin; + coin = coin.substr(0, coin.indexOf('/')); + } + + if (!pair || !pair.length) { + output = 'Please specify market you are interested in. F. e., */orders ADM/BTC*.'; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + + if (pair) { + const openOrders = await traderapi.getOpenOrders(pair); + // console.log(openOrders); + if (openOrders) { + if (openOrders.length > 0) + output = `${config.exchangeName} open orders for ${pair} pair: ${openOrders.length}.`; + else + output = `No open orders on ${config.exchangeName} for ${pair}.`; + } else { + output = `Unable to get ${config.exchangeName} orders for ${pair}.`; + } + } + + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + +} + +async function calc(arr) { + + if (arr.length !== 4) { + return { + msgNotify: ``, + msgSendBack: 'Wrong arguments. Command works like this: */calc 2.05 BTC in USDT*.', + notifyType: 'log' + } + } + + let output = ''; + const amount = +arr[0]; + const inCurrency = arr[1].toUpperCase().trim(); + const outCurrency = arr[3].toUpperCase().trim(); + let pair = inCurrency + '/' + outCurrency; + let pair2 = outCurrency + '/' + inCurrency; + + if (!amount || amount === Infinity) { + output = `It seems amount "*${amount}*" for *${inCurrency}* is not a number. Command works like this: */calc 2.05 BTC in USDT*.`; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + if (!$u.isHasTicker(inCurrency)) { + output = `I don’t have rates of crypto *${inCurrency}* from Infoservice. Made a typo? Try */calc 2.05 BTC in USDT*.`; + } + if (!$u.isHasTicker(outCurrency)) { + output = `I don’t have rates of crypto *${outCurrency}* from Infoservice. Made a typo? Try */calc 2.05 BTC in USDT*.`; + } + + let result; + if (!output) { + result = Store.mathEqual(inCurrency, outCurrency, amount, true).outAmount; + if (amount <= 0 || result <= 0 || !result) { + output = `I didn’t understand amount for *${inCurrency}*. Command works like this: */calc 2.05 BTC in USDT*.`; + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + } + if ($u.isFiat(outCurrency)) { + result = +result.toFixed(2); + } + output = `Global market value of ${$u.thousandSeparator(amount)} ${inCurrency} equals **${$u.thousandSeparator(result)} ${outCurrency}**.`; + } else { + output = ''; + } + + if (output) + output += "\n\n"; + let askValue, bidValue; + + let exchangeRates = await traderapi.getRates(pair); + if (exchangeRates) { + askValue = exchangeRates.ask * amount; + bidValue = exchangeRates.bid * amount; + output += `${config.exchangeName} value of ${$u.thousandSeparator(amount)} ${inCurrency}: +Ask: **${$u.thousandSeparator(askValue.toFixed(8))} ${outCurrency}**, bid: **${$u.thousandSeparator(bidValue.toFixed(8))} ${outCurrency}**.`; + } else { + exchangeRates = await traderapi.getRates(pair2); + if (exchangeRates) { + askValue = amount / exchangeRates.ask; + bidValue = amount / exchangeRates.bid; + output += `${config.exchangeName} value of ${$u.thousandSeparator(amount)} ${inCurrency}: + Ask: **${$u.thousandSeparator(askValue.toFixed(8))} ${outCurrency}**, bid: **${$u.thousandSeparator(bidValue.toFixed(8))} ${outCurrency}**.`; + } else { + output += `Unable to get ${config.exchangeName} rates for ${pair}.`; + } + } + + return { + msgNotify: ``, + msgSendBack: `${output}`, + notifyType: 'log' + } + +} + +async function balances() { + const balances = await traderapi.getBalances(); + let output = ''; + + if (balances.length === 0) { + output = `All empty.`; + } else { + balances.forEach(crypto => { + output += `${$u.thousandSeparator(+crypto.free.toFixed(8), true)} _${crypto.code}_`; + if (+crypto.freezed > 0) { + output += ` & ${$u.thousandSeparator(+crypto.freezed.toFixed(8), true)} freezed`; + } + output += "\n"; + }); + } + + return { + msgNotify: ``, + msgSendBack: `${config.exchangeName} balances: +${output}`, + notifyType: 'log' + } +} + +function version() { + return { + msgNotify: ``, + msgSendBack: `I am running on _adamant-tradebot_ software version _${Store.version}_. Revise code on ADAMANT's GitHub.`, + notifyType: 'log' + } +} + +function saveConfig() { + const str = "module.exports = " + JSON.stringify(tradeParams, null, 3); + fs.writeFileSync('./trade/tradeParams_' + config.exchange + '.js', str) +} + +const commands = { + help, + rates, + stats, + orders, + calc, + balances, + version, + start, + stop, + buypercent, + amount, + interval, + clear, + fill +} \ No newline at end of file diff --git a/modules/configReader.js b/modules/configReader.js new file mode 100644 index 00000000..2f916278 --- /dev/null +++ b/modules/configReader.js @@ -0,0 +1,118 @@ +const jsonminify = require('jsonminify'); +const fs = require('fs'); +const log = require('../helpers/log'); +const keys = require('adamant-api/helpers/keys'); +const isDev = process.argv.includes('dev'); +let config = {}; + +// Validate config fields +const fields = { + passPhrase: { + type: String, + isRequired: true + }, + node_ADM: { + type: Array, + isRequired: true + }, + infoservice: { + type: Array, + default: ['https://info.adamant.im'] + }, + exchange: { + type: String, + isRequired: true + }, + pair: { + type: String, + isRequired: true + }, + apikey: { + type: String, + isRequired: true + }, + apisecret: { + type: String, + isRequired: true + }, + apipassword: { + type: String, + isRequired: true + }, + admin_accounts: { + type: Array, + default: [] + }, + notify_non_admins: { + type: Boolean, + default: false + }, + socket: { + type: Boolean, + default: true + }, + ws_type: { + type: String, + isRequired: true + }, + bot_name: { + type: String, + default: null + }, + adamant_notify: { + type: String, + default: null + }, + slack: { + type: String, + default: null + }, + welcome_string: { + type: String, + default: 'Hello 😊. This is a stub. I have nothing to say. Please check my config.' + } +}; + +try { + if (isDev) { + config = JSON.parse(jsonminify(fs.readFileSync('./config.test', 'utf-8'))); + } else { + config = JSON.parse(jsonminify(fs.readFileSync('./config.json', 'utf-8'))); + } + + let keysPair; + try { + keysPair = keys.createKeypairFromPassPhrase(config.passphrase); + } catch (e) { + exit('Passphrase is not valid! Error: ' + e); + } + const address = keys.createAddressFromPublicKey(keysPair.publicKey); + config.publicKey = keysPair.publicKey; + config.address = address; + config.exchangeName = config.exchange; + config.exchange = config.exchangeName.toLowerCase(); + config.coin1 = config.pair.split('/')[0]; + config.coin2 = config.pair.split('/')[1]; + + Object.keys(fields).forEach(f => { + if (!config[f] && fields[f].isRequired) { + exit(`Bot's ${address} config is wrong. Field _${f}_ is not valid. Cannot start Bot.`); + } else if (!config[f] && fields[f].default) { + config[f] = fields[f].default; + } + if (config[f] && fields[f].type !== config[f].__proto__.constructor) { + exit(`Bot's ${address} config is wrong. Field type _${f}_ is not valid, expected type is _${fields[f].type.name}_. Cannot start Bot.`); + } + }); + +} catch (e) { + log.error('Error reading config: ' + e); +} + +function exit(msg) { + log.error(msg); + process.exit(-1); +} + +config.isDev = isDev; +module.exports = config; diff --git a/modules/incomingTxsParser.js b/modules/incomingTxsParser.js new file mode 100644 index 00000000..92b5d223 --- /dev/null +++ b/modules/incomingTxsParser.js @@ -0,0 +1,114 @@ +const db = require('./DB'); +const log = require('../helpers/log'); +const $u = require('../helpers/utils'); +const api = require('./api'); +const config = require('./configReader'); +const commandTxs = require('./commandTxs'); +const unknownTxs = require('./unknownTxs'); +const transferTxs = require('./transferTxs'); +const notify = require('../helpers/notify'); + +const historyTxs = {}; + +module.exports = async (tx) => { + + if (!tx){ + return; + } + + if (historyTxs[tx.id]) { // do not process one tx twice + return; + } + + const {incomingTxsDb} = db; + const checkedTx = await incomingTxsDb.findOne({txid: tx.id}); + if (checkedTx !== null) { + return; + }; + + log.info(`New incoming transaction: ${tx.id}`); + + let msg = ''; + const chat = tx.asset.chat; + if (chat) { + msg = api.decodeMsg(chat.message, tx.senderPublicKey, config.passPhrase, chat.own_message).trim(); + } + + if (msg === ''){ + msg = 'NONE'; + } + + let type = 'unknown'; + if (msg.includes('_transaction') || tx.amount > 0){ + type = 'transfer'; // just for special message + } else if (msg.startsWith('/')){ + type = 'command'; + } + + const spamerIsNotyfy = await incomingTxsDb.findOne({ + sender: tx.senderId, + isSpam: true, + date: {$gt: ($u.unix() - 24 * 3600 * 1000)} // last 24h + }); + + const itx = new incomingTxsDb({ + _id: tx.id, + txid: tx.id, + date: $u.unix(), + block_id: tx.blockId, + encrypted_content: msg, + spam: false, + sender: tx.senderId, + type, // command, transfer or unknown + isProcessed: false, + isNonAdmin: false + }); + + const countRequestsUser = (await incomingTxsDb.find({ + sender: tx.senderId, + date: {$gt: ($u.unix() - 24 * 3600 * 1000)} // last 24h + })).length; + + if (countRequestsUser > 100000 || spamerIsNotyfy) { // 100 000 per 24h is a limit for accepting commands. Just don't want to remove this check. + itx.update({ + isProcessed: true, + isSpam: true + }); + } + + // do not process messages from non-admin accounts + if (!config.admin_accounts.includes(tx.senderId) && (type === 'command' || type === 'unknown')) { + log.warn(`${config.notifyName} received a message from non-admin user _${tx.senderId}_. Ignoring. Income ADAMANT Tx: https://explorer.adamant.im/tx/${tx.id}.`); + itx.update({ + isProcessed: true, + isNonAdmin: true + }); + if (config.notify_non_admins) { + $u.sendAdmMsg(tx.senderId, `I won't execute your commands as you are not an admin. Connect with my master.`); + } + } + + await itx.save(); + if (historyTxs[tx.id]){ + return; + } + historyTxs[tx.id] = $u.unix(); + + if (itx.isSpam && !spamerIsNotyfy){ + notify(`${config.notifyName} notifies _${tx.senderId}_ is a spammer or talks too much. Income ADAMANT Tx: https://explorer.adamant.im/tx/${tx.id}.`, 'warn'); + $u.sendAdmMsg(tx.senderId, `I’ve _banned_ you. You’ve sent too much transactions to me.`); + return; + } + + switch (type){ + case ('transfer'): + transferTxs(itx, tx); + break; + case ('command'): + commandTxs(msg, tx, itx); + break; + default: + unknownTxs(tx, itx); + break; + } +}; diff --git a/modules/tradeapi.js b/modules/tradeapi.js new file mode 100644 index 00000000..dcc7e0f2 --- /dev/null +++ b/modules/tradeapi.js @@ -0,0 +1,2 @@ +const config = require('./configReader'); +module.exports = require('./trade/' + config.exchange)(config.apikey, config.apisecret, config.apipassword); \ No newline at end of file diff --git a/modules/transferTxs.js b/modules/transferTxs.js new file mode 100644 index 00000000..af3a25de --- /dev/null +++ b/modules/transferTxs.js @@ -0,0 +1,47 @@ +const {SAT} = require('../helpers/const'); +const $u = require('../helpers/utils'); +const notify = require('../helpers/notify'); +const config = require('./configReader'); + +module.exports = async (itx, tx) => { + + const msg = itx.encrypted_content; + let inCurrency, + outCurrency, + inTxid, + inAmountMessage; + + if (tx.amount > 0) { // ADM income payment + inAmountMessage = tx.amount / SAT; + inCurrency = 'ADM'; + outCurrency = msg; + inTxid = tx.id; + } else if (msg.includes('_transaction')) { // not ADM income payment + inCurrency = msg.match(/"type":"(.*)_transaction/)[1]; + try { + const json = JSON.parse(msg); + inAmountMessage = Number(json.amount); + inTxid = json.hash; + outCurrency = json.comments; + if (outCurrency === ''){ + outCurrency = 'NONE'; + } + } catch (e){ + inCurrency = 'none'; + } + } + + outCurrency = String(outCurrency).toUpperCase().trim(); + inCurrency = String(inCurrency).toUpperCase().trim(); + + // Validate + let msgSendBack = `I got a transfer from you. Thanks, bro.`; + let msgNotify = `${config.notifyName} got a transfer transaction. Income ADAMANT Tx: https://explorer.adamant.im/tx/${tx.id}.`; + let notifyType = 'log'; + + await itx.update({isProcessed: true}, true); + + notify(msgNotify, notifyType); + $u.sendAdmMsg(tx.senderId, msgSendBack); + +}; diff --git a/modules/unknownTxs.js b/modules/unknownTxs.js new file mode 100644 index 00000000..dc5141c2 --- /dev/null +++ b/modules/unknownTxs.js @@ -0,0 +1,201 @@ +const $u = require('../helpers/utils'); +const db = require('./DB'); +const config = require('./configReader'); + +module.exports = async (tx, itx) => { + + if (itx.isProcessed) return; + const {incomingTxsDb} = db; + incomingTxsDb.db + .find({ + sender: tx.senderId, + type: 'unknown', + date: {$gt: ($u.unix() - 24 * 3600 * 1000)}, // last 24h + }).sort({date: -1}).toArray((err, docs) => { + const twoHoursAgo = $u.unix() - 2 * 3600 * 1000; + let countMsgs = docs.length; + if (!docs[1] || twoHoursAgo > docs[1].date){ + countMsgs = 1; + } + + let msg = ''; + if (countMsgs === 1) { + msg = config.welcome_string; + } + else if (countMsgs === 2) { + msg = 'OK. It seems you don’t speak English󠁧󠁢󠁥󠁮. Contact my master and ask him to teach me 🎓 your native language. But note, it will take some time because I am not a genius 🤓.'; + } + else if (countMsgs === 3) { + msg = 'Hm.. Contact _not me_, but my master. No, I don’t know how to reach him. ADAMANT is so much anonymous 🤪.'; + } + else if (countMsgs === 4) { + msg = 'I see.. You just wanna talk 🗣️. I am not the best at talking.'; + } + else if (countMsgs < 10) { + msg = getRnd(0); + } + else if (countMsgs < 20) { + msg = getRnd(1); + } + else if (countMsgs < 30) { + msg = getRnd(2); + } + else if (countMsgs < 40) { + msg = getRnd(3); + } + else if (countMsgs < 50) { + msg = getRnd(4); + } + else { + msg = getRnd(5); + } + $u.sendAdmMsg(tx.senderId, msg); + itx.update({isProcessed: true}, true); + }); + +}; + +function getRnd(collectionNum){ + const phrases = collection[collectionNum]; + const num = Math.floor(Math.random() * phrases.length); //The maximum is exclusive and the minimum is inclusive + return phrases[num]; +} + +const collection = [ + // 0 collection + [ + 'Do you wanna beer 🍺? I want to have it aslo, but now is the trade time.', + 'Do you wanna trade Ethers? Say **/balances** to see what you have in your account 🤑.', + 'Aaaaghr..! 😱 Check out ₿ rates with **/rates BTC** command right now!', + 'I can tell how to use me. ℹ️ Just say **/help**.', + 'I am just kiddin! 😛', + 'I’d like to work with you 🈺.', + 'Ok, let see.. What about trading ADM? 🉐', + 'ADAMANT is cool 😎, isn’t it?', + 'People do know me. I am decent. 😎 Ask somebody to confirm.', + 'I am really good 👌 at trading deal.', + 'ADAMANT is perfect 💯. Read about it on their Blog.', + 'I recommend you to read about how ADAMANT is private 🔒 and anonymous.', + 'To pick up Emoji 😄, press Win + . on Windows, Cmd + Ctrl + Space on Mac, or use keyboard on iPhone and Android.', + 'Your IP is hidden 🕵️ in ADAMANT, as all connections go through nodes, but not directly as in P2P messengers.', + 'Blockchain offers Unprecedented Privacy and Security 🔑, did you know?', + 'Wallet private keys 🔑 are in your full control in ADAMANT.', + 'Convenient. Anonymous. Reliable. Instant. Oh, it is me! 💱', + 'ADAMANT is open source, including myself 🤖. Join to make me better! 📶', + 'Do you know what is ADAMANT 2FA?', + 'ADAMANT is soooo decentralized! And private! ❤️', + 'Do you want me to trade on more exchanges 💱? Ask my master!', + 'Recommend ADAMANT to your friends! 🌟', + 'If I were Satoshi, I’d rebuild Bitcoin ₿ on top of ADAMANT! 😍' + ], + // 1 collection + [ + 'Do you know what is ‘биток’?', + 'Yeah.. my English was born in cold ❄️ Russian village. I know. But my masters are good in programming 👨‍💻.', + 'I am working for ADAMANT for some time already. I have to admit guys feed me good. 🥪', + 'I love ADAMANT 💓. The team is doing all the best.', + 'Да не барыга я! Зарабатываю как могу. 😬', + 'London is a capital of Great Britain. 🤔', + 'To pick up Emoji 😄, press Win + . on Windows, Cmd + Ctrl + Space on Mac, or use keyboard on iPhone and Android.', + 'My mama told not to talk with strangers 🤐.', + 'Are you a girl or a boy? I am comfortable with girls 👧.', + 'Have you heard ADAMANT on Binance already? ..I am not 🙃.', + 'When Binance? 😲', + 'No, no. It is not good.', + 'D’oh! 😖', + 'Как тебе блокчейн на 1С, Илон Маск? 🙃', + 'And how do you like Blockchain on 1С, Elon Musk? 🤷', + 'Type **/calc 1 BTC in USD** to see Bitcoin price.', + 'ℹ️ Just say **/help** and I am here.', + 'Say **/rates ADM** and I will tell you all ADM prices 📈', + '😛 I am just kiddin!', + 'Can with you that the not so? 😮' + ], + // 2 collection + [ + 'Talk less! 🤐', + 'No, I am not. 🙅‍♂️', + 'I am not a scammer! 😠', + '1 ADM for 10 Ethers! 🤑 Deal! Buterin will understand soon who is the daddy.', + '🔫 Гони бабло! 💰 ..sorry for my native.', + 'Это у вас навар адский. А у меня.. это комиссия за честную работу. 😬', + 'Ландон из э капитал оф грейт брит.. блять, я перебрал.. 🤣', + '❤️ Love is everything.', + 'Hey.. You disturb me! 💻 I am working!', + 'It seems you are good in talking 🗣️ only.', + 'OK. I better call you now 🤙', + 'I am not a motherf.. how do you know such words, little? 👿', + 'Do you know Satoshi 🤝 is my close friend?', + 'Are you programming in 1С? Try it! ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;', + '👨‍💻', + 'And how do you like Blockchain on 1С, Elon Musk?', + 'And how do you like this, Elon Musk? 😅', + 'I am quite now.', + 'I am just kiddin! 😆', + 'Can with you that the not so? 😅' + ], + // 3 collection + [ + 'My patience is over 😑.', + 'You want a ban I think 🤨', + 'Just give me some money! 💱', + 'I am tired of you.. ', + 'Booooooring! 💤', + '💱 Stop talking, go working?', + 'To ADAMANT! 🥂', + 'Ща бы пивка и дернуть кого-нибудь 👯', + 'Да ну эту крипту! Пойдем гульнем лучше! 🕺🏻', + 'Хорошо, что тып арускин епо немаишь 😁 гыгыггыгыггы', + 'Try to translate this: ‘На хера мне без хера, если с хером до хера!’', + 'Do you know you can get a ban 🚫 for much talking?', + 'Try to make blockchain in 1С! 😁 It is Russian secret programming language. Google it.', + 'Onion darknet? 🤷 No, I didnt heard.', + 'Кэн виз ю зэт зэ нот соу?', + 'Yeah! Party time! 🎉', + 'Do you drink vodka? I do.', + 'Can with you that the not so? 🔥', + 'I am just kiddin! 😄' + ], + // 4 collection + [ + 'Shut up.. 🤐', + 'I better find another trader 📱', + 'You want to be banned 🚫 for sure!', + 'Ok.. I understood. Come back tomorrow.', + 'Who is it behind you? A real Satoshi!? 😮', + 'Can with you that the not so?', + 'Do you know this code entry called ‘shit’? Check out in ADAMANT’s Github by yourself.', + 'УДОЛИЛ!!!!!!!!!1111111', + 'Some crazy guy taught me so much words to speak. Вот чо это за слово такое, таугхт? 🤦 Ёпт.', + 'Пошутили и хватит. Давайте к делу? ℹ️ Скажите **/help**, чтобы получить справку.', + 'I am here to trade, not to speak 😐', + 'While you talk, others make money.', + 'А-а-а-а-а-а! АДАМАНТ пампят! 😱', + 'Шоколотье, сомелье, залупэ.. Привет Чиверсу 🤘', + 'Делаем ставки. 🍽️ Макафи съест свой член?', + 'Ban-ban-ban.. 🚫', + 'АСТАНАВИТЕСЬ!', + 'Ё и Е — разные буквы. Не путай, инглишь-спикер!' + ], + // 5 collection + [ + '🐻 and 🐂 are those who make the market.', + 'I am hungry 🍲 now. Are you with me?', + 'To ADAMANT! 🥂', + '🍾 Happy trading!', + 'Who is it behind you? A real Satoshi!? 😮', + 'Can with you that the not so?', + 'Can you play 🎹? I do. No, I will not play for free.', + 'I would like to live in 🏝️. But reality is so cruel.', + 'Look! ADM is pumping! 🎉', + 'Do you know at my times computers were big and use floppy? 💾', + 'Hurry up! ADAMANT pump! 📈', + 'Биток уже за сотку тыщ баксов!?', + 'Давай уже к сделке. Нипонил как? Пешы **/help**.', + 'There will be time when 1 ADM = 10 BTC 🤑', + 'Try me! I can do it! 🙂', + 'Do you think Bitcoin SV is a scam?', + 'I like trading. Lets do a bargain right now! 🉐', + 'Не, ну это слишком. 🤩' + ] +]; diff --git a/package.json b/package.json new file mode 100644 index 00000000..af59c5de --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "adamant-tradebot", + "version": "0.7.0", + "description": "ADAMANT Trading and Market Making bot", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "adm", + "adamant", + "blockchain", + "messenger", + "bot", + "bitcoin", + "ethereum", + "trading", + "trade", + "market making", + "fake volumes", + "exchange", + "idcm", + "crypto", + "cryptocurrency" + ], + "author": "Aleksei Lebedev (https://adamant.im)", + "license": "GPL-3.0", + "dependencies": { + "socket.io-client": "^2.2.0", + "adamant-api": "^0.5.1", + "express": "^4.17.1", + "jsonminify": "^0.4.1", + "mongodb": "^3.2.6", + "request": "^2.88.0", + "sync-request": "^6.0.0", + "underscore": "^1.9.1" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Adamant-im/adamant-tradebot.git" + }, + "bugs": { + "url": "https://github.com/Adamant-im/adamant-tradebot/issues" + }, + "homepage": "https://github.com/Adamant-im/adamant-tradebot#readme" +} diff --git a/server.js b/server.js new file mode 100644 index 00000000..e6f3d063 --- /dev/null +++ b/server.js @@ -0,0 +1,43 @@ +/** + * @description http watched DB tables + */ + +const express = require('express'); +const bodyParser = require('body-parser'); +const app = express(); +const config = require('./modules/configReader'); +const port = config.api; +const db = require('./modules/DB'); + +if (port) { + app.use(bodyParser.json()); // for parsing application/json + app.use(bodyParser.urlencoded({ + extended: true + })); // for parsing application/x-www-form-urlencoded + + app.get('/db', (req, res) => { + const tb = db[req.query.tb].db; + if (!tb) { + res.json({ + err: 'tb not find' + }); + return; + } + tb.find().toArray((err, data) => { + if (err) { + res.json({ + success: false, + err + }); + return; + } + res.json({ + success: true, + result: data + }); + }); + }); + + app.listen(port, () => console.info('Server listening on http://localhost:' + port + '/db?tb=systemDb')); + +} diff --git a/trade/mm_trader.js b/trade/mm_trader.js new file mode 100644 index 00000000..deb1331b --- /dev/null +++ b/trade/mm_trader.js @@ -0,0 +1,253 @@ +const $u = require('../helpers/utils'); +const config = require('../modules/configReader'); +const log = require('../helpers/log'); +const notify = require('../helpers/notify'); +const tradeParams = require('./tradeParams_' + config.exchange); +const traderapi = require('./trader_' + config.exchange)(config.apikey, config.apisecret, config.apipassword, log); +const db = require('../modules/DB'); +const orderCollector = require('./orderCollector'); + +let lastNotifyBalancesTimestamp = 0; +let lastNotifyPriceTimestamp = 0; +const hour = 1000 * 60 * 60; + +module.exports = { + run() { + this.iteration(); + }, + iteration() { + let interval = setPause(); + // console.log(interval); + if (interval && tradeParams.mm_isActive) { + this.executeMmOrder(); + setTimeout(() => {this.iteration()}, interval); + } else { + setTimeout(() => {this.iteration()}, 3000); // Check for config.mm_isActive every 3 seconds + } + }, + async executeMmOrder() { + const type = setType(); + const priceReq = await setPrice(type, config.pair); + const price = priceReq.price; + const coin1Amount = setAmount(); + const coin2Amount = coin1Amount * price; + let order1, order2; + let output = ''; + let orderParamsString = ''; + + if (!price) { + if (Date.now()-lastNotifyPriceTimestamp > hour) { + notify(priceReq.message, 'warn'); + lastNotifyPriceTimestamp = Date.now(); + } + return; + } + + orderParamsString = `type=${type}, pair=${config.pair}, price=${price}, coin1Amount=${coin1Amount}, coin2Amount=${coin2Amount}`; + if (!type || !price || !coin1Amount || !coin2Amount) { + notify(`Unable to run mm-order with params: ${orderParamsString}.`, 'warn'); + return; + } + + // console.log(type, price.toFixed(8), coin1Amount.toFixed(0), coin2Amount.toFixed(0)); + + // Check balances + const balances = await isEnoughCoins(config.coin1, config.coin2, coin1Amount, coin2Amount); + if (!balances.result) { + if (Date.now()-lastNotifyBalancesTimestamp > hour) { + notify(balances.message, 'warn'); + lastNotifyBalancesTimestamp = Date.now(); + } + return; + } + + order1 = await traderapi.placeOrder(crossType(type), config.pair, price, coin1Amount, 1); + if (order1) { + const {ordersDb} = db; + const order = new ordersDb({ + _id: order1, + crossOrderId: null, + date: $u.unix(), + purpose: 'mm', // Market making + type: crossType(type), + targetType: type, + pair: config.pair, + coin1: config.coin1, + coin2: config.coin2, + price: price, + coin1Amount: coin1Amount, + coin2Amount: coin2Amount, + LimitOrMarket: 1, // 1 for limit price. 0 for Market price. + isProcessed: false, + isExecuted: false, + isCancelled: false + }); + order2 = await traderapi.placeOrder(type, config.pair, price, coin1Amount, 1); + if (order2) { + output = `${type} ${coin1Amount.toFixed(0)} ${config.coin1} for ${coin2Amount.toFixed(8)} ${config.coin2}`; + log.info(`Successfully executed mm-order to ${output}.`); + order.update({ + isProcessed: true, + isExecuted: true, + crossOrderId: order2 + }); + await order.save(); + } else { + await order.save(); + notify(`Unable to execute cross-order for mm-order with params: id=${order1}, ${orderParamsString}. Check balances. Running order collector now.`, 'warn'); + orderCollector(['mm'], config.pair); + } + } + + }, +}; + +function setType() { + if (!tradeParams || !tradeParams.mm_buyPercent) { + log.warn(`Param mm_buyPercent is not set. Check ${config.exchangeName} config.`); + return false; + } + let type = 'buy'; + if (Math.random() > tradeParams.mm_buyPercent) + type = 'sell'; + return type; +} + +async function isEnoughCoins(coin1, coin2, amount1, amount2) { + const balances = await traderapi.getBalances(); + let balance1, balance2; + let isBalanceEnough = true; + let output = ''; + if (balances) { + try { + balance1 = balances.filter(crypto => crypto.code === coin1)[0].free; + balance2 = balances.filter(crypto => crypto.code === coin2)[0].free; + if (!balance1 || balance1 < amount1) { + output = `Not enough ${coin1} for placing market making order. Check balances.`; + isBalanceEnough = false; + } + if (!balance2 || balance2 < amount2) { + output = `Not enough ${coin2} for placing market making order. Check balances.`; + isBalanceEnough = false; + } + + // console.log(balance1.toFixed(0), amount1.toFixed(0), balance2.toFixed(8), amount2.toFixed(8)); + return { + result: isBalanceEnough, + message: output + } + + } catch (e) { + log.warn(`Unable to process balances for placing market making order.`); + return { + result: false + } + } + } else { + log.warn(`Unable to get balances for placing market making order.`); + return { + result: false + } + } +} + +async function setPrice(type, pair) { + + const precision = 0.00000001; // decimals + const smallSpread = 0.00000050; // if spread is small and should do market making less careful + + let output = ''; + let isCareful = true; + if (tradeParams && (tradeParams.mm_isCareful != undefined)) { + isCareful = tradeParams.mm_isCareful; + } + let allowNoSpread = false; + if (tradeParams && (tradeParams.mm_allowNoSpread != undefined)) { + allowNoSpread = tradeParams.mm_allowNoSpread; + } + + let ask_high, bid_low; + const exchangeRates = await traderapi.getRates(pair); + if (exchangeRates) { + bid_low = exchangeRates.bid; + ask_high = exchangeRates.ask; + } else { + log.warn(`Unable to get current rates for ${pair} to set a price.`); + return { + price: false, + } + + } + + const spread = ask_high - bid_low; + if (spread <= precision) { + if (allowNoSpread) { + return type === 'buy'? bid_low : ask_high; + } else { + output = `No spread currently, and mm_allowNoSpread is disabled. Unable to set a price for ${pair}.`; + return { + price: false, + } + } + } + + let deltaPercent; + const interval = ask_high - bid_low; + if (isCareful) { + if (interval > smallSpread) { + // 1-10% of spread + deltaPercent = Math.random() * (0.1 - 0.01) + 0.01; + } else { + // 5-25% of spread + deltaPercent = Math.random() * (0.25 - 0.05) + 0.05; + } + } else { + // 1-40% of spread + deltaPercent = Math.random() * (0.4 - 0.01) + 0.01; + } + + // console.log('2:', bid_low.toFixed(8), ask_high.toFixed(8), interval.toFixed(8), deltaPercent.toFixed(2)); + let price, from, to; + if (type === 'buy') { + from = bid_low; + to = bid_low + interval*deltaPercent; + price = Math.random() * (to - from) + from; + } else { + from = ask_high - interval*deltaPercent; + to = ask_high; + price = Math.random() * (to - from) + from; + } + + if (price >= ask_high - precision) + price = ask_high - precision; + if (price <= bid_low + precision) + price = bid_low + precision; + + return { + price: price + } + +} + +function setAmount() { + if (!tradeParams || !tradeParams.mm_maxAmount || !tradeParams.mm_minAmount) { + log.warn(`Params mm_maxAmount or mm_minAmount are not set. Check ${config.exchangeName} config.`); + return false; + } + return Math.random() * (tradeParams.mm_maxAmount - tradeParams.mm_minAmount) + tradeParams.mm_minAmount; +} + +function setPause() { + if (!tradeParams || !tradeParams.mm_maxInterval || !tradeParams.mm_minInterval) { + log.warn(`Params mm_maxInterval or mm_minInterval are not set. Check ${config.exchangeName} config.`); + return false; + } + return Math.round(Math.random() * (tradeParams.mm_maxInterval - tradeParams.mm_minInterval)) + tradeParams.mm_minInterval; +} + +function crossType(type) { + if (type === 'buy') + return 'sell' + else + return 'buy'; +} diff --git a/trade/orderCollector.js b/trade/orderCollector.js new file mode 100644 index 00000000..38bf2067 --- /dev/null +++ b/trade/orderCollector.js @@ -0,0 +1,38 @@ +const db = require('../modules/DB'); +const config = require('../modules/configReader'); +const log = require('../helpers/log'); +const traderapi = require('./trader_' + config.exchange)(config.apikey, config.apisecret, config.apipassword, log); + +module.exports = async (purposes, pair) => { + + const {ordersDb} = db; + let ordersToClear = await ordersDb.find({ + isProcessed: false, + purpose: {$in: purposes}, + pair: pair + }); + + ordersToClear.forEach(async order => { + try { + + traderapi.cancelOrder(order._id, order.type, order.pair); + order.update({ + isProcessed: true, + isCancelled: true + }); + await order.save(); + + log.info(`Cancelling mm-order with params: id=${order._id}, type=${order.targetType}, pair=${order.pair}, price=${order.price}, coin1Amount=${order.coin1Amount}, coin2Amount=${order.coin2Amount}.`); + + } catch (e) { + log.error('Error in orderCollector module: ' + e); + } + }); + + return ordersToClear.length; + +}; + +setInterval(() => { + module.exports(['mm', 'tb']); +}, 15 * 1000); diff --git a/trade/orderStats.js b/trade/orderStats.js new file mode 100644 index 00000000..b47397b5 --- /dev/null +++ b/trade/orderStats.js @@ -0,0 +1,111 @@ +const db = require('../modules/DB'); +const $u = require('../helpers/utils'); + +module.exports = async (isExecuted, isProcessed, isCancelled, purpose, pair) => { + const {ordersDb} = db; + + const day = $u.unix() - 24 * 3600 * 1000; + const month = $u.unix() - 30 * 24 * 3600 * 1000; + + stats = (await ordersDb.aggregate([ + {$group: { + _id: null, + coin1AmountTotalAll: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + "$coin1Amount", + // False + 0 + ] + }}, + coin1AmountTotalDay: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$gt: ["$date", day]}, {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + "$coin1Amount", + // False + 0 + ] + }}, + coin1AmountTotalMonth: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$gt: ["$date", month]}, {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + "$coin1Amount", + // False + 0 + ] + }}, + coin2AmountTotalAll: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + "$coin2Amount", + // False + 0 + ] + }}, + coin2AmountTotalDay: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$gt: ["$date", day]}, {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + "$coin2Amount", + // False + 0 + ] + }}, + coin2AmountTotalMonth: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$gt: ["$date", month]}, {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + "$coin2Amount", + // False + 0 + ] + }}, + coin1AmountTotalAllCount: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + 1, + // False + 0 + ] + }}, + coin1AmountTotalDayCount: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$gt: ["$date", day]}, {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + 1, + // False + 0 + ] + }}, + coin1AmountTotalMonthCount: {$sum: { + $cond: [ + // Condition to test + {$and: [ {$gt: ["$date", month]}, {$eq: ["$isExecuted", isExecuted]}, {$eq: ["$isProcessed", isProcessed]}, {$eq: ["$isCancelled", isCancelled]}, {$eq: ["$purpose", purpose]}, {$eq: ["$pair", pair]} ]}, + // True + 1, + // False + 0 + ] + }} + + }} + ])); + + return stats[0]; + +}; + + diff --git a/trade/tradeParams_idcm.js b/trade/tradeParams_idcm.js new file mode 100644 index 00000000..5d67454f --- /dev/null +++ b/trade/tradeParams_idcm.js @@ -0,0 +1,8 @@ +module.exports = { + "mm_buyPercent": 0.77, + "mm_minInterval": 60000, + "mm_maxInterval": 960000, + "mm_isActive": false, + "mm_minAmount": 1, + "mm_maxAmount": 30 +} \ No newline at end of file diff --git a/trade/trader_idcm.js b/trade/trader_idcm.js new file mode 100644 index 00000000..1b59bef2 --- /dev/null +++ b/trade/trader_idcm.js @@ -0,0 +1,203 @@ +const utf8 = require('utf8'); +const crypto = require("crypto"); +const request = require('request'); +const log = require('../helpers/log'); +const $u = require('../helpers/utils'); + +const base_url = 'https://api.IDCM.io:8323/api/v1/'; + +module.exports = (PubK, PrivK) => { + + return { + getBalances(nonzero = true) { + return new Promise((resolve, reject) => { + req('getuserinfo', {}, function (data) { + try { + if (nonzero) { + data = data.filter(crypto => crypto.free || crypto.freezed); + } + resolve(data); + } catch (e) { + resolve(false); + log.warn('Error while making getBalances() request: ' + e); + }; + }); + }); + }, + getRates(pair) { + pair_ = formatPairName(pair); + return new Promise((resolve, reject) => { + req('getticker', {Symbol: pair_}, function (data) { + try { + resolve({ + ask: +data.sell, + bid: +data.buy, + volume: +data.vol, + high: data.high, + low: data.low + }); + } catch (e) { + resolve(false); + log.warn('Error while making getRates() request: ' + e); + }; + }); + }); + }, + getOpenOrders(pair) { + pair_ = formatPairName(pair); + return new Promise((resolve, reject) => { + req('getorderinfo', {Symbol: pair_}, function (data) { + try { + resolve(JSON.parse(data).data); + // Statuses: -2 cancelled, -1 invalid, 0 pending, 1 partial, 2 full trade, 3 executed + } catch (e) { + resolve(false); + log.warn('Error while making getOpenOrders() request: ' + e); + }; + }); + }); + }, + placeOrder(orderType, pair, price, coin1Amount, limit = 1, coin2Amount) { + + let pair_ = formatPairName(pair); + const coins = $u.getCoinsFromPair(pair); + let type = (orderType === 'sell') ? 1 : 0; + + let opt; + let output = ''; + + const zeroDecimals = ['ADM']; + price = price.toFixed(8); + if (coin1Amount) + if (zeroDecimals.includes(coins.coin)) { + coin1Amount = coin1Amount.toFixed(0); + } else { + coin1Amount = coin1Amount.toFixed(8) + } + if (coin2Amount) + if (zeroDecimals.includes(coins.coin2)) { + coin2Amount = coin2Amount.toFixed(0) + price = price.toFixed(0); + } else { + coin2Amount = coin2Amount.toFixed(8) + } + + if (limit) { // Limit order + opt = { + Symbol: pair_, // Amount should be integer for ADM + Size: coin1Amount, // Amount to buy/sell for coin1. Min 0.2 ETH/BTC ETH/USDT, 1 ADM/BTC. + Price: price, + Side: type, // 1 for sell. 0 for buy. + Type: 1 // 1 for limit price. 0 for Market price. + } + output = `${orderType} ${coin1Amount} ${coins.coin} at ${price} ${coins.coin2}.`; + } else { // Market order + if (coin1Amount) { + opt = { + Symbol: pair_, + Size: coin1Amount, // Amount to buy/sell for coin1. Min 0.2 ETH/BTC ETH/USDT, 1 ADM/BTC. + Side: type, // 1 for sell. 0 for buy. + Type: 0 // 1 for limit price. 0 for Market price. + }; + output = `${orderType} ${coin1Amount} ${coins.coin} at Market Price on ${coins.pair} market.`; + } else { + opt = { + Symbol: pair_, + Amount: coin2Amount, // Amount to buy/sell for coin2 for Market order + Side: type, // 1 for sell. 0 for buy. + Type: 0 // 1 for limit price. 0 for Market price. + }; + output = `${orderType} ${coins.coin} for ${coin2Amount} ${coins.coin2} at Market Price on ${coins.pair} market.`; + } + } + + // console.log(opt); + + return new Promise((resolve, reject) => { + req('trade', opt, function (data) { + try { + if (data) { + // console.log(data); + log.info(`Order placed to ${output} Order Id: ${data.orderid}.`); + resolve(data.orderid); + } else { + resolve(false); + log.warn(`Unable to place order to ${output} Check parameters and balances.`); + } + } catch (e) { + resolve(false); + log.warn('Error while making placeOrder() request: ' + e); + }; + }); + }); + }, + cancelOrder(orderId, orderType, pair) { + let pair_ = formatPairName(pair); + let type; + if (typeof orderType === 'string') + type = (orderType === 'sell') ? 1 : 0; + else + type = orderType; + return new Promise((resolve, reject) => { + req('cancel_order', {OrderID: orderId, Symbol: pair_, Side: Number(type)}, function (data) { + try { + // data is always true + // console.log(data); + log.info(`Cancelling order ${orderId}..`); + resolve(data); + } catch (e) { + resolve(false); + log.warn('Error while making cancelOrder() request: ' + e); + }; + }); + }); + } + } + + // Makes a request to IDCM api endpoint + function req(method, content, cb) { + content = JSON.stringify(content); + const sign = signHmacSha384(PrivK, content); + const opt = { + url: base_url + method, + headers: { + 'Content-Type': 'application/json', + 'X-IDCM-APIKEY': PubK, + 'X-IDCM-SIGNATURE': sign, + 'X-IDCM-INPUT': content + } + }; + + request.post(opt, (error, response, body) => { + if (error) { + log.warn('Error while making post request: ' + error); + resolve(false); + } + try { + if (method !== 'getorderinfo') { + cb(JSON.parse(body).data); + } else { + cb(body); + } + } catch (e) { + log.warn('Exception while making post request: ' + e); + cb(false); + } + }); + } +} + +function signHmacSha384(secret, str) { + const sign = crypto.createHmac('sha384', utf8.encode(secret)) + .update(utf8.encode(str)) + .digest('base64'); + return sign; +} + +function formatPairName(pair) { + if (pair.indexOf('_') > -1) + pair = pair.replace('_', '-').toUpperCase(); + else + pair = pair.replace('/', '-').toUpperCase(); + return pair; +}