├── 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 |
--------------------------------------------------------------------------------