├── .gitignore ├── README.md ├── app.js ├── exchanges ├── bitfinex.js ├── config-example.js └── poloniex.js ├── package.json ├── playground.js └── utils ├── logger.js └── messenger.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | config.js 31 | package-lock.json 32 | .idea 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | Cryptocurrency Arbitrage Bot is a node.js trading system that does automatic long/short arbitrage between the two biggest Bitcoin exchanges: [Poloniex](https://poloniex.com/) and [Bitfinex](https://www.bitfinex.com/). The purpose of this bot is to automatically profit from these temporary price differences whilst remaining market-neutral. 4 | 5 | Here is a real example where an arbitrage opportunity exists between Poloniex (long) and Bitfinex (short): 6 | 7 | ![image](http://i.imgur.com/t9Pnjz1.png) 8 | At the first vertical line, the spread between the exchanges is high so the bot buys Poloniex and short sells Bitfinex. Then, when the spread closes (second vertical line), the bot exits the market by selling Poloniex and buying Bitfinex back. Note that this methodology means that profits are realised *even in if the price of your asset decreases.* 9 | 10 | ### Advantages 11 | 12 | Unlike other Bitcoin arbitrage systems, this bot doesn't sell but actually short sells Bitcoin (and other Cryptos) on the short exchange. This feature offers two important advantages: 13 | 14 | 1. The strategy is market-neutral: the Bitcoin market's moves (up or down) don't impact the strategy returns. This removes a huge risk from the strategy. The Bitcoin market could suddenly lose twice its value that this won't make any difference in the strategy returns. 15 | 16 | 2. The strategy doesn't need to transfer funds (USD or BTC) between Bitcoin exchanges. The buy/sell and sell/buy trading activities are done in parallel on two different exchanges, independently. This means that there is no need to deal with transfer latency issues. 17 | 18 | A situational explanation can be found [in the wiki](https://github.com/joepegler/Cryptocurrency-Arbitrage-Bot/wiki) 19 | 20 | ### Installation 21 | 22 | This bot requires [Node.js](https://nodejs.org/) v4+ to run. 23 | 24 | Install the dependencies and devDependencies and start the server. 25 | 26 | ```sh 27 | $ npm install -d 28 | ``` 29 | 30 | To test the bot and investigate it's features: 31 | ```sh 32 | $ node playground 33 | ``` 34 | 35 | To start the bot and begin listening for opportunities: 36 | ```sh 37 | $ node app 38 | ``` -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const exchanges = { 4 | poloniex: require('./exchanges/poloniex'), 5 | bitfinex: require('./exchanges/bitfinex'), 6 | }; 7 | 8 | const logger = require('./utils/logger'); 9 | const messenger = require('./utils/messenger'); 10 | const _ = require('lodash'); 11 | const SUPPORTED_PAIRS = ['LTCBTC','ETHBTC','XRPBTC','XMRBTC','DSHBTC']; 12 | const OPPORTUNITY_THRESHOLD_PERCENTAGE = 1; 13 | let interval; 14 | 15 | 16 | (function init(){ 17 | Promise.all([exchanges.poloniex.init(), exchanges.bitfinex.init()]).then((messages)=>{ 18 | logger.log(messages[0]); 19 | logger.log(messages[1]); 20 | interval = setInterval(tick, 3000); 21 | }).catch(logger.error); 22 | function tick(){ 23 | getPrices(SUPPORTED_PAIRS).then(getOrderSize).then(messenger.broadcast).catch(logger.error); 24 | } 25 | }()); 26 | 27 | /* 28 | function placeOrders(orders){ 29 | return new Promise((resolve, reject) => { 30 | let short = orders[0]; 31 | let long = orders[1]; 32 | //pair, amount, price, side 33 | // exchange[short.exchangeName].order(short.pair, short.price); 34 | logger.log(long); 35 | logger.log(short); 36 | let message = `Placing a ${long.pair} buy order on ${long.exchangeName} at a price of ${long.price}. Placing a ${short.pair} sell order on ${short.exchangeName} at a price of ${short.price}.`; 37 | logger.log(message); 38 | resolve(); 39 | }); 40 | } 41 | */ 42 | 43 | function getOrderSize(opportunity){ 44 | /* 45 | * 46 | * Determines the order size by retrieving the balance. The min balance from both exchanges is used, and added to the opportunity object. 47 | * 48 | * { 49 | * pair: 'ETHUSD' 50 | * shortExchange: 'poloniex', 51 | * ... 52 | * orderSize: 1.713 53 | * } 54 | * 55 | * */ 56 | return new Promise((resolve, reject) => { 57 | const balancePromises = [ 58 | exchanges[opportunity.shortExchange].balance(opportunity.pair).catch(reject), 59 | exchanges[opportunity.longExchange].balance(opportunity.pair).catch(reject) 60 | ]; 61 | Promise.all(balancePromises).then(balances => { 62 | opportunity.orderSize = _.min(balances); 63 | resolve(opportunity); 64 | }).catch(reject); 65 | }); 66 | } 67 | 68 | function getPrices(pairs) { 69 | /* 70 | * 71 | * Returns the best available opportunity for arbitrage (if any). The delta is calculated as: 72 | * 73 | * 100 - (longExchange.lowestAsk / shortExchange.highestBid) 74 | * 75 | * because those are the prices that orders are most likely to be filled at. 76 | * 77 | * args: 78 | * 79 | * ['LTCBTC','ETHBTC','XRPBTC','XMRBTC','DASHBTC'] 80 | * 81 | * return: 82 | * 83 | * { 84 | * pair: 'ETHUSD' 85 | * shortExchange: 'poloniex', 86 | * longExchange: 'bitfinex', 87 | * shortExchangeAsk: 322, 88 | * shortExchangeBid: 320, 89 | * shortExchangeMid: 321, 90 | * longExchangeAsk: 305, 91 | * longExchangeBid: 301, 92 | * longExchangeMid: 303, 93 | * delta: 4.68, 94 | * } 95 | * 96 | * */ 97 | // logger.log('pairs: ' + JSON.stringify(pairs)); 98 | return new Promise((resolve, reject) => { 99 | const pricePromises = [ 100 | exchanges.poloniex.tick(pairs).catch(reject), 101 | exchanges.bitfinex.tick(pairs).catch(reject) 102 | ]; 103 | Promise.all(pricePromises).then(prices => { 104 | let opportunity = {}; 105 | let poloniexPrices = prices[0]; 106 | let bitfinexPrices = prices[1]; 107 | // prices = [{exchange: 'bitfinex', pair: 'ETHUSD', ask: 312, bid: 310, mid: 311}, {exchange: 'bitfinex', pair: 'LTCUSD', ask: 46, bid: 44, mid: 45}, {exchange: 'bitfinex', pair: 'BTCUSD', ask: 3800, bid: 3700, mid: 3750}] 108 | _.each(poloniexPrices, (poloniexPrice) =>{ 109 | _.each(bitfinexPrices, (bitfinexPrice) =>{ 110 | if(poloniexPrice.pair === bitfinexPrice.pair){ 111 | let ordered =_.sortBy([poloniexPrice, bitfinexPrice], ['mid']); 112 | let longExchange = ordered[0]; 113 | let shortExchange = ordered[1]; 114 | let delta = parseFloat(100 - (longExchange.ask / shortExchange.bid * 100)).toFixed(2); 115 | if ( delta > OPPORTUNITY_THRESHOLD_PERCENTAGE ){ 116 | // logger.log(`Opportunity found for ${shortExchange.pair}: [[${longExchange.ask}][${shortExchange.bid}] - [${delta}]]`); 117 | if((opportunity && (opportunity.delta < delta)) || _.isEmpty(opportunity)){ 118 | opportunity = { 119 | pair: poloniexPrice.pair, 120 | shortExchange: shortExchange.exchange, 121 | longExchange: longExchange.exchange, 122 | shortExchangeAsk: shortExchange.ask, 123 | shortExchangeBid: shortExchange.bid, 124 | shortExchangeMid: shortExchange.mid, 125 | longExchangeAsk: longExchange.ask, 126 | longExchangeBid: longExchange.bid, 127 | longExchangeMid: longExchange.mid, 128 | delta: delta, 129 | } 130 | } 131 | } 132 | else{ 133 | // logger.log(`No opportunity for ${shortExchange.pair}: [[${longExchange.ask}][${shortExchange.bid}] - [${delta}]]`); 134 | } 135 | } 136 | }) 137 | }); 138 | if(_.isEmpty(opportunity)){ 139 | reject('No opportunity.') 140 | } 141 | else{ 142 | resolve(opportunity); 143 | } 144 | }); 145 | }); 146 | } 147 | 148 | -------------------------------------------------------------------------------- /exchanges/bitfinex.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | "use strict"; 3 | 4 | const SETTINGS = require('./config')['BITFINEX']; 5 | const Promise = require('promise'); 6 | const _ = require('lodash'); 7 | const BFX = require('bitfinex-api-node'); 8 | const logger = require('../utils/logger'); 9 | const bitfinex = new BFX(SETTINGS.API_KEY, SETTINGS.API_SECRET, {version: 1, transform: true}).rest; 10 | const bitfinex_websocket = new BFX('', '', { version: 2, transform: true }).ws; 11 | const devMode = process.argv.includes('dev'); 12 | let prices = {}, dollarBalance = 0; 13 | 14 | return { 15 | tick: (pairArray) => { 16 | /* 17 | * 18 | * Returns an array of price values corresponding to the pairArray provided. e.g. 19 | * 20 | * args: 21 | * ['ETHUSD', 'LTCUSD', 'BTCUSD'] 22 | * 23 | * returns: 24 | * [{exchange: 'bitfinex', pair: 'ETHUSD', ask: 312, bid: 310, mid: 311}, {exchange: 'bitfinex', pair: 'LTCUSD', ask: 46, bid: 44, mid: 45}, {exchange: 'bitfinex', pair: 'BTCUSD', ask: 3800, bid: 3700, mid: 3750}] 25 | * 26 | * */ 27 | return new Promise((resolve) => { 28 | resolve(pairArray.map(pair => {return prices[pair];})); 29 | }); 30 | }, 31 | balance(pair) { 32 | /* 33 | * 34 | * Returns a single float value of approximate balance of the selected coin. 35 | * It is slightly adjust to give a margin of error for the exchange rate e.g. 36 | * 37 | * args: 38 | * 'LTC' 39 | * 40 | * returns: 41 | * 1.235 42 | * 43 | * */ 44 | return new Promise((resolve, reject) => { 45 | if(_.isNumber(dollarBalance)) { 46 | // For bitfinex we must translate the price to bitcoin first. 47 | let bitcoinPrice = _.find(prices, {pair: 'BTCUSD'}).mid; 48 | let pairPriceInBitcoin = _.find(prices, {pair: pair}).mid; 49 | let bitcoinBalance = parseFloat( dollarBalance / bitcoinPrice ); 50 | let coinBalance = parseFloat( bitcoinBalance / pairPriceInBitcoin ); 51 | resolve(coinBalance); 52 | } 53 | else{ 54 | reject(`Bitfinex could retrieve the balance`) 55 | } 56 | }); 57 | }, 58 | order(pair, amount, price, side) { 59 | /* 60 | * 61 | * Place an order 62 | * 63 | * */ 64 | return new Promise((resolve, reject) => { 65 | // [symbol, amount, price, exchange, side, type, is_hidden, postOnly, cb] 66 | bitfinex.new_order(SETTINGS.COINS[pair], amount.toFixed(7), price.toFixed(9), 'bitfinex', side, 'limit', (err, data) => { 67 | if (!err) { 68 | // {"id":3341017504,"cid":1488258364,"cid_date":"2017-08-13","gid":null,"symbol":"ethbtc","exchange":"bitfinex","price":"0.078872","avg_execution_price":"0.0","side":"sell","type":"limit","timestamp":"1502583888.325827284","is_live":true,"is_cancelled":false,"is_hidden":false,"oco_order":null,"was_forced":false,"original_amount":"0.01","remaining_amount":"0.01","executed_amount":"0.0","src":"api","order_id":3341017504} 69 | resolve(data); 70 | } 71 | else { 72 | logger.error(err); 73 | reject(err); 74 | } 75 | }); 76 | }); 77 | }, 78 | init(){ 79 | /* 80 | * 81 | * Initiating the exchange will start the ticker and also retrieve the balance for trading. 82 | * It returns a simple success message (String) 83 | * 84 | * */ 85 | let once; 86 | return new Promise((resolve, reject) => { 87 | const invertedMap = (_.invert(SETTINGS.COINS)); 88 | bitfinex_websocket.on('open', () => { 89 | _.each(SETTINGS.COINS, pair => { 90 | bitfinex_websocket.subscribeTicker(pair); 91 | }); 92 | bitfinex_websocket.on('ticker', (ePair, ticker) => { 93 | let pair = invertedMap[ePair.substring(1)]; 94 | prices[pair] = { 95 | exchange: 'bitfinex', 96 | pair: pair, 97 | ask: parseFloat(ticker.ASK) + (devMode ? (parseFloat(ticker.ASK) * .02) : 0), 98 | bid: parseFloat(ticker.BID) + (devMode ? (parseFloat(ticker.ASK) * .02) : 0), 99 | mid: parseFloat((parseFloat(ticker.ASK) + parseFloat(ticker.BID)) / 2) 100 | }; 101 | if (!once) { 102 | once = true; 103 | bitfinex.margin_infos((err, data) => { 104 | if (!err) { 105 | try { 106 | //[{"margin_balance":"72.84839221","tradable_balance":"182.120980525","unrealized_pl":"0.0","unrealized_swap":"0.0","net_value":"72.84839221","required_margin":"0.0","leverage":"2.5","margin_requirement":"13.0","margin_limits":[{"on_pair":"BTCUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"LTCUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"LTCBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"ETHUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"ETHBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"ETCBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"ETCUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"ZECUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"ZECBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"XMRUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"XMRBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"DSHUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"DSHBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"IOTUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"IOTBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"IOTETH","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"EOSUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"EOSBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"EOSETH","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"OMGUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"OMGBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"OMGETH","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"BCHUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"BCHBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"BCHETH","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"}],"message":"Margin requirement, leverage and tradable balance are now per pair. Values displayed in the root of the JSON message are incorrect (deprecated). You will find the correct ones under margin_limits, for each pair. Please update your code as soon as possible."}] 107 | dollarBalance = parseFloat(data[0].margin_balance); 108 | resolve(`Successfully initiated bitfinex. Your balance is: ${dollarBalance} Dollars. `); 109 | } 110 | catch (e) { 111 | reject(e); 112 | } 113 | } 114 | else { 115 | reject(err); 116 | } 117 | }); 118 | } 119 | }); 120 | }); 121 | }); 122 | } 123 | }; 124 | })(); -------------------------------------------------------------------------------- /exchanges/config-example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | POLONIEX : { 3 | API_KEY: 'X', 4 | API_SECRET: 'X', 5 | COINS: { 6 | BTCUSD:'BTC_USDT', 7 | LTCBTC:'BTC_LTC', 8 | ETHBTC:'BTC_ETH', 9 | XRPBTC:'BTC_XRP', 10 | XMRBTC:'BTC_XMR', 11 | DASHBTC:'BTC_DASH' 12 | } 13 | }, 14 | BITFINEX : { 15 | API_KEY: 'X', 16 | API_SECRET: 'X', 17 | COINS: { 18 | LTCBTC:'LTCBTC', 19 | ETHBTC:'ETHBTC', 20 | XRPBTC:'XRPBTC', 21 | XMRBTC:'XMRBTC', 22 | DASHBTC:'DSHBTC' 23 | } 24 | } 25 | }; -------------------------------------------------------------------------------- /exchanges/poloniex.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | "use strict"; 3 | const SETTINGS = require('./config')['POLONIEX']; 4 | const Promise = require('promise'); 5 | const Poloniex = require('poloniex-api-node'); 6 | const poloniex = new Poloniex(SETTINGS.API_KEY, SETTINGS.API_SECRET); 7 | const _ = require('lodash'); 8 | const logger = require('../utils/logger'); 9 | const devMode = process.argv.includes('dev'); 10 | let bitcoinBalance, prices = {}; 11 | return { 12 | tick: (pairArray) => { 13 | /* 14 | * 15 | * Returns an array of price values corresponding to the pairArray provided. e.g. 16 | * 17 | * args: 18 | * ['ETHUSD', 'LTCUSD', 'BTCUSD'] 19 | * 20 | * return: 21 | * [{exchange: 'poloniex', pair: 'ETHUSD', ask: 312, bid: 310, mid: 311}, {exchange: 'poloniex', pair: 'LTCUSD', ask: 46, bid: 44, mid: 45}, {exchange: 'poloniex', pair: 'BTCUSD', ask: 3800, bid: 3700, mid: 3750}] 22 | * 23 | * */ 24 | return new Promise((resolve, reject) => { 25 | poloniex.returnTicker((err, data) => { 26 | if(!err && !data.error){ 27 | resolve(pairArray.map(pair => { 28 | let coin = data[SETTINGS.COINS[pair]]; 29 | return { 30 | exchange: 'poloniex', 31 | pair: pair, 32 | ask: parseFloat(coin.lowestAsk), 33 | bid: parseFloat(coin.highestBid), 34 | mid: parseFloat(((parseFloat(coin.lowestAsk) + parseFloat(coin.highestBid))/2)) 35 | }; 36 | })); 37 | } 38 | else{ 39 | logger.error('Poloniex'); 40 | reject(err || _.get('data.error')); 41 | } 42 | }); 43 | }); 44 | }, 45 | balance(pair) { 46 | /* 47 | * 48 | * Returns a single float value of approximate balance of the selected coin. e.g. 49 | * 50 | * args: 51 | * 'LTC' 52 | * 53 | * returns: 54 | * 1.235 55 | * 56 | * */ 57 | return new Promise((resolve, reject) => { 58 | if(_.isNumber(bitcoinBalance)) { 59 | let pairPriceInBitcoin = _.find(prices, {pair: pair}).mid; 60 | let coinBalance = parseFloat( bitcoinBalance / pairPriceInBitcoin ); 61 | resolve(coinBalance); 62 | } 63 | else{ 64 | reject(`Bitfinex could retrieve the balance`) 65 | } 66 | }); 67 | }, 68 | order(pair, amount, price, side) { 69 | /* 70 | * 71 | * Place an order 72 | * 73 | * */ 74 | return new Promise((resolve, reject) => { 75 | poloniex[side==='buy'?'marginBuy':'marginSell'](SETTINGS.COINS[pair], price, amount, null , (err, data) => { 76 | if(!err){ 77 | resolve(data); 78 | } 79 | else{ 80 | reject(err || _.get('data.error')); 81 | } 82 | }); 83 | }); 84 | }, 85 | init: function(){ 86 | /* 87 | * 88 | * Initiating the exchange will start the ticker and also retrieve the balance for trading. 89 | * It returns a simple success message (String) 90 | * 91 | * */ 92 | const that = this; 93 | return new Promise((resolve, reject) => { 94 | that.tick(Object.keys(SETTINGS.COINS)).then(_prices => { 95 | prices = _prices; 96 | poloniex.returnMarginAccountSummary((err, data) => { 97 | if (!err && !data.error) { 98 | // {"totalValue":"0.01761446","pl":"0.00000000","lendingFees":"0.00000000","netValue":"0.01761446","totalBorrowedValue":"0.00000000","currentMargin":"1.00000000"} 99 | bitcoinBalance = parseFloat(data.totalValue); 100 | resolve(`Successfully initiated poloniex. Your balance is: ${bitcoinBalance} Bitcoin. `); 101 | } 102 | else{ 103 | reject(`Poloniex couldn't retrieve the balance`); 104 | } 105 | }); 106 | }).catch(reject); 107 | }); 108 | } 109 | }; 110 | 111 | })(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moneymaker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node playground.js 'dev'", 8 | "start": "node app.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bitfinex-api-node": "^1.0.2", 14 | "gdax": "^0.4.2", 15 | "kraken-api": "git+ssh://git@github.com/nothingisdead/npm-kraken-api.git", 16 | "kraken-exchange-api": "^0.2.5", 17 | "lodash": "^4.17.4", 18 | "node-telegram-bot-api": "^0.27.1", 19 | "node.bittrex.api": "^0.4.3", 20 | "poloniex-api-node": "^1.3.1", 21 | "promise": "^8.0.1" 22 | }, 23 | "devDependencies": { 24 | "prompt": "^1.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /playground.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const poloniex = require('./exchanges/poloniex'); 4 | const bitfinex = require('./exchanges/bitfinex'); 5 | 6 | const prompt = require('prompt'); 7 | const logger = require('./utils/logger'); 8 | const Promise = require('promise'); 9 | 10 | const SUPPORTED_PAIRS = ['BTCUSD', 'LTCBTC','ETHBTC','XRPBTC','XMRBTC','DSHBTC']; 11 | const devMode = process.argv.includes('dev'); 12 | // devMode && logger.log('Development mode: ' + devMode); 13 | 14 | prompt.start(); 15 | 16 | function win(message){ 17 | logger.log(message); 18 | getUserInput(); 19 | } 20 | 21 | function fail(message){ 22 | logger.error(message); 23 | getUserInput(); 24 | } 25 | 26 | function getUserInput(){ 27 | logger.log('\nPick an action: \n1) Ticker\n2) Balance\n3) Order\n4) Back\n5) Quit'); 28 | prompt.get(['action'], function (err, res) { 29 | if (!err) { 30 | switch(parseInt(res.action)){ 31 | case 1: 32 | chooseExchange().then(exchange => { 33 | logger.log('Getting price for following pairs: ' + JSON.stringify(SUPPORTED_PAIRS)); 34 | exchange.tick(SUPPORTED_PAIRS).then(win).catch(fail); 35 | }).catch(fail); 36 | break; 37 | case 2: 38 | chooseExchange().then(exchange => { 39 | logger.log('Getting balance.'); 40 | choosePair().then(pair => { 41 | logger.log('Pair selected: ' + pair); 42 | exchange.balance(pair).then(win).catch(fail); 43 | }).catch(fail); 44 | }).catch(fail); 45 | break; 46 | case 3: 47 | logger.log('Placing an order'); 48 | chooseExchange().then(exchange => { 49 | choosePair().then(pair => { 50 | logger.log('Pair selected: ' + pair); 51 | exchange.tick([pair]).then(priceArr => { 52 | exchange.balance(pair).then(coinBalance => { 53 | logger.log('Retrieved coin balance: ' + coinBalance); 54 | longOrShort().then(longOrShort => { 55 | let price = longOrShort === 'buy' ? priceArr[0].bid : priceArr[0].ask; 56 | let message = `\nPair: ${pair}. \nAmount: ${coinBalance}. \nPrice (${longOrShort === 'buy' ? 'highest bid' : 'lowest ask' }): ${price}. \nBuyOrSell: ${longOrShort}\n`; 57 | logger.error('[WARNING] You are about to place an order'); 58 | logger.log(message); 59 | confirm().then(yesNo => { 60 | if (yesNo === 'yes'){ 61 | logger.log('\nPlacing an order with the following details: '); 62 | logger.log(message); 63 | exchange.order(pair, coinBalance, price, longOrShort).then(win).catch(fail); 64 | } 65 | else{ 66 | fail('Aborted'); 67 | } 68 | }).catch(fail) 69 | }).catch(fail); 70 | }).catch(fail); 71 | }).catch(fail); 72 | }).catch(fail); 73 | }).catch(fail); 74 | break; 75 | case 4: 76 | win('Choose Again...'); 77 | break; 78 | case 5: 79 | process.exit(); 80 | break; 81 | default: 82 | fail('Invalid choice'); 83 | } 84 | } 85 | else{ 86 | fail(err); 87 | } 88 | }); 89 | } 90 | 91 | function chooseExchange(){ 92 | return new Promise((resolve, reject) => { 93 | logger.log('\nPick an exchange: \n1) Poloniex\n2) Bitfinex\n3) Back\n4) Quit'); 94 | prompt.get(['exchange'], (err, res) => { 95 | if (!err && res.exchange) { 96 | switch (parseInt(res.exchange)){ 97 | case 1: 98 | return resolve(poloniex); 99 | break; 100 | case 2: 101 | return resolve(bitfinex); 102 | break; 103 | case 3: 104 | reject('Choose Again...'); 105 | break; 106 | case 4: 107 | process.exit(); 108 | break; 109 | default: 110 | reject('Invalid choice'); 111 | } 112 | } 113 | }); 114 | }); 115 | } 116 | 117 | function longOrShort(){ 118 | return new Promise((resolve, reject) => { 119 | logger.log('\nBuy or Sell: \n1) Long\n2) Short\n3) Back\n4) Quit'); 120 | prompt.get(['longOrShort'], (err, res) => { 121 | if (!err && res.longOrShort) { 122 | switch (parseInt(res.longOrShort)){ 123 | case 1: 124 | return resolve('buy'); 125 | break; 126 | case 2: 127 | return resolve('sell'); 128 | break; 129 | case 3: 130 | reject('Choose Again...'); 131 | break; 132 | case 4: 133 | process.exit(); 134 | break; 135 | default: 136 | reject('Invalid choice'); 137 | } 138 | } 139 | }); 140 | }); 141 | } 142 | 143 | function confirm(){ 144 | return new Promise((resolve, reject) => { 145 | logger.log('\nAre you sure? \n1) Yes\n2) No\n3) Back\n4) Quit'); 146 | prompt.get(['sure'], (err, res) => { 147 | if (!err && res.sure) { 148 | switch (parseInt(res.sure)){ 149 | case 1: 150 | return resolve('yes'); 151 | break; 152 | case 2: 153 | return resolve('no'); 154 | break; 155 | case 3: 156 | reject('Choose Again...'); 157 | break; 158 | case 4: 159 | process.exit(); 160 | break; 161 | default: 162 | reject('Invalid choice'); 163 | } 164 | } 165 | }); 166 | }); 167 | } 168 | 169 | function choosePair(){ 170 | return new Promise((resolve, reject) => { 171 | logger.log(`\nPick a coin: \n1) ${SUPPORTED_PAIRS[1]}\n2) ${SUPPORTED_PAIRS[2]}\n3) ${SUPPORTED_PAIRS[3]}\n4) ${SUPPORTED_PAIRS[4]}\n5) Back \n6) Quit`); 172 | prompt.get(['pair'], (err, res) => { 173 | if (!err && res.pair) { 174 | switch (parseInt(res.pair)){ 175 | case 1: 176 | return resolve(SUPPORTED_PAIRS[1]); 177 | break; 178 | case 2: 179 | return resolve(SUPPORTED_PAIRS[2]); 180 | break; 181 | case 3: 182 | return resolve(SUPPORTED_PAIRS[3]); 183 | break; 184 | case 4: 185 | return resolve(SUPPORTED_PAIRS[4]); 186 | break; 187 | case 5: 188 | reject('Choose Again...'); 189 | break; 190 | case 6: 191 | process.exit(); 192 | break; 193 | default: 194 | reject('Invalid choice'); 195 | } 196 | } 197 | }); 198 | }); 199 | } 200 | 201 | Promise.all([poloniex.init(), bitfinex.init()]).then((messages)=>{ 202 | logger.log(messages[0]); 203 | logger.log(messages[1]); 204 | getUserInput(); 205 | }).catch(logger.error); -------------------------------------------------------------------------------- /utils/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | log: function(message){ 5 | if( typeof message === 'object'){ 6 | console.log(JSON.stringify(message)); 7 | } 8 | else { 9 | console.log(message); 10 | } 11 | }, 12 | error: function(err) { 13 | if (typeof err === 'object') { 14 | console.error(); 15 | console.error(JSON.stringify(err)); 16 | } 17 | else { 18 | console.error(); 19 | console.error(err); 20 | } 21 | } 22 | }; -------------------------------------------------------------------------------- /utils/messenger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const TelegramBot = require('node-telegram-bot-api'); 4 | const TELELGRAM_KEY = '380504145:AAF16WbEqFSaKtP6ZidJE6mxUD9QmU3tePc'; 5 | const CHAT_ID = '-237675778'; 6 | const devMode = process.argv.includes('dev'); 7 | const logger = require('./logger'); 8 | 9 | module.exports = (() => { 10 | const bot = new TelegramBot(TELELGRAM_KEY); 11 | return { 12 | broadcast: (message) => { 13 | if(message){ 14 | message = typeof message === 'object' ? JSON.stringify(message) : message; 15 | if(!devMode) { 16 | bot.sendMessage(CHAT_ID, message); 17 | } 18 | else{ 19 | logger.log(message); 20 | } 21 | } 22 | } 23 | }; 24 | })(); --------------------------------------------------------------------------------