├── actions └── buyallthecoins │ ├── excluded_coins.txt │ └── index.js ├── commands ├── coinx-update.js ├── coinx-unlock.js ├── coinx-action.js ├── coinx.js ├── coinx-config.js ├── coinx-lock.js ├── coinx-price.js ├── coinx-core.js ├── coinx-buy.js └── coinx-funds.js ├── lib ├── cryptocompare.js ├── coinmarketcap.js ├── liqui.js ├── poloniex.js ├── bitfinex.js ├── binance.js ├── kraken.js └── bittrex.js ├── LICENSE ├── .gitignore ├── package.json ├── CHANGELOG.md └── README.md /actions/buyallthecoins/excluded_coins.txt: -------------------------------------------------------------------------------- 1 | USDT 2 | BCC 3 | VERI -------------------------------------------------------------------------------- /commands/coinx-update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const coinx = require('./coinx-core'); 4 | const program = require('commander'); 5 | const chalk = require('chalk'); 6 | const homedir = require('homedir'); 7 | const path = require('path'); 8 | const coinmarketcap = require('../lib/coinmarketcap'); 9 | 10 | console.log(chalk.blue('Updating coin list...')); 11 | 12 | coinmarketcap.getList().then( data => { 13 | let coins = {}; 14 | data.forEach(coin => { 15 | coins[coin.symbol] = coin; 16 | }) 17 | coinx.coins(coins); 18 | console.log(chalk.green('Coin list updated.')); 19 | }); 20 | -------------------------------------------------------------------------------- /commands/coinx-unlock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const coinx = require('./coinx-core'); 4 | const program = require('commander'); 5 | const chalk = require('chalk'); 6 | const crypto = require('crypto'); 7 | 8 | const algorithm = 'aes-256-ctr'; 9 | 10 | function encrypt(text, password) { 11 | var cipher = crypto.createCipher(algorithm, password) 12 | var crypted = cipher.update(text, 'utf8', 'hex') 13 | crypted += cipher.final('hex'); 14 | return crypted; 15 | } 16 | 17 | program.parse(process.argv); 18 | 19 | var password = program.args[0]; 20 | 21 | if (!password) { 22 | console.log(chalk.red('Provide a password to unlock your config.')); 23 | process.exit(1); 24 | } 25 | 26 | let hash = encrypt(password, password); 27 | 28 | coinx.unlockConfig(hash); -------------------------------------------------------------------------------- /commands/coinx-action.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const program = require('commander'); 4 | const chalk = require('chalk'); 5 | const fs = require('fs-extra'); 6 | const path = require('path'); 7 | 8 | const coinx = require('./coinx-core'); 9 | 10 | 11 | program.parse(process.argv); 12 | 13 | let actionName = program.args[0]; 14 | let actionPath = path.join(coinx.actionPath(), actionName, 'index.js'); 15 | let actionExists = fs.existsSync(actionPath); 16 | 17 | if (!actionExists){ 18 | badAction(); 19 | } 20 | 21 | let action; 22 | try { 23 | action = require(actionPath); 24 | } catch (e){ 25 | console.log(e); 26 | badAction(); 27 | } 28 | 29 | action.run(program); 30 | 31 | 32 | function badAction(){ 33 | console.log(chalk.red('Could not find that action.')); 34 | process.exit(1); 35 | } -------------------------------------------------------------------------------- /commands/coinx.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var program = require('commander'); 3 | 4 | program 5 | .version(require('../package.json').version) 6 | .command('price [symbol]', 'get the price of a coin from all exchanges').alias('p') 7 | .command('buy [symbol]', 'buy a coin from an exchange. Auto finds the best price.').alias('b') 8 | .command('action [name]', 'run an automated action, such as buying multiple coins.').alias('a') 9 | .command('config [exchange]', 'set your api keys for an exchange').alias('c') 10 | .command('funds', 'get a list of your funds from the exchanges').alias('f') 11 | .command('update', 'updates the list of known coins').alias('u') 12 | .command('lock', 'encrypts configuration file with a password').alias('l') 13 | .command('unlock', 'decrypts configuration file with a password').alias('u') 14 | .parse(process.argv); 15 | -------------------------------------------------------------------------------- /lib/cryptocompare.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const rp = require('request-promise'); 3 | 4 | class CryptoCompare{ 5 | constructor(){ 6 | this.url = 'https://min-api.cryptocompare.com/data/'; 7 | }; 8 | 9 | // https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=BTC,USD,EUR 10 | price(symbol, quoteCurrency){ 11 | var options = { 12 | url: this.url + 'price', 13 | json: true, 14 | qs: { 15 | fsym: symbol, 16 | tsyms: quoteCurrency 17 | } 18 | } 19 | return rp(options).then( data => { 20 | return data[quoteCurrency]; 21 | }); 22 | }; 23 | 24 | priceMulti(symbols, quoteCurrency){ 25 | var options = { 26 | url: this.url + 'pricemulti', 27 | json: true, 28 | qs: { 29 | fsyms: symbols.join(','), 30 | tsyms: quoteCurrency 31 | } 32 | } 33 | 34 | return rp(options).then( data => { 35 | let results = {}; 36 | Object.keys(data).forEach( symbol => { 37 | results[symbol] = data[symbol][quoteCurrency]; 38 | }); 39 | return results; 40 | }); 41 | } 42 | } 43 | 44 | module.exports = new CryptoCompare(); 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 John Titus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # ignore test.js 61 | test.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coinx", 3 | "version": "0.11.2", 4 | "description": "Buy and sell crypto-currencies from the command line.", 5 | "main": "coinx.js", 6 | "bin": { 7 | "coinx": "./commands/coinx.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:johntitus/coinx.git" 12 | }, 13 | "keywords": [ 14 | "bitcoin", 15 | "crypto", 16 | "currencies" 17 | ], 18 | "author": "John Titus", 19 | "license": "MIT", 20 | "dependencies": { 21 | "binance": "^1.0.2", 22 | "bitfinex-api-node": "^1.0.2", 23 | "bluebird": "^3.5.0", 24 | "capitalize": "^1.0.0", 25 | "chalk": "^2.0.1", 26 | "columnify": "^1.5.4", 27 | "commander": "^2.10.0", 28 | "fs-extra": "^4.0.0", 29 | "homedir": "^0.6.0", 30 | "inquirer": "^3.1.1", 31 | "json2csv": "^3.9.1", 32 | "kraken-api": "git+https://github.com/johntitus/npm-kraken-api.git", 33 | "lodash": "^4.17.4", 34 | "mkdirp": "^0.5.1", 35 | "moment": "^2.18.1", 36 | "node.bittrex.api": "^0.3.2", 37 | "node.liqui.io": "^1.0.0", 38 | "poloniex.js": "0.0.7", 39 | "request": "^2.81.0", 40 | "request-promise": "^4.2.1", 41 | "zxcvbn": "^4.4.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/coinmarketcap.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const rp = require('request-promise'); 3 | 4 | class CoinMarketCap { 5 | constructor() { 6 | this.url = 'https://api.coinmarketcap.com/v1/ticker/'; 7 | }; 8 | 9 | getList(limit = 500) { 10 | var options = { 11 | url: this.url, 12 | json: true, 13 | qs: { 14 | limit: limit 15 | } 16 | } 17 | return rp(options).then(data => { 18 | let coins = data.map(coin => { 19 | return { 20 | id: coin.id, 21 | name: coin.name, 22 | symbol: coin.symbol, 23 | rank: parseInt(coin.rank), 24 | price_usd: parseFloat(coin.price_usd), 25 | price_btc: parseFloat(coin.price_btc), 26 | '24h_volume_usd': parseFloat(coin['24h_volume_usd']), 27 | market_cap_usd: parseFloat(coin.market_cap_usd), 28 | available_supply: parseFloat(coin.available_supply), 29 | total_supply: parseFloat(coin.total_supply), 30 | percent_change_1h: parseFloat(coin.percent_change_1h), 31 | percent_change_7d: parseFloat(coin.percent_change_7d), 32 | percent_change_24h: parseFloat(coin.percent_change_24h), 33 | last_updated: coin.last_updated 34 | } 35 | }); 36 | return coins; 37 | }); 38 | } 39 | } 40 | 41 | module.exports = new CoinMarketCap(); 42 | -------------------------------------------------------------------------------- /commands/coinx-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const coinx = require('./coinx-core'); 4 | const program = require('commander'); 5 | const chalk = require('chalk'); 6 | const capitalize = require('capitalize'); 7 | const inquirer = require('inquirer'); 8 | const validExchanges = Object.keys(coinx.exchanges()); 9 | 10 | program 11 | .option('-d, --delete', 'Delete config for the exchange.') 12 | .parse(process.argv); 13 | 14 | var exchange = program.args; 15 | 16 | if (!exchange.length || validExchanges.indexOf(exchange[0]) == -1) { 17 | console.error(chalk.red('Please specify an exchange: ' + validExchanges.join(', '))); 18 | process.exit(1); 19 | } 20 | exchange = exchange[0]; 21 | 22 | if (program.delete){ 23 | let config = coinx.config(); 24 | if (config[exchange]){ 25 | delete config[exchange]; 26 | coinx.config(config); 27 | console.log(chalk.green('Deleted data for ' + capitalize(exchange))); 28 | } else { 29 | console.log(chalk.green('Data not found for ' + capitalize(exchange))); 30 | } 31 | process.exit(0); 32 | } 33 | 34 | let questions = [ 35 | { 36 | name: 'apiKey', 37 | message: capitalize(exchange) + ' API Key' 38 | }, 39 | { 40 | name: 'apiSecret', 41 | message: capitalize(exchange) + ' API Secret' 42 | } 43 | ]; 44 | 45 | inquirer 46 | .prompt(questions) 47 | .then( results => { 48 | let config = coinx.config(); 49 | config[exchange] = results; 50 | coinx.config(config); 51 | console.log(chalk.green('Saved data for ' + capitalize(exchange))); 52 | }); 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## 0.11.1 - 2017-10-15 5 | - Minor bug fixes for funds showing NaN. 6 | 7 | ## 0.11.1 - 2017-09-08 8 | - Fixes a bug that popped up in the 'funds' command when there were more than 50 coins to get a price of at the same time. 9 | 10 | ## 0.11.0 - 2017-08-29 11 | - Fixes display of funds for IOT and CH (aka IOTA and BCH). 12 | 13 | ## 0.10.1 - 2017-07-23 14 | - Fixes a bug where `coinx funds -c` would terminate if any of the exchanges did not have that currency. 15 | 16 | ## 0.10.0 - 2017-07-23 17 | - Adds `coinx action` to run custom scripts that make use of coinx-core. 18 | - Adds buyallthecoins action. Lets you buy the top crypto coins in an automated fashion. 19 | 20 | ## 0.9.0 - 2017-07-22 21 | - Adds `lock` and `unlock`, which will encrypt and decrypt the coinx file that contains your API keys. 22 | 23 | ## 0.8.0 24 | - Adds logging. Closes issue #16. 25 | - Rewrite of the configuration part that allows to add more markets quicker (thanks @AlexandrFox) 26 | - Fixes version display number (thanks @driftfox) 27 | 28 | 29 | ## 0.7.1 30 | Arbitrarily large version increase :) 31 | - Fixes issues 5 & 6 (output bugs when buying) 32 | - Fixes issue 13 thanks to @driftfox (windows/linux differences issue) 33 | - Implements issue 7 (support for coin names). Requires you do run `coinx update`. 34 | - Uses coinmarketcap.com to get a "real" price for BTC in USD. 35 | - Starts this file. 36 | 37 | ## 0.1.0 38 | Initial release 39 | -------------------------------------------------------------------------------- /commands/coinx-lock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const coinx = require('./coinx-core'); 4 | const program = require('commander'); 5 | const chalk = require('chalk'); 6 | const capitalize = require('capitalize'); 7 | const inquirer = require('inquirer'); 8 | 9 | const crypto = require('crypto'); 10 | const zxcvbn = require('zxcvbn'); 11 | 12 | const algorithm = 'aes-256-ctr'; 13 | 14 | function encrypt(text, password){ 15 | var cipher = crypto.createCipher(algorithm,password) 16 | var crypted = cipher.update(text,'utf8','hex') 17 | crypted += cipher.final('hex'); 18 | return crypted; 19 | } 20 | 21 | const config = coinx.config(); 22 | 23 | program 24 | .option('-f, --force', 'Lets you lock the coinx config using a new password.') 25 | .parse(process.argv); 26 | 27 | var password = program.args[0]; 28 | 29 | if (!password){ 30 | console.log(chalk.red('Provide a password to lock your config.')); 31 | process.exit(1); 32 | } 33 | 34 | if (Object.keys(config).length === 0) { 35 | console.log(chalk.red('Need to configure at least one exchange before locking.')); 36 | console.log(chalk.red('Run \'coinx configure [name of exchange]\'')); 37 | process.exit(1); 38 | } 39 | 40 | let hash = encrypt(password,password); 41 | 42 | if (config.passwordHash){ 43 | if ( hash != config.passwordHash && !program.force){ 44 | console.log(chalk.red('Password different than previous. Use -f --force to overwrite with new password.')); 45 | } else { 46 | let encryptedConfig = encrypt(JSON.stringify(config), hash); 47 | coinx.lockConfig(encryptedConfig); 48 | } 49 | } else { 50 | let score = zxcvbn(password).score; 51 | if (score < 2){ 52 | console.log(chalk.red('Please use a stronger password.')); 53 | process.exit(1); 54 | } 55 | config.passwordHash = hash; 56 | let encryptedConfig = encrypt(JSON.stringify(config), hash); 57 | coinx.lockConfig(encryptedConfig); 58 | } -------------------------------------------------------------------------------- /lib/liqui.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | 5 | const API = require('node.liqui.io'); 6 | 7 | class Liqui { 8 | constructor(apiKey, apiSecret) { 9 | this.name = 'liqui'; 10 | this.api = Promise.promisifyAll(new API(apiKey, apiSecret)); 11 | }; 12 | 13 | getBTCinUSD() { 14 | let pair = 'btc_usdt'; 15 | return this.api 16 | .ticker(pair) 17 | .then(data => { 18 | if (data.error) { 19 | return { 20 | exchange: 'liqui', 21 | symbol: 'BTC', 22 | available: false 23 | } 24 | } else { 25 | return { 26 | exchange: 'liqui', 27 | symbol: 'BTC', 28 | priceUSD: data[pair].last, 29 | available: true 30 | } 31 | } 32 | }) 33 | .catch( e => { 34 | return { 35 | exchange: 'liqui', 36 | symbol: 'BTC', 37 | available: false 38 | } 39 | }); 40 | }; 41 | 42 | getPriceInBTC(symbol) { 43 | if (symbol == 'BTC') { 44 | return Promise.reject('Use getBTCinUSD to get BTC price.'); 45 | } else { 46 | let pair = symbol.toLowerCase() + '_btc'; 47 | return this.api 48 | .ticker(pair) 49 | .then(data => { 50 | if (data.error) { 51 | return { 52 | exchange: 'liqui', 53 | symbol: symbol, 54 | available: false 55 | } 56 | } else { 57 | return { 58 | exchange: 'liqui', 59 | symbol: symbol, 60 | priceBTC: parseFloat(data[pair].sell), 61 | available: true 62 | }; 63 | } 64 | }) 65 | .catch(e => { 66 | return { 67 | exchange: 'liqui', 68 | symbol: symbol, 69 | available: false 70 | } 71 | }); 72 | } 73 | }; 74 | 75 | buy(symbol, USDAmount) { 76 | var self = this; 77 | let orderNumber; 78 | let numCoinsToBuy; 79 | let rate; 80 | let btcUSD; 81 | 82 | return Promise.all([ 83 | self.api.ticker(symbol.toLowerCase() + '_btc'), 84 | self.api.ticker('btc_usdt') 85 | ]) 86 | .then(results => { 87 | btcUSD = results[1]['btc_usdt'].last; 88 | rate = results[0][symbol.toLowerCase() + '_btc'].sell; 89 | numCoinsToBuy = (USDAmount / (rate * btcUSD)).toFixed(8); 90 | 91 | let params = { 92 | pair: symbol.toLowerCase() + '_btc', 93 | rate: rate, 94 | amount: numCoinsToBuy 95 | } 96 | return self.api.buy(params); 97 | }) 98 | .then(data => { 99 | let result = { 100 | market: 'liqui', 101 | orderNumber: data['order_id'], 102 | numCoinsBought: data.received, 103 | rate: rate, 104 | usdValue: (rate * data.received * btcUSD), 105 | complete: (data.remains == 0) 106 | } 107 | return result; 108 | }); 109 | 110 | }; 111 | 112 | getBalances() { 113 | let self = this; 114 | return this.api 115 | .getInfo() 116 | .then(data => { 117 | let balances = {}; 118 | Object.keys(data.funds).forEach(key => { 119 | if (data.funds[key]) { 120 | balances[key.toUpperCase()] = data.funds[key]; 121 | } 122 | }) 123 | let result = { 124 | market: self.name, 125 | available: true, 126 | funds: balances 127 | } 128 | return result; 129 | }) 130 | .catch(e => { 131 | let result = { 132 | market: self.name, 133 | available: false 134 | } 135 | return result; 136 | }); 137 | }; 138 | }; 139 | 140 | module.exports = Liqui; 141 | -------------------------------------------------------------------------------- /lib/poloniex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const API = require('poloniex.js'); 4 | const Promise = require('bluebird'); 5 | 6 | class Poloniex { 7 | constructor(apiKey, apiSecret) { 8 | this.name = 'poloniex'; 9 | this.api = Promise.promisifyAll(new API(apiKey, apiSecret)); 10 | }; 11 | 12 | getBTCinUSD() { 13 | let self = this; 14 | return this.api 15 | .returnTickerAsync() 16 | .then(data => { 17 | let result = { 18 | exchange: self.name, 19 | symbol: 'BTC', 20 | available: true, 21 | priceUSD: parseFloat(data['USDT_BTC'].last) 22 | }; 23 | return result; 24 | }); 25 | }; 26 | 27 | getPriceInBTC(symbol) { 28 | let self = this; 29 | if (symbol == 'BTC') { 30 | return Promise.reject('Use getBTCinUSD to get BTC price.'); 31 | } else { 32 | return this.api 33 | .returnTickerAsync() 34 | .then(data => { 35 | let pair = 'BTC_' + symbol; 36 | if (data[pair]) { 37 | let result = { 38 | exchange: self.name, 39 | symbol: symbol, 40 | priceBTC: parseFloat(data[pair].lowestAsk), 41 | available: true 42 | }; 43 | return result; 44 | } else { 45 | let result = { 46 | exchange: self.name, 47 | symbol: symbol, 48 | available: false 49 | }; 50 | return result; 51 | } 52 | 53 | }) 54 | .catch(e => { 55 | let result = { 56 | exchange: self.name, 57 | symbol: symbol, 58 | available: false 59 | }; 60 | }); 61 | } 62 | }; 63 | 64 | buy(symbol, USDAmount) { 65 | var self = this; 66 | let orderNumber; 67 | let numCoinsToBuy; 68 | let rate; 69 | let btcUSD; 70 | let orderResult; 71 | 72 | return this.api.returnTickerAsync() 73 | .then(data => { 74 | btcUSD = data['USDT_BTC'].last; 75 | 76 | rate = parseFloat(data['BTC_' + symbol].lowestAsk); 77 | 78 | numCoinsToBuy = (USDAmount / (rate * btcUSD)).toFixed(8); 79 | 80 | return self.api.buyAsync('BTC', symbol, rate, numCoinsToBuy); 81 | }) 82 | .then(orderData => { 83 | let orderResult = { 84 | market: self.name, 85 | orderNumber: parseInt(orderData.orderNumber), 86 | numCoinsBought: 0, 87 | rate: rate, 88 | complete: false 89 | } 90 | if (orderData.resultingTrades.length) { 91 | orderData.resultingTrades.forEach(trade => { 92 | orderResult.numCoinsBought += parseFloat(trade.amount); 93 | }); 94 | orderResult.complete = (orderResult.numCoinsBought == numCoinsToBuy); 95 | orderResult.usdValue = rate * orderResult.numCoinsBought * btcUSD; 96 | return orderResult; 97 | } else { 98 | console.log('not filled') 99 | return Promise.delay(500) 100 | .then(() => { 101 | this.api.returnOrderTradesAsync(orderResult.orderNumber); 102 | }) 103 | .then(trades => { 104 | console.log(trades); 105 | return orderResult; 106 | }); 107 | } 108 | 109 | }); 110 | }; 111 | 112 | getBalances() { 113 | let self = this; 114 | return this.api 115 | .myBalancesAsync() 116 | .then(data => { 117 | let balances = {}; 118 | Object.keys(data).forEach(key => { 119 | let balance = parseFloat(data[key]); 120 | if (balance) { 121 | balances[key] = balance; 122 | } 123 | }); 124 | let result = { 125 | market: self.name, 126 | available: true, 127 | funds: balances 128 | } 129 | return result; 130 | }) 131 | .catch(e => { 132 | let result = { 133 | market: self.name, 134 | available: false 135 | } 136 | return result; 137 | }); 138 | }; 139 | }; 140 | 141 | 142 | module.exports = Poloniex; 143 | -------------------------------------------------------------------------------- /lib/bitfinex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | const API = require('bitfinex-api-node'); 5 | 6 | class Bitfinex { 7 | constructor(apiKey, apiSecret) { 8 | this.name = 'bitfinex'; 9 | this.rest = Promise.promisifyAll(new API(apiKey, apiSecret, { 10 | version: 1, 11 | transform: true, 12 | autoOpen: false 13 | }).rest); 14 | }; 15 | 16 | getBTCinUSD() { 17 | let self = this; 18 | let pair = 'btcusd'; 19 | return this.rest.tickerAsync(pair) 20 | .then(data => { 21 | return { 22 | exchange: self.name, 23 | symbol: 'BTC', 24 | priceUSD: parseFloat(data.last_price), 25 | volume: parseFloat(data.volume), 26 | available: true 27 | } 28 | }) 29 | .catch(e => { 30 | return { 31 | exchange: self.name, 32 | symbol: 'BTC', 33 | available: false 34 | } 35 | }); 36 | }; 37 | 38 | getPriceInBTC(symbol) { 39 | let self = this; 40 | if (symbol == 'BTC') { 41 | return Promise.reject('Use getBTCinUSD to get BTC price.'); 42 | } else { 43 | let pair = symbol + 'BTC'; 44 | return this.rest.tickerAsync(pair) 45 | .then(data => { 46 | return { 47 | exchange: self.name, 48 | symbol: symbol, 49 | priceBTC: parseFloat(data.ask), 50 | available: true 51 | }; 52 | }) 53 | .catch(e => { 54 | if (e.message == 'Unknown symbol') { 55 | return { 56 | exchange: self.name, 57 | symbol: symbol, 58 | available: false 59 | } 60 | } else { 61 | //console.log('error getting price from btc'); 62 | return { 63 | exchange: self.name, 64 | symbol: symbol, 65 | available: false 66 | } 67 | } 68 | }); 69 | } 70 | }; 71 | 72 | buy(symbol, USDAmount) { 73 | var self = this; 74 | let orderNumber; 75 | let numCoinsToBuy; 76 | let rate; 77 | let btcUSD; 78 | 79 | return Promise.all([ 80 | self.rest.tickerAsync('BTCUSD'), 81 | self.rest.tickerAsync(symbol + 'BTC') 82 | ]).then(results => { 83 | btcUSD = parseFloat(results[0].last_price); 84 | rate = parseFloat(results[1].ask); 85 | numCoinsToBuy = (USDAmount / (rate * btcUSD)).toFixed(8); 86 | 87 | //(symbol, amount, price, exchange, side, type, is_hidden, postOnly, cb 88 | return self.rest.new_orderAsync(symbol + 'BTC', numCoinsToBuy, '' + rate, 'bitfinex', 'buy', 'exchange fill-or-kill'); 89 | }) 90 | .then(result => { 91 | orderNumber = result.order_id; 92 | return Promise.delay(500); // wait for the fill or kill to happen. 93 | }) 94 | .then(() => { 95 | return self.rest.order_statusAsync(orderNumber); 96 | }) 97 | .then(status => { 98 | let result = { 99 | market: self.name, 100 | orderNumber: orderNumber, 101 | numCoinsBought: parseFloat(status.executed_amount), 102 | rate: parseFloat(status.avg_execution_price), 103 | complete: (parseFloat(status.remaining_amount) == 0) 104 | } 105 | result.usdValue = parseFloat(result.rate * result.numCoinsBought) * btcUSD; 106 | return result; 107 | }); 108 | 109 | }; 110 | 111 | getBalances() { 112 | var self = this; 113 | return new Promise((resolve, reject) => { 114 | this.rest.wallet_balances(function(err, data) { 115 | if (err) { 116 | let result = { 117 | market: self.name, 118 | available: false 119 | } 120 | resolve(result); 121 | } else { 122 | let balances = {}; 123 | data.forEach(balance => { 124 | balances[balance.currency.toUpperCase()] = parseFloat(balance.available); 125 | }); 126 | let result = { 127 | market: self.name, 128 | available: true, 129 | funds: balances 130 | } 131 | resolve(result); 132 | } 133 | }); 134 | }); 135 | } 136 | } 137 | 138 | module.exports = Bitfinex; 139 | -------------------------------------------------------------------------------- /lib/binance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | 5 | const API = require('binance'); 6 | const cryptoCompare = require('./cryptocompare'); 7 | 8 | class Binance { 9 | constructor(apiKey, apiSecret) { 10 | this.name = 'binance'; 11 | this.api = new API.BinanceRest({ 12 | key: apiKey, 13 | secret: apiSecret 14 | }); 15 | }; 16 | 17 | getBTCinUSD() { 18 | return { 19 | exchange: this.name, 20 | symbol: 'BTC', 21 | available: false 22 | } 23 | }; 24 | 25 | getPriceInBTC(symbol) { 26 | if (symbol == 'BTC') { 27 | return Promise.reject('Use getBTCinUSD to get BTC price.'); 28 | } else { 29 | let pair = symbol.toUpperCase() + 'BTC'; 30 | return this.api 31 | .depth(pair) 32 | .then(data => { 33 | return { 34 | exchange: this.name, 35 | symbol: symbol, 36 | priceBTC: parseFloat(data.asks[0]), 37 | available: true 38 | }; 39 | 40 | }) 41 | .catch(e => { 42 | return { 43 | exchange: this.name, 44 | symbol: symbol, 45 | available: false 46 | } 47 | }); 48 | } 49 | }; 50 | 51 | buy(symbol, USDAmount) { 52 | var self = this; 53 | let orderNumber; 54 | let numCoinsToBuy; 55 | let rate; 56 | let btcUSD; 57 | 58 | /* 59 | symbol STRING YES 60 | side ENUM YES 61 | type ENUM YES 62 | timeInForce ENUM YES 63 | quantity DECIMAL YES 64 | price DECIMAL YES 65 | */ 66 | 67 | return Promise.all([ 68 | this.getPriceInBTC(symbol), 69 | cryptoCompare.price('BTC','USD') 70 | ]) 71 | .then(results => { 72 | btcUSD = results[1]; 73 | rate = results[0].priceBTC; 74 | numCoinsToBuy = (USDAmount / (rate * btcUSD)).toFixed(8); 75 | 76 | let params = { 77 | symbol: symbol.toUpperCase() + 'BTC', 78 | side: 'BUY', 79 | type: 'MARKET', 80 | quantity: parseFloat(numCoinsToBuy), 81 | timestamp: new Date().getTime() 82 | } 83 | console.log(params); 84 | return this.api.newOrder(params); 85 | }) 86 | .then(data => { 87 | console.log('order results'); 88 | console.log(data); 89 | orderNumber = data.orderId; 90 | return Promise.delay(1000).then( () => { 91 | let params = { 92 | symbol: symbol.toUpperCase() + 'BTC', 93 | orderId: orderNumber 94 | } 95 | return self.api.queryOrder() 96 | }); 97 | }) 98 | .then(data => { 99 | console.log('order status'); 100 | console.log(data); 101 | 102 | /* 103 | { 104 | "symbol": "LTCBTC", 105 | "orderId": 1, 106 | "clientOrderId": "myOrder1", 107 | "price": "0.1", 108 | "origQty": "1.0", 109 | "executedQty": "0.0", 110 | "status": "NEW", 111 | "timeInForce": "GTC", 112 | "type": "LIMIT", 113 | "side": "BUY", 114 | "stopPrice": "0.0", 115 | "icebergQty": "0.0", 116 | "time": 1499827319559 117 | } 118 | */ 119 | // orderStatus NEW, PARTIALLY_FILLED, FILLED, CANCELED,PENDING_CANCEL, REJECTED, EXPIRED 120 | let result = { 121 | market: this.name, 122 | orderNumber: orderNumber, 123 | numCoinsBought: parseFloat(data.executedQty), 124 | rate: rate, 125 | usdValue: (rate * data.executedQty * btcUSD), 126 | complete: (data.orderStatus == 'FILLED') 127 | } 128 | return result; 129 | }); 130 | 131 | }; 132 | 133 | getBalances() { 134 | let self = this; 135 | return this.api 136 | .account() 137 | .then(data => { 138 | let balances = {}; 139 | data.balances.forEach(balance => { 140 | let key = balance.asset; 141 | let value = parseFloat(balance.free); 142 | if (value) 143 | balances[key.toUpperCase()] = value; 144 | }) 145 | let result = { 146 | market: self.name, 147 | available: true, 148 | funds: balances 149 | } 150 | return result; 151 | }) 152 | .catch(e => { 153 | let result = { 154 | market: self.name, 155 | available: false 156 | } 157 | return result; 158 | }); 159 | }; 160 | }; 161 | 162 | module.exports = Binance; 163 | -------------------------------------------------------------------------------- /lib/kraken.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | const API = require('kraken-api'); 5 | 6 | class Kraken { 7 | constructor(apiKey, apiSecret) { 8 | this.name = 'kraken'; 9 | this.api = Promise.promisifyAll(new API(apiKey, apiSecret)); 10 | }; 11 | 12 | getBTCinUSD() { 13 | let pair = 'XXBTZUSD' 14 | return this.api.apiAsync('Ticker', { 15 | pair: pair 16 | }) 17 | .then( data => { 18 | if (data.error && data.error.length){ 19 | return data.error; 20 | } else { 21 | return { 22 | exchange: 'kraken', 23 | symbol: 'BTC', 24 | priceUSD: parseFloat(data.result[pair].a[0]), 25 | available: true 26 | } 27 | } 28 | }) 29 | .catch( e => { 30 | return { 31 | exchange: 'kraken', 32 | symbol: 'BTC', 33 | available: false 34 | } 35 | }); 36 | }; 37 | 38 | getPriceInBTC(symbol){ 39 | if (symbol == 'BTC'){ 40 | return Promise.reject('Use getBTCinUSD to get BTC price.'); 41 | } else { 42 | let pair = 'X' + symbol + 'XXBT'; 43 | return this.api.apiAsync('Ticker', { 44 | pair: pair 45 | }) 46 | .then( data => { 47 | if (data.error && data.error.length){ 48 | return data.error; 49 | } else { 50 | return { 51 | exchange: 'kraken', 52 | symbol: symbol, 53 | priceBTC: parseFloat(data.result[pair].a[0]), 54 | available: true 55 | } 56 | } 57 | }) 58 | .catch( e => { 59 | return { 60 | exchange: 'kraken', 61 | symbol: symbol, 62 | available: false 63 | } 64 | }); 65 | } 66 | }; 67 | 68 | getOrderInfo(orderId){ 69 | return this.api.apiAsync('QueryOrders', { 70 | txid: orderId 71 | }); 72 | }; 73 | 74 | buy(symbol, USDAmount){ 75 | var self = this; 76 | let orderNumber; 77 | let numCoinsToBuy; 78 | let rate; 79 | let btcUSD; 80 | let assetPair = 'X' + symbol + 'XXBT'; 81 | 82 | let pairs = [ 83 | assetPair, 84 | 'XXBTZUSD' 85 | ]; 86 | return this.api.apiAsync('Ticker', { 87 | pair: pairs.join(',') 88 | }) 89 | .then( ticker => { 90 | if (ticker.error && ticker.error.length){ 91 | return Promise.reject(ticker.error); 92 | } else { 93 | btcUSD = ticker.result['XXBTZUSD'].c[0]; // last price 94 | rate = ticker.result[assetPair].a[0]; //ask price 95 | numCoinsToBuy = (USDAmount / (rate * btcUSD)).toFixed(8); 96 | } 97 | console.log('buying on kraken volume'); 98 | console.log(numCoinsToBuy) 99 | return this.api.apiAsync('AddOrder', { 100 | pair: assetPair, 101 | type: 'buy', 102 | ordertype: 'market', 103 | volume: numCoinsToBuy 104 | }); 105 | }) 106 | .then( result => { 107 | orderNumber = result.result.txid[0]; 108 | return Promise.delay(500); // wait for order to fill 109 | }) 110 | .then( () => { 111 | return this.getOrderInfo(orderNumber); 112 | }) 113 | .then( orderData => { 114 | let result = { 115 | market: 'kraken', 116 | orderNumber: orderNumber, 117 | numCoinsBought: parseFloat(orderData.result[orderNumber].vol), 118 | rate: parseFloat(orderData.result[orderNumber].price), 119 | complete: (orderData.result[orderNumber].status == 'closed') 120 | } 121 | result.usdValue = parseFloat(orderData.result[orderNumber].price) * parseFloat(orderData.result[orderNumber].vol) * btcUSD; 122 | return result; 123 | }); 124 | }; 125 | 126 | getBalances() { 127 | let self = this; 128 | return this.api.apiAsync('Balance',null) 129 | .then( data => { 130 | if (data.error && data.error.length ){ 131 | let result = { 132 | market: self.name, 133 | available: false 134 | } 135 | return result; 136 | } else { 137 | let balances = {}; 138 | Object.keys(data.result).forEach( key => { 139 | let balance = data.result[key]; 140 | 141 | key = key.slice(1); 142 | 143 | if (key == 'XBT') key = 'BTC'; 144 | balances[key] = balance; 145 | }); 146 | let result = { 147 | market: self.name, 148 | available: true, 149 | funds: balances 150 | } 151 | return result; 152 | } 153 | }) 154 | .catch( e => { 155 | let result = { 156 | market: self.name, 157 | available: false 158 | } 159 | return result; 160 | }); 161 | }; 162 | 163 | }; 164 | 165 | module.exports = Kraken; 166 | -------------------------------------------------------------------------------- /lib/bittrex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | 5 | class Bittrex { 6 | constructor(apiKey, apiSecret) { 7 | this.name = 'bittrex'; 8 | this.api = Promise.promisifyAll(require('node.bittrex.api')); 9 | if (apiKey){ 10 | this.api.options({ 11 | 'apikey': apiKey, 12 | 'apisecret': apiSecret 13 | }); 14 | } 15 | }; 16 | 17 | getBTCinUSD() { 18 | let pair = 'USDT-BTC'; 19 | return new Promise((resolve, reject) => { 20 | this.api.getticker({ 21 | market: pair 22 | }, data => { 23 | let result; 24 | if (!data.result){ 25 | result = { 26 | exchange: 'bittrex', 27 | symbol: 'BTC', 28 | available: false 29 | }; 30 | } else { 31 | result = { 32 | exchange: 'bittrex', 33 | symbol: 'BTC', 34 | priceUSD: data.result.Ask, 35 | available: true 36 | }; 37 | } 38 | resolve(result); 39 | }); 40 | }); 41 | }; 42 | 43 | getPriceInBTC(symbol) { 44 | if (symbol == 'BTC') { 45 | return Promise.reject('Use getBTCinUSD to get BTC price.'); 46 | } else { 47 | return new Promise((resolve, reject) => { 48 | let pair = 'BTC-' + symbol; 49 | this.api.getticker({ 50 | market: pair 51 | }, data => { 52 | if (!data.success) { 53 | resolve({ 54 | exchange: 'bittrex', 55 | symbol: symbol, 56 | available: false 57 | }); 58 | } else { 59 | let result = { 60 | exchange: 'bittrex', 61 | symbol: symbol, 62 | priceBTC: data.result.Ask, 63 | available: true 64 | }; 65 | resolve(result); 66 | }; 67 | }); 68 | }); 69 | }; 70 | } 71 | 72 | getBalances() { 73 | var self = this; 74 | return new Promise((resolve, reject) => { 75 | self.api.getbalances(function(data) { 76 | if (!data.result){ 77 | let result = { 78 | market: self.name, 79 | available: false 80 | } 81 | resolve(result); 82 | } else { 83 | let balances = {}; 84 | if (data.result) { 85 | data.result.forEach(balance => { 86 | balances[balance.Currency] = balance.Balance; 87 | }); 88 | } 89 | let result = { 90 | market: self.name, 91 | available: true, 92 | funds: balances 93 | } 94 | resolve(result); 95 | } 96 | }); 97 | }); 98 | }; 99 | 100 | getOrderHistory(market) { 101 | var options = {}; 102 | if (market) options.market = market; 103 | 104 | return new Promise((resolve, reject) => { 105 | this.api.getorderhistory(options, function(data) { 106 | if (!data.success) { 107 | reject(data.message); 108 | } else { 109 | resolve(data.result); 110 | } 111 | }); 112 | }); 113 | }; 114 | 115 | buy(symbol, USDAmount) { 116 | var self = this; 117 | let orderNumber; 118 | let numCoinsToBuy; 119 | let rate; 120 | let btcUSD; 121 | 122 | return new Promise((resolve, reject) => { 123 | this.api.getmarketsummaries(data => { 124 | if (!data.success) { 125 | reject(data.message); 126 | } else { 127 | data.result.forEach(market => { 128 | if (market.MarketName == 'USDT-BTC') { 129 | btcUSD = market.Ask; 130 | } else if (market.MarketName == 'BTC-' + symbol) { 131 | rate = parseFloat(market.Ask); 132 | } 133 | }); 134 | 135 | numCoinsToBuy = (USDAmount / (rate * btcUSD)).toFixed(8); 136 | 137 | var options = { 138 | market: 'BTC-' + symbol, 139 | quantity: numCoinsToBuy, 140 | rate: rate 141 | } 142 | self.api.buylimit(options, function(data) { 143 | if (!data.success) { 144 | reject(data.message); 145 | } else { 146 | orderNumber = data.result.uuid; 147 | resolve(); 148 | } 149 | }); 150 | } 151 | }); 152 | }) 153 | .delay(500) 154 | .then(data => { 155 | return new Promise( ( resolve, reject ) => { 156 | var options = { 157 | uuid: orderNumber 158 | } 159 | return self.api.getorder(options, data => { 160 | if (data.success){ 161 | resolve(data.result); 162 | } else { 163 | reject(data.message); 164 | } 165 | }); 166 | }) 167 | }) 168 | .then(order => { 169 | let result = { 170 | market: 'bittrex', 171 | orderNumber: orderNumber, 172 | numCoinsBought: order.Quantity, 173 | rate: order.PricePerUnit, 174 | complete: (order.QuantityRemaining == 0), 175 | usdValue: order.PricePerUnit * order.Quantity * btcUSD 176 | } 177 | return result; 178 | }); 179 | } 180 | }; 181 | 182 | module.exports = Bittrex; 183 | -------------------------------------------------------------------------------- /commands/coinx-price.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const coinx = require('./coinx-core'); 4 | const program = require('commander'); 5 | const chalk = require('chalk'); 6 | const capitalize = require('capitalize'); 7 | const columnify = require('columnify'); 8 | 9 | const cryptocompare = require('../lib/cryptocompare'); 10 | 11 | const coins = coinx.coins(); 12 | const exchanges = Object.values(coinx.exchanges()); 13 | delete exchanges.passwordHash; 14 | 15 | if (Object.keys(coins).length === 0) { 16 | console.error(chalk.red('Please run `coinx update` to get the latest list of coins.')); 17 | process.exit(1); 18 | } 19 | 20 | program.parse(process.argv); 21 | 22 | var symbol = program.args; 23 | 24 | if (!symbol.length) { 25 | console.error(chalk.red('No coin symbol provided.')); 26 | process.exit(1); 27 | } 28 | 29 | symbol = symbol[0].toUpperCase(); 30 | 31 | if (coins[symbol]){ 32 | console.log(chalk.blue('Getting prices for ' + coins[symbol].name + ' (' + symbol + ')...')); 33 | } else { 34 | console.log(chalk.blue('Getting prices for ' + symbol + '...')); 35 | } 36 | 37 | 38 | let requests = []; 39 | 40 | if (symbol == 'BTC'){ 41 | requests = exchanges.map( exchange => { 42 | return exchange.getBTCinUSD() 43 | }); 44 | } else { 45 | requests = [ 46 | cryptocompare.price('BTC','USD') 47 | ]; 48 | let priceInBTCRequests = exchanges.map( exchange => { 49 | return exchange.getPriceInBTC(symbol); 50 | }); 51 | requests = requests.concat(priceInBTCRequests); 52 | } 53 | 54 | Promise 55 | .all(requests) 56 | .then(results => { 57 | if (symbol == 'BTC'){ 58 | processBTC(results); 59 | } else { 60 | processCoin(results); 61 | } 62 | }); 63 | 64 | function processBTC(results){ 65 | let priceResults = results.filter(result => { 66 | return result.available; 67 | }); 68 | if (!priceResults.length) { 69 | console.log(chalk.red('Coin not found on any exchange.')); 70 | process.exit(0); 71 | } 72 | 73 | let averageUSD = priceResults.reduce((sum, result) => { 74 | return parseFloat(sum) + parseFloat(result.priceUSD); 75 | }, 0.0) / priceResults.length; 76 | 77 | priceResults.sort( (a, b) => { 78 | if (a.priceUSD < b.priceUSD){ 79 | return -1; 80 | } 81 | return 1; 82 | }); 83 | 84 | priceResults.push({}); 85 | 86 | priceResults.push({ 87 | exchange: 'average', 88 | priceUSD: averageUSD 89 | }); 90 | 91 | let columns = columnify(priceResults, { 92 | columns: ['exchange', 'priceUSD'], 93 | config: { 94 | exchange: { 95 | headingTransform: function(heading) { 96 | return capitalize(heading); 97 | }, 98 | dataTransform: function(data) { 99 | return capitalize(data); 100 | } 101 | }, 102 | priceUSD: { 103 | headingTransform: function(heading) { 104 | return 'Price in USD' 105 | }, 106 | dataTransform: function(data) { 107 | return (data) ? '$' + parseFloat(data).toFixed(2) : ''; 108 | }, 109 | align: 'right' 110 | } 111 | } 112 | }); 113 | console.log(columns); 114 | } 115 | 116 | 117 | function processCoin(results){ 118 | let btcPrice = results.shift(); 119 | 120 | let priceResults = results.filter(result => { 121 | return result.available && result.priceBTC; 122 | }).map(result => { 123 | result.priceUSD = (result.priceBTC * btcPrice).toFixed(8); 124 | return result; 125 | }); 126 | 127 | if (!priceResults.length) { 128 | console.log(chalk.red('Coin not found on any exchange.')); 129 | process.exit(0); 130 | } 131 | 132 | let averageUSD = priceResults.reduce((sum, result) => { 133 | return parseFloat(sum) + parseFloat(result.priceUSD); 134 | }, 0.0) / priceResults.length; 135 | 136 | let averageBTC = priceResults.reduce((sum, result) => { 137 | return parseFloat(sum) + parseFloat(result.priceBTC); 138 | }, 0) / priceResults.length; 139 | 140 | priceResults.sort( (a, b) => { 141 | if (a.priceUSD < b.priceUSD){ 142 | return -1; 143 | } 144 | return 1; 145 | }); 146 | 147 | priceResults.push({}); 148 | 149 | priceResults.push({ 150 | exchange: 'average', 151 | priceBTC: averageBTC, 152 | priceUSD: averageUSD 153 | }); 154 | 155 | let columns = columnify(priceResults, { 156 | columns: ['exchange', 'priceBTC', 'priceUSD'], 157 | config: { 158 | exchange: { 159 | headingTransform: function(heading) { 160 | return capitalize(heading); 161 | }, 162 | dataTransform: function(data) { 163 | return capitalize(data); 164 | } 165 | }, 166 | priceBTC: { 167 | headingTransform: function(heading) { 168 | return 'Price in BTC' 169 | }, 170 | dataTransform: function(data) { 171 | return (data) ? parseFloat(data).toFixed(8) : ''; 172 | }, 173 | align: 'right' 174 | }, 175 | priceUSD: { 176 | headingTransform: function(heading) { 177 | return 'Price in USD' 178 | }, 179 | dataTransform: function(data) { 180 | let price = parseFloat(data); 181 | let decimals = (price > .01) ? 2 : 5; 182 | 183 | return (data) ? '$' + price.toFixed(decimals) : ''; 184 | }, 185 | align: 'right' 186 | } 187 | } 188 | }); 189 | console.log(columns); 190 | } 191 | -------------------------------------------------------------------------------- /commands/coinx-core.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | const os = require('os'); 6 | const homedir = require('homedir'); 7 | const json2csv = require('json2csv'); 8 | const moment = require('moment'); 9 | const chalk = require('chalk'); 10 | const crypto = require('crypto'); 11 | 12 | const algorithm = 'aes-256-ctr'; 13 | 14 | const exchangeModules = [ 15 | 'bitfinex', 16 | 'bittrex', 17 | 'kraken', 18 | 'liqui', 19 | 'poloniex' 20 | ]; 21 | 22 | class Coinx { 23 | 24 | static exchanges() { 25 | let exchanges = []; 26 | const config = Coinx.config(); 27 | exchangeModules.forEach(function(file) { 28 | let name = path.basename(file, '.js'); 29 | let classname = require('../lib/' + file); 30 | if (name in config) { 31 | exchanges[name] = new classname(config[name].apiKey, config[name].apiSecret); 32 | } else { 33 | exchanges[name] = new classname(); 34 | } 35 | }); 36 | return exchanges; 37 | } 38 | 39 | static coins(newCoins) { 40 | if (newCoins) { 41 | if (!fs.existsSync(Coinx.configPath())) { 42 | fs.mkdirSync(Coinx.configPath()); 43 | } 44 | fs.writeFileSync(Coinx.coinListFilePath(), JSON.stringify(newCoins)); 45 | return newCoins; 46 | } else { 47 | if (fs.existsSync(Coinx.coinListFilePath())) { 48 | return require(Coinx.coinListFilePath()); 49 | } else { 50 | return {}; 51 | } 52 | } 53 | } 54 | 55 | static configFilePath() { 56 | return path.join(homedir(), 'coinx', 'coinx.json'); 57 | } 58 | 59 | static configPath() { 60 | return path.dirname(Coinx.configFilePath()); 61 | } 62 | 63 | static coinListFilePath() { 64 | return path.join(Coinx.configPath(), 'coinList.json'); 65 | } 66 | 67 | static configLogPath() { 68 | return path.join(Coinx.configPath(), 'log.csv'); 69 | } 70 | 71 | static actionPath(){ 72 | return path.join(__dirname,'../actions/'); 73 | } 74 | 75 | static config(newConfig) { 76 | if (newConfig) { 77 | if (!fs.existsSync(Coinx.configPath())) { 78 | fs.mkdirSync(Coinx.configPath()); 79 | } 80 | fs.writeFileSync(Coinx.configFilePath(), JSON.stringify(newConfig, null, 4)); 81 | return newConfig; 82 | } else { 83 | if (fs.existsSync(Coinx.configFilePath())) { 84 | try { 85 | let config = require(Coinx.configFilePath()); 86 | return config; 87 | } catch (e) { 88 | console.log(chalk.red('Could not read config file. Is it locked?')); 89 | process.exit(1); 90 | } 91 | } else { 92 | return {}; 93 | } 94 | } 95 | } 96 | 97 | static lockConfig(encryptedConfig) { 98 | if (!fs.existsSync(Coinx.configPath())) { 99 | fs.mkdirSync(Coinx.configPath()); 100 | } 101 | fs.writeFileSync(Coinx.configFilePath(), encryptedConfig); 102 | console.log(chalk.green('Config file locked.')); 103 | } 104 | 105 | static unlockConfig(hash) { 106 | function decrypt(text, password) { 107 | var decipher = crypto.createDecipher(algorithm, password) 108 | var dec = decipher.update(text, 'hex', 'utf8') 109 | dec += decipher.final('utf8'); 110 | return dec; 111 | } 112 | try { 113 | require(Coinx.configFilePath()); 114 | console.log(chalk.red('Config already unlocked')); 115 | } catch (e) { 116 | let data = fs.readFileSync(Coinx.configFilePath()).toString(); 117 | try { 118 | let decrypted = JSON.parse(decrypt(data, hash)); 119 | Coinx.config(decrypted); 120 | console.log(chalk.green('Config file unlocked.')); 121 | } catch (e) { 122 | console.log(chalk.red('Wrong password.')); 123 | process.exit(1); 124 | } 125 | } 126 | 127 | } 128 | 129 | /*********** 130 | / Quote Currency - usually btc 131 | / Base Currency - what you're buying or selling in exchange for the quote currency 132 | / If I confused those two, someone let me know. 133 | / 134 | / params: An object containing the following 135 | / action: buy or sell 136 | / exchange: name of exchange action occured on 137 | / orderNumber: Unique ID provided by the exchange that identifies the order. Not always available. 138 | / quoteCurrency: usually btc 139 | / baseCurrency: what you're buying or selling in exchange for the quote currency 140 | / amount: amount of coins bought or sold 141 | / rate: exchange rate between quote currency and base currency 142 | / valueUSD: the value of the trade in US dollars 143 | / complete: boolean - whether or not the trade was fully executed by the exchange 144 | ************/ 145 | static log(params) { 146 | let fields = [ 147 | 'date', 148 | 'action', 149 | 'exchange', 150 | 'orderNumber', 151 | 'quoteCurrency', 152 | 'baseCurrency', 153 | 'amount', 154 | 'rate', 155 | 'valueUSD', 156 | 'complete' 157 | ]; 158 | 159 | return fs 160 | .pathExists(Coinx.configLogPath()) 161 | .then(exists => { 162 | if (!exists) { 163 | let columnTitles = '"Date","Action","Exchange","Order Number","Quote Currency","Base Currency","Amount","Rate","Value USD","Complete"'; 164 | return fs.outputFile(Coinx.configLogPath(), columnTitles); 165 | } else { 166 | return; 167 | } 168 | }) 169 | .then(() => { 170 | params.date = moment().format('YYYY-MM-DD HH:mm:ss'); 171 | let csv = json2csv({ 172 | fields: fields, 173 | data: params, 174 | hasCSVColumnTitle: false 175 | }); 176 | return fs.appendFile(Coinx.configLogPath(), os.EOL + csv); 177 | }); 178 | } 179 | } 180 | 181 | module.exports = Coinx; -------------------------------------------------------------------------------- /commands/coinx-buy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const coinx = require('./coinx-core'); 4 | const path = require('path'); 5 | const program = require('commander'); 6 | const chalk = require('chalk'); 7 | const capitalize = require('capitalize'); 8 | const inquirer = require('inquirer'); 9 | const columnify = require('columnify'); 10 | const cryptocompare = require('../lib/cryptocompare'); 11 | const config = coinx.config(); 12 | const exchanges = coinx.exchanges(); 13 | const coins = coinx.coins(); 14 | let exchangesToRequest = []; 15 | 16 | if (Object.keys(coins).length === 0) { 17 | console.error(chalk.red('Please run `coinx update` to get the latest list of coins.')); 18 | process.exit(1); 19 | } 20 | 21 | program 22 | .option('-$, --usd [amount]', 'Amount of US Dollars to spend.') 23 | .option('-e, --exchange [name]', 'A specific exchange to buy from.') 24 | .parse(process.argv); 25 | 26 | if (Object.keys(config).length === 0) { 27 | console.log(chalk.red('Need to configure at least one exchange.')); 28 | console.log(chalk.red('Run \'coinx configure [name of exchange]\'')); 29 | process.exit(1); 30 | } 31 | 32 | let symbol = program.args; 33 | if (!symbol.length) { 34 | console.error(chalk.red('No coin symbol provided.')); 35 | process.exit(1); 36 | } 37 | 38 | symbol = symbol[0].toUpperCase(); 39 | if (!program.usd) { 40 | console.log(chalk.red('You must specify the amount of USD to spend. -$ or --usd')); 41 | } 42 | 43 | if (program.exchange) { 44 | if (Object.keys(exchanges).indexOf(program.exchange) === -1) { 45 | console.log(chalk.red('Unknown exchange')); 46 | process.exit(1); 47 | } 48 | if (Object.keys(config).indexOf(program.exchange) === -1) { 49 | console.log(chalk.red('Exchange is not configured')); 50 | process.exit(1); 51 | } 52 | exchangesToRequest.push(exchanges[program.exchange]); 53 | console.log(chalk.blue('Buying on ' + capitalize(program.exchange) + '...')); 54 | } else { 55 | for (const name in config) { 56 | if (name !== 'passwordHash'){ 57 | exchangesToRequest.push(exchanges[name]); 58 | } 59 | } 60 | console.log(chalk.blue('Buying...')); 61 | } 62 | 63 | if (program.usd) { 64 | Promise.all([ 65 | getBTCPriceInUSD(), 66 | getCoinPriceInBTC(exchangesToRequest, symbol) 67 | ]) 68 | .then(results => { 69 | let btcPrice = results[0]; 70 | let coinPrices = results[1]; 71 | 72 | coinPrices.map(result => { 73 | result.priceUSD = (result.priceBTC * btcPrice).toFixed(2); 74 | return result; 75 | }); 76 | 77 | if (!coinPrices.length) { 78 | console.log(chalk.red('Coin not found on any exchange.')); 79 | process.exit(0); 80 | } 81 | 82 | coinPrices.sort((a, b) => { 83 | if (a.priceBTC < b.priceBTC) return -1; 84 | return 1; 85 | }); 86 | 87 | let bestMarket = coinPrices.shift(); 88 | 89 | if (bestMarket.exchange in config) { 90 | console.log(chalk.green('Best price found on ' + capitalize(bestMarket.exchange) + ' at $' + bestMarket.priceUSD)); 91 | } else { 92 | console.log(chalk.red('Best price found on ' + capitalize(bestMarket.exchange) + ' but it is not configured, run "coinx config ' + bestMarket.exchange + '" to configure')); 93 | process.exit(1); 94 | } 95 | let numCoinsToBuy = (program.usd / (bestMarket.priceBTC * btcPrice)).toFixed(8); 96 | 97 | console.log(''); 98 | console.log(chalk.magenta('*Note that the number of coins may change slightly if the market fluctuates*')); 99 | console.log(''); 100 | 101 | let questions = [{ 102 | type: 'confirm', 103 | name: 'proceed', 104 | message: 'Buy about ' + numCoinsToBuy + ' worth of ' + symbol + '?' 105 | }]; 106 | 107 | inquirer 108 | .prompt(questions) 109 | .then(results => { 110 | if (!results.proceed) { 111 | process.exit(0); 112 | } 113 | console.log(chalk.green('Buying...')); 114 | return exchanges[bestMarket.exchange].buy(symbol, program.usd); 115 | }) 116 | .then(result => { 117 | if (result.complete) { 118 | console.log(chalk.green('Order complete!')); 119 | console.log(chalk.green(capitalize(result.market) + ' order number ' + result.orderNumber)); 120 | console.log(chalk.green('Bought ' + result.numCoinsBought + ' ' + symbol + ' at ' + result.rate + ' BTC per coin')); 121 | console.log(chalk.green('Worth about $' + parseFloat(result.usdValue).toFixed(2))); 122 | } else { 123 | console.log(chalk.green('Order placed, but not completed.')); 124 | console.log(chalk.green(capitalize(result.market) + ' order number ' + result.orderNumber)); 125 | console.log('Details:'); 126 | console.log(result); 127 | } 128 | 129 | let logParams = { 130 | action: 'buy', 131 | exchange: result.market, 132 | orderNumber: result.orderNumber, 133 | baseCurrency: symbol, 134 | quoteCurrency: 'BTC', 135 | amount: result.numCoinsBought, 136 | rate: result.rate, 137 | valueUSD: parseFloat(result.usdValue).toFixed(2), 138 | complete: result.complete 139 | } 140 | return coinx.log(logParams); 141 | }) 142 | .catch(e => { 143 | console.error(chalk.red('An error occurred.')); 144 | console.log(e); 145 | }); 146 | }); 147 | } 148 | 149 | function getCoinPriceInBTC(exchanges, symbol) { 150 | if (coins[symbol]) { 151 | console.log(chalk.blue('Checking ' + coins[symbol].name + ' (' + symbol + ') on the markets...')); 152 | } else { 153 | console.log(chalk.blue('Checking ' + symbol + ' on the markets...')); 154 | } 155 | 156 | let coinPriceRequests = exchanges.map(exchange => { 157 | return exchange.getPriceInBTC(symbol).catch(e => { 158 | console.log('error') 159 | console.log(e); 160 | console.log(exchange) 161 | }); 162 | }); 163 | 164 | return Promise 165 | .all(coinPriceRequests) 166 | .then(results => { 167 | let priceResults = results.filter(result => { 168 | return result.available && result.priceBTC; 169 | }); 170 | 171 | return priceResults; 172 | }); 173 | } 174 | 175 | function getBTCPriceInUSD() { 176 | return cryptocompare.price('BTC', 'USD'); 177 | } -------------------------------------------------------------------------------- /actions/buyallthecoins/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Buy the top 50 coins by market cap. 3 | */ 4 | const chalk = require('chalk'); 5 | const fs = require('fs-extra'); 6 | const path = require('path'); 7 | const inquirer = require('inquirer'); 8 | const Promise = require('bluebird'); 9 | const _ = require('lodash'); 10 | const capitalize = require('capitalize'); 11 | 12 | const coinx = require('../../commands/coinx-core'); 13 | const coinmarketcap = require('../../lib/coinmarketcap'); 14 | const cryptocompare = require('../../lib/cryptocompare'); 15 | 16 | const config = coinx.config(); 17 | const exchanges = coinx.exchanges(); 18 | const coins = coinx.coins(); 19 | 20 | module.exports = { 21 | run: function(program) { 22 | let coinList; 23 | let btcPriceInUSD; 24 | 25 | program 26 | .option('-$, --usd [amount]', 'Amount of US Dollars to spend per coin.') 27 | .option('-t, --top [number]', 'Buy the top [number] of coins by market cap .', 50) 28 | .parse(process.argv); 29 | 30 | if (!program.usd) { 31 | console.log(chalk.red('Need to use the -$ flag to specify how much to spend per coin.')); 32 | process.exit(1); 33 | } 34 | 35 | let excluded = ['BTC']; 36 | let excludedFilePath = path.join(__dirname, 'excluded_coins.txt'); 37 | if (fs.existsSync(excludedFilePath)) { 38 | excluded = excluded.concat(fs.readFileSync(excludedFilePath).toString().split(/\r|\n/)); 39 | } else { 40 | console.log('not found') 41 | } 42 | console.log(chalk.blue('Buy all the coins!')); 43 | 44 | console.log(chalk.green('Spend per coin: $' + program.usd)); 45 | console.log(chalk.green('Coins to buy: ' + program.top)); 46 | console.log(chalk.green('Maximum spend: $' + (program.usd * program.top) + ' (assumes all coins available to buy)')); 47 | console.log(chalk.green('Will not buy: ', excluded.join(', '))); 48 | 49 | let questions = [{ 50 | type: 'confirm', 51 | name: 'proceed', 52 | message: 'Proceed?' 53 | }]; 54 | 55 | inquirer 56 | .prompt(questions) 57 | .then(results => { 58 | if (!results.proceed){ 59 | process.exit(0); 60 | } else { 61 | console.log(chalk.blue('Getting latest Market Cap list...')); 62 | return cryptocompare.price('BTC', 'USD'); 63 | } 64 | }) 65 | .then( btcPrice => { 66 | console.log(chalk.blue('Got list...')); 67 | btcPriceInUSD = btcPrice; 68 | return coinmarketcap.getList(program.top); 69 | }) 70 | .then( results => { 71 | let exchangesToRequest = []; 72 | for (const name in config) { 73 | if (name !== 'passwordHash'){ 74 | exchangesToRequest.push(exchanges[name]); 75 | } 76 | } 77 | return Promise 78 | .map(results, coin => { 79 | if ( excluded.indexOf(coin.symbol) == -1){ 80 | return buyCoin(coin, btcPriceInUSD, exchangesToRequest, program.usd); 81 | } else { 82 | console.log(chalk.green('Skipping excluded coin ' + coin.name + '.')); 83 | console.log(''); 84 | return Promise.resolve(); 85 | } 86 | 87 | }, {concurrency:1}); 88 | }) 89 | .then( () => { 90 | console.log(chalk.green('Done.')); 91 | }); 92 | } 93 | } 94 | 95 | function buyCoin(coin, btcPrice, exchangesToRequest, usd){ 96 | console.log(chalk.green('Buying ' + coin.name + '.')); 97 | 98 | return getCoinPriceInBTC(exchangesToRequest, coin.symbol) 99 | .then( coinPrices => { 100 | if (!coinPrices.length) { 101 | console.log(chalk.red('Coin not found on any exchange.')); 102 | console.log(''); 103 | return Promise.resolve(); 104 | } else { 105 | coinPrices.map(result => { 106 | result.priceUSD = (result.priceBTC * btcPrice).toFixed(2); 107 | return result; 108 | }); 109 | 110 | let bestMarket = _.sortBy(coinPrices,'priceBTC').shift(); 111 | 112 | console.log(chalk.green('Best price found on ' + capitalize(bestMarket.exchange) + ' at $' + bestMarket.priceUSD)); 113 | 114 | let numCoinsToBuy = (usd / (bestMarket.priceBTC * btcPrice)).toFixed(8); 115 | 116 | console.log(chalk.green('Buying ' + numCoinsToBuy + ' ' + coin.name + ' on ' + capitalize(bestMarket.exchange))); 117 | 118 | return exchanges[bestMarket.exchange].buy(coin.symbol, usd); 119 | } 120 | }) 121 | .then( result => { 122 | if (result.complete) { 123 | console.log(chalk.green('Order complete!')); 124 | console.log(chalk.green(capitalize(result.market) + ' order number ' + result.orderNumber)); 125 | console.log(chalk.green('Bought ' + result.numCoinsBought + ' ' + coin.symbol + ' at ' + result.rate + ' BTC per coin')); 126 | console.log(chalk.green('Worth about $' + parseFloat(result.usdValue).toFixed(2))); 127 | } else { 128 | console.log(chalk.green('Order placed, but not completed.')); 129 | console.log(chalk.green(capitalize(result.market) + ' order number ' + result.orderNumber)); 130 | console.log('Details:'); 131 | console.log(result); 132 | } 133 | console.log(''); 134 | 135 | let logParams = { 136 | action: 'buy', 137 | exchange: result.market, 138 | orderNumber: result.orderNumber, 139 | baseCurrency: coin.symbol, 140 | quoteCurrency: 'BTC', 141 | amount: result.numCoinsBought, 142 | rate: result.rate, 143 | valueUSD: parseFloat(result.usdValue).toFixed(2), 144 | complete: result.complete 145 | } 146 | return coinx.log(logParams); 147 | }); 148 | } 149 | 150 | function getCoinPriceInBTC(exchanges, symbol) { 151 | if (coins[symbol]) { 152 | console.log(chalk.blue('Checking ' + coins[symbol].name + ' (' + symbol + ') on the markets...')); 153 | } else { 154 | console.log(chalk.blue('Checking ' + symbol + ' on the markets...')); 155 | } 156 | 157 | let coinPriceRequests = exchanges.map(exchange => { 158 | return exchange.getPriceInBTC(symbol).catch(e => { 159 | console.log('error') 160 | console.log(e); 161 | console.log(exchange) 162 | }); 163 | }); 164 | 165 | return Promise 166 | .all(coinPriceRequests) 167 | .then(results => { 168 | let priceResults = results.filter(result => { 169 | return result.available && result.priceBTC; 170 | }); 171 | 172 | return priceResults; 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /commands/coinx-funds.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const coinx = require('./coinx-core'); 5 | const chalk = require('chalk'); 6 | const program = require('commander'); 7 | const capitalize = require('capitalize'); 8 | const inquirer = require('inquirer'); 9 | const columnify = require('columnify'); 10 | const cryptocompare = require('../lib/cryptocompare'); 11 | const coinsLookup = coinx.coins(); 12 | const config = coinx.config(); 13 | const exchanges = coinx.exchanges(); 14 | let exchangesToCheck = []; 15 | 16 | if (Object.keys(coinsLookup).length === 0) { 17 | console.error(chalk.red('Please run `coinx update` to get the latest list of coins.')); 18 | process.exit(1); 19 | } 20 | 21 | if (Object.keys(config).length === 0) { 22 | console.log(chalk.red('Need to configure at least one exchange.')); 23 | console.log(chalk.red('Run \'coinx configure [name of exchange]\'')); 24 | process.exit(1); 25 | } 26 | 27 | program 28 | .option('-e, --exchange [name]', 'Get balances at the specified exchange.') 29 | .option('-a, --alphabetically', 'Sort the balance list alphabetically.') 30 | .option('-n, --numerically', 'Sort the balance list by the number of coins, descending.') 31 | .option('-c, --coin [symbol]', 'Only get balances for this coin.') 32 | .option('-v, --value', 'Sort the balance list by the value of each coin in US dollars, descending.') 33 | .parse(process.argv); 34 | 35 | if (program.exchange) { 36 | if (Object.keys(exchanges).indexOf(program.exchange) === -1) { 37 | console.log(chalk.red('Unknown exchange')); 38 | process.exit(1); 39 | } 40 | if (Object.keys(config).indexOf(program.exchange) === -1) { 41 | console.log(chalk.red('Exchange is not configured')); 42 | process.exit(1); 43 | } 44 | exchangesToCheck.push(exchanges[program.exchange]); 45 | console.log(chalk.blue('Getting balances on ' + capitalize(program.exchange) + '...')); 46 | } else { 47 | for (const name in config) { 48 | if (name !== 'passwordHash') { 49 | exchangesToCheck.push(exchanges[name]); 50 | } 51 | } 52 | console.log(chalk.blue('Getting balances...')); 53 | } 54 | 55 | let requests = exchangesToCheck.map(exchange => { 56 | return exchange.getBalances().then(balance => { 57 | if (!balance.available) { 58 | console.log(chalk.red(capitalize(balance.market) + ' returned an error. Is your API key and secret correct?')); 59 | } 60 | return balance; 61 | }) 62 | }); 63 | 64 | let balances; 65 | 66 | Promise 67 | .all(requests) 68 | .then(results => { 69 | let fsymbols = []; 70 | balances = results; 71 | 72 | results.forEach(exchange => { 73 | if (exchange.available) { 74 | Object.keys(exchange.funds).forEach(coin => { 75 | if (coin == 'CH') { 76 | coin = 'BCH'; 77 | } 78 | fsymbols.push(coin); 79 | }); 80 | } 81 | }); 82 | 83 | // Cryptocompare seems to break occasionaly if > 50 symbols are sent into 84 | // priceMulti at the same time. 85 | fsymbols = _.uniq(fsymbols); 86 | 87 | var i, j, temparray, chunk = 30; 88 | let tasks = []; 89 | for (i = 0, j = fsymbols.length; i < j; i += chunk) { 90 | tasks.push(cryptocompare.priceMulti(fsymbols.slice(i, i + chunk), 'USD')); 91 | } 92 | 93 | return Promise.all(tasks).then( results => { 94 | let prices = {}; 95 | results.forEach( result => { 96 | Object.keys(result).forEach( item => { 97 | prices[item] = result[item] 98 | }); 99 | }); 100 | return prices; 101 | }); 102 | }) 103 | .then(prices => { 104 | 105 | balances.forEach(balance => { 106 | if (balance.available) { 107 | let funds = balance.funds; 108 | let coins = Object.keys(funds).map(coin => { 109 | let name = (coinsLookup[coin]) ? coinsLookup[coin].name : ''; 110 | if (coin == 'CH') name = 'Bitcoin Cash'; 111 | if (coin == 'IOT') name = 'IOTA'; 112 | let valueUSD = (coin != 'CH') ? funds[coin] * prices[coin] : funds[coin] * prices['BCH']; 113 | return { 114 | name: name, 115 | symbol: coin, 116 | count: funds[coin], 117 | valueUSD: valueUSD 118 | } 119 | }); 120 | 121 | if (program.coin) { 122 | coins = coins.filter(coin => { 123 | return coin.symbol.toLowerCase() == program.coin.toLowerCase(); 124 | }); 125 | if (coins.length == 0) { 126 | if (program.exchange) { 127 | console.log(chalk.red('Coin not found on this exchange.')); 128 | } 129 | return; 130 | } 131 | } 132 | if (program.alphabetically) { 133 | coins.sort((a, b) => { 134 | if (a.name < b.name) { 135 | return -1; 136 | } else { 137 | return 1; 138 | } 139 | }); 140 | } else if (program.numerically) { 141 | coins.sort((a, b) => { 142 | if (a.count > b.count) { 143 | return -1; 144 | } else { 145 | return 1; 146 | } 147 | }); 148 | } else { 149 | coins.sort((a, b) => { 150 | if (parseFloat(a.valueUSD) > parseFloat(b.valueUSD)) { 151 | return -1; 152 | } else { 153 | return 1; 154 | } 155 | }); 156 | } 157 | 158 | let total = { 159 | name: 'Total', 160 | valueUSD: 0 161 | } 162 | coins.forEach(coin => { 163 | total.valueUSD += coin.valueUSD; 164 | }); 165 | coins.push(total); 166 | 167 | let columns = columnify(coins, { 168 | columns: ['name', 'symbol', 'count', 'valueUSD'], 169 | config: { 170 | name: { 171 | headingTransform: function(heading) { 172 | return capitalize(heading); 173 | } 174 | }, 175 | symbol: { 176 | headingTransform: function(heading) { 177 | return capitalize(heading); 178 | } 179 | }, 180 | count: { 181 | headingTransform: function(heading) { 182 | return capitalize(heading); 183 | }, 184 | dataTransform: function(data) { 185 | return (data) ? parseFloat(data).toFixed(8) : ''; 186 | }, 187 | align: 'right' 188 | }, 189 | valueUSD: { 190 | headingTransform: function() { 191 | return 'Value USD'; 192 | }, 193 | dataTransform: function(data) { 194 | return '$' + parseFloat(data).toFixed(2); 195 | }, 196 | align: 'right' 197 | } 198 | } 199 | }); 200 | console.log(chalk.green(capitalize(balance.market))); 201 | console.log(columns); 202 | } 203 | }); 204 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coinx 2 | A command-line tool to interact with multiple crypto-currencies exchanges. Buy, sell, find the best price, and check your exchange balances. 3 | 4 | #### Setup 5 | * [Installation](#install) 6 | * [Upgrading](#upgrade) 7 | * [Updating coin list](#update) 8 | * [Configure Exchanges](#configure) 9 | 10 | #### Usage 11 | * [Coin prices](#price) 12 | * [Check exchange funds](#funds) 13 | * [Buying coins](#buy) 14 | 15 | #### Security 16 | * [Encrypting API keys](#lock) 17 | * [Decrypting API keys](#unlock) 18 | 19 | #### Automation 20 | * [Buy all the coins](#buyallthecoins) 21 | 22 | ## Install 23 | Install it globally on your computer. 24 | `npm install -g coinx` 25 | 26 | ## Upgrade to the latest version 27 | Coinx is currently at version 0.10.0 You can upgrade with npm: 28 | `npm update -g coinx` 29 | 30 | ## Supported Exchanges 31 | Currently: Kraken, Poloniex, Bitfinex, Liqui, Bittrex. 32 | 33 | ## Configure 34 | The tool uses your exchange API keys to make requests and queries. You'll have to get your API keys from each exchange manually, but then you can store it in the tool by using the `config` command. 35 | ```bash 36 | $ coinx config kraken 37 | ? Kraken API Key abcd 38 | ? Kraken API Secret efgh 39 | Saved data for Kraken 40 | ``` 41 | 42 | Note: Your API Keys and Secrets are stored in your operating system home directory in a `coinx` directory as a JSON file. 43 | 44 | ## Update 45 | Use `coinx update` to update coinx with the latest list of coins from [coinmarketcap.com](https://coinmaketcap.com). 46 | 47 | ## Coin Price 48 | Get the price of any crypto-currency by using the coin's symbol. Bitcoin is shown in US Dollars, all other coins are shown in BTC and in US Dollars. 49 | 50 | For example, to get the price of Bitcoin: 51 | ```bash 52 | $ coinx price btc 53 | Getting prices for BTC... 54 | Exchange Price in USD 55 | Liqui $2419.87 56 | Bitfinex $2429.68 57 | Poloniex $2431.92 58 | Bittrex $2442.46 59 | Kraken $2454.00 60 | 61 | Average $2435.59 62 | ``` 63 | Or, to get the price of Etherium: 64 | ```bash 65 | $ coinx price eth 66 | Getting prices for Ethereum (ETH)... 67 | Exchange Price in BTC Price in USD 68 | Liqui 0.08789270 $208.30 69 | Poloniex 0.08809500 $208.78 70 | Kraken 0.08811500 $208.82 71 | Bitfinex 0.08821900 $209.07 72 | Bittrex 0.08840483 $209.51 73 | 74 | Average 0.08814531 $208.89 75 | ``` 76 | 77 | Or, for Siacoin: 78 | ```bash 79 | $ coinx price sc 80 | Getting prices for Siacoin (SC)... 81 | Exchange Price in BTC Price in USD 82 | Bittrex 0.00000335 $0.01 83 | Poloniex 0.00000333 $0.01 84 | 85 | Average 0.00000334 $0.01 86 | ``` 87 | 88 | ## Check Exchange Funds 89 | Check your balances on the exchanges. 90 | 91 | ```bash 92 | $ coinx funds 93 | Getting balances... 94 | Poloniex 95 | Name Symbol Count Value USD 96 | Bitcoin BTC 0.03227520 $76.51 97 | Siacoin SC 2465.11765598 $19.46 98 | NEM XEM 151.10258763 $18.43 99 | Dash DASH 0.09817530 $16.94 100 | 101 | ... 102 | ``` 103 | Options: 104 | ```bash 105 | $ coinx funds --help 106 | Options: 107 | 108 | -e, --exchange [name] Get balances at the specified exchange. 109 | -a, --alphabetically Sort the balance list alphabetically. 110 | -n, --numerically Sort the balance list by the number of coins, descending. 111 | -c, --coin [symbol] Only get balances for this coin. 112 | ``` 113 | For example, to check balances only on Liqui: 114 | ```bash 115 | $ coinx funds -e poloniex 116 | Getting balances on Liqui... 117 | Liqui 118 | Name Symbol Count Value USD 119 | Bitcoin BTC 0.02564645 $30.77 120 | Ethereum ETH 0.08706164 $18.04 121 | Augur REP 0.66674308 $13.59 122 | MobileGo MGO 17.23038495 $13.33 123 | ... 124 | ``` 125 | Or, to check how many BTC you have on all the exchanges: 126 | ```bash 127 | $ coinx funds -c btc 128 | Getting balances... 129 | Poloniex 130 | Name Symbol Count Value USD 131 | Bitcoin BTC 0.00227520 $6.53 132 | Total $6.53 133 | Kraken 134 | Name Symbol Count Value USD 135 | Bitcoin BTC 0.00237879 $6.40 136 | Total $6.40 137 | Liqui 138 | Name Symbol Count Value USD 139 | Bitcoin BTC 0.00256464 $6.81 140 | Total $6.81 141 | 142 | 143 | ``` 144 | ## Buy Coins 145 | Buy a coin by specifying, in US dollars, how much you want to spend. Note that BTC is what will actually be spent! You must have the necessary BTC available on the exchange for the purchase to go through. 146 | 147 | Coinx will automatically use the exchange with the best rate, unless you specify an exchange to use via the `--exchange` option. 148 | 149 | Before the purchase goes through, you'll be asked to confirm. 150 | 151 | For example, to buy $2 worth of AntShares at the best available price: 152 | ```bash 153 | $ coinx buy ans -$ 2 154 | Checking AntShares (ANS) on the markets... 155 | Best price found on Bittrex at $8.14 156 | 157 | *Note that the number of coins may change slightly if the market fluctuates* 158 | ? Buy about 0.24562982 worth of ANS? Yes 159 | Buying... 160 | Order complete! 161 | Bittrex order number xxxxx-xxxxx-xxxxxxx 162 | Bought 0.2461696 ANS 163 | Worth about $2.00 164 | ``` 165 | 166 | Or, to buy $2 worth of Ethereum on the Liqui exchange: 167 | ```bash 168 | $ coinx buy eth -e liqui -$ 2 169 | Checking Ethereum (ETH) on the markets... 170 | Best price found on Liqui at $278.70 171 | 172 | *Note that the number of coins may change slightly if the market fluctuates* 173 | 174 | ? Buy about 0.00717629 worth of ETH? Yes 175 | Buying... 176 | Order complete! 177 | Liqui order number 0 178 | Bought 0.00717629 ETH 179 | Worth about $2.00 180 | ``` 181 | The results of all purchases are logged into `{home folder}/coinx/log.csv`. 182 | ## Sell Coins 183 | Coming soon. 184 | 185 | ## Lock 186 | Encrypt the file that contains your API keys. Please choose a good password, and don't forget it. If you do forget it, you'll have to delete the `coinx.json` file in your `{homedir}/coinx` folder, and rerun `coinx config` for each exchange. 187 | ``` 188 | coinx lock password 189 | ``` 190 | After you lock your config, you will not be able to use coinx until you unlock it. 191 | 192 | ## Unlock 193 | Decrypt your API key file. 194 | ``` 195 | coinx unlock password 196 | ``` 197 | 198 | ## Actions 199 | Actions are a way to automate steps in coinx. 200 | 201 | ### Buy all the Coins! 202 | Lets you buy the top crypto coins by market cap. Specify how much you want to spend per coin (-$), and how many coins you want to buy (-t, default is 50). 203 | 204 | You can exclude coins by putting their symbol into the `exclude_coins.txt` in the actions/buyallthecoins folder. 205 | ``` 206 | $ coinx action buyallthecoins -$ 2 -t 50 207 | Buy all the coins! 208 | Spend per coin: $2 209 | Coins to buy: 50 210 | Maximum spend: $100 (assumes all coins available to buy) 211 | Will not buy: BTC, USDT, BCC, VERI 212 | ? Proceed? Yes 213 | Getting latest Market Cap list... 214 | Got list... 215 | Skipping excluded coin Bitcoin. 216 | 217 | Buying Ethereum. 218 | Checking Ethereum (ETH) on the markets... 219 | Best price found on Bittrex at $228.64 220 | Buying 0.00874752 Ethereum on Bittrex 221 | Order complete! 222 | Bittrex order number e2fffabd-915b-4d13-bf37-af534cf82af8 223 | Bought 0.00874747 ETH at 0.08268905 BTC per coin 224 | Worth about $2.00 225 | 226 | Buying Ripple. 227 | Checking Ripple (XRP) on the markets... 228 | Best price found on Bittrex at $0.20 229 | ... 230 | 231 | ``` 232 | --------------------------------------------------------------------------------