UNPKG

bitdo2

Version:

A cryptocurrency order-rule executor

328 lines (311 loc) 9.44 kB
#!/usr/bin/env node const Promise = require('bluebird'); const Plugins = require('./plugins'); const Exchanges = require('./exchanges'); const config = require('./config'); const _ = require('lodash'); const HoldingsService = require('./services/holdings'); const DB = require('./lib/db'); const moment = require('moment'); const log = require('./log'); const fs = require('fs'); log.enableConsole(); /* Uses the web plugin to generate your own custom dashboard */ const args = require('yargs') .describe('db', 'Database URL') .string('db') .default('db', config.db || 'sqlite://db.sqlite') .describe('poll', 'How many seconds between polls') .number('poll') .default('poll', 60) .describe('history', 'Number of ticks ot keep in history') .number('history') .default('history', 60 * 12) .describe('state', 'Filename to save state') .string('state') .env('BITDO') .epilog('Environment variables settable with prefix BITDO_') .argv; const { Holdings } = DB(args.db); const exchanges = Exchanges.createFromConfig(config.exchanges); const holdingsService = new HoldingsService(exchanges); const plugins = Plugins.createFromConfig({ web: {}, }); function loadState() { if (args.state && fs.existsSync(args.state)) { log.info(`Loading state file ${args.state}`); try { const raw = fs.readFileSync(args.state, 'utf8'); return JSON.parse(raw); } catch (err) { log.error(`Unable to read state file: ${err.message}`); } } return { tickers: {} }; } const { tickers } = loadState(); function update() { holdingsService.getHoldings() .then(holdings => _.orderBy(holdings, h => h.conversions.USD, 'desc')) .then(holdings => _.filter(holdings, h => h.balance > 0)) .then(holdings => [ holdings, Promise.map(exchanges, exch => exch.getOrders()).then(_.flatten), ]) .spread((holdings, orders) => { plugins.graph('holdings', { type: 'bar', data: { labels: _.map(holdings, 'currency'), datasets: [{ label: 'USD', backgroundColor: 'rgba(255,0,0,0.5)', borderColor: 'rgba(255,0,0,1.0)', borderWidth: 1, data: _.map(holdings, 'conversions.USD'), }], }, }); _.each(holdings, ({ currency, ticker, balance, exchange }) => { if (currency === 'USD') return; if (!tickers[currency]) { tickers[currency] = { prices: [], }; } const currencyTicker = tickers[currency]; currencyTicker.prices.push({ ts: new Date(), price: ticker.USD, balance, }); while (currencyTicker.prices.length > args.history) currencyTicker.prices.shift(); plugins.graph(`holdings-${exchange.name}-${currency}`, { type: 'bar', data: { labels: _.map(currencyTicker.prices, 'ts'), datasets: [{ type: 'line', label: 'USD-Price', fill: true, pointRadius: 0, backgroundColor: 'rgba(255,0,0,0.5)', borderColor: 'rgba(255,0,0,1.0)', data: _.map(currencyTicker.prices, 'price'), yAxisID: 'y-axis-1', }, { type: 'line', label: 'Balance', fill: false, borderWidth: 1, pointRadius: 0, backgroundColor: 'rgba(127,127,127,0.2)', borderColor: 'rgba(127,127,127,0.5)', data: _.map(currencyTicker.prices, 'balance'), yAxisID: 'y-axis-2', }, { type: 'line', label: 'Buys', backgroundColor: 'rgba(0,255,0,0.4)', borderColor: 'rgba(0,255,0,0.8)', data: _(orders) .filter(o => o.product.substr(0, 3) === currency && o.exchange.name === exchange.name && o.status === 'F' && o.side === 'buy' && moment(o.date) >= moment(_(currencyTicker).map('ts').min()).subtract(1, 'week')) .map(o => ({ x: o.date, y: o.price, text: `${o.size}` })), fill: true, pointRadius: 10, pointHoverRadius: 15, showLine: false, }, { type: 'line', label: 'Sells', backgroundColor: 'rgba(128,0,255,0.4)', borderColor: 'rgba(128,0,255,0.8)', data: _(orders) .filter(o => o.product.substr(0, 3) === currency && o.exchange.name === exchange.name && o.status === 'F' && o.side === 'sell' && moment(o.date) >= moment(_(currencyTicker).map('ts').min()).subtract(1, 'week')) .map(o => ({ x: o.date, y: o.price, text: `${o.size}` })), fill: true, pointRadius: 10, pointHoverRadius: 15, showLine: false, }], }, options: { stacked: false, title: { text: `[${exchange.name}] ${currency}`, }, scales: { xAxes: [{ type: 'time', distribution: 'series', ticks: { source: 'labels', minRotation: 30, autoSkip: true, }, time: { tooltipFormat: 'll HH:mm', }, }], yAxes: [{ type: 'linear', display: true, position: 'left', id: 'y-axis-1', }, { type: 'linear', display: true, position: 'right', id: 'y-axis-2', gridLines: { drawOnChartArea: false, }, }], }, }, }); }); }); Holdings.findSumOverTime({ where: { createdAt: { $gt: moment().subtract(7, 'day').toDate() }, }, }).then(results => { if (results.length > 0) { const hourOHLC = _(results) .groupBy(x => moment(x.ts).startOf('hour')) .map((dayItems, ts) => { const amounts = _.map(dayItems, x => x.get('sumUsd')); return { ts, mean: _.mean(amounts), }; }) .value(); plugins.graph('Long-term-USD', { type: 'line', data: { labels: _.map(hourOHLC, 'ts'), datasets: [{ label: 'USD', fill: true, pointRadius: 0, backgroundColor: 'rgba(255,0,0,0.5)', borderColor: 'rgba(255,0,0,1.0)', borderWidth: 1, data: _.map(hourOHLC, 'mean'), }], }, options: { scales: { xAxes: [{ type: 'time', distribution: 'series', ticks: { source: 'labels', minRotation: 30, autoSkip: true, }, time: { tooltipFormat: 'll HH:mm', }, }], }, }, html: { width: '66%', }, }); const bucketedOHLC = _(results) .groupBy(x => moment(x.ts).startOf('day')) .map((dayItems, ts) => { const amounts = _.map(dayItems, x => x.get('sumUsd')); return { ts, min: _.min(amounts), max: _.max(amounts), open: amounts[0], close: amounts[amounts.length - 1], }; }) .value(); plugins.graph('Holdings Low/High', { type: 'line', data: { labels: _.map(bucketedOHLC, 'ts'), datasets: [{ label: 'Min', fill: '1', borderColor: 'rgba(255,0,0,0.8)', tension: 0, data: _.map(bucketedOHLC, 'min'), }, { label: 'Max', fill: '-1', backgroundColor: 'rgba(0,255,0,0.4)', borderColor: 'rgba(0,255,0,0.8)', tension: 0, data: _.map(bucketedOHLC, 'max'), }], }, options: { scales: { xAxes: [{ type: 'time', distribution: 'series', ticks: { source: 'labels', minRotation: 30, autoSkip: true, }, time: { tooltipFormat: 'll HH:mm', }, }], }, }, }); } }); } function save(cb) { try { if (args.state) { log.debug('Saving state...'); const out = JSON.stringify({ tickers }); return fs.writeFile(args.state, out, 'utf8', (err) => { if (!err) log.info(`Saved state to ${args.state}`); else log.warn(`Error saving state to ${args.state}: ${err.message}`); if (cb) cb(); }); } } catch (err) { log.error(`Failed saving state to ${args.state}: ${err.message}`); } if (cb) cb(); return null; } setInterval(update, args.poll * 1000); update(); process.on('SIGINT', () => { save(() => { process.exit(0); }); });