diff --git a/.gitignore b/.gitignore index 11399b5391..5e7eec6c36 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ npm-debug.log db.json dump conf/secret.json +github.pub +github diff --git a/README.md b/README.md index 279d1b5350..e4036173e7 100644 --- a/README.md +++ b/README.md @@ -7,38 +7,55 @@ - Follow zenbot [on Twitter!](https://twitter.com/zenbot_btc) - Check out zenbot's [live feed!](https://zenbot.s8f.org/) -- Join the discussion on [Reddit!](https://www.reddit.com/r/Bitcoin/comments/4rym6o/zenbot_an_automated_bitcoin_trading_bot_for_gdax/) +- Join the discussion on [Reddit!](https://www.reddit.com/r/Bitcoin/comments/4xqo8q/announcing_zenbot_3_your_new_btcethltc_trading/)! + +## Updates + +- **3.1.2** - Relaxed backfill timeout. Backfill is slower to let reducer catch up. Reducer report interval -> 30s, Trade report interval -> 30s + +- **3.1.1** - Updated zenbrain version. Please run `./update.sh`. + +- **3.1.0** - Major logic update. Much of the default trade logic reprogrammed. Moved default logic to `./default_logic.js`. RSI now backfills by default, reconfigured to 15m intervals. Safe to drop your zenbrain DB before this update. ## Description zenbot is an automated cryptocurrency trading bot. It runs on node.js and MongoDB and is fully open-sourced. A plugin architecture is included that allows any exchange, trade strategy, or currency pair to be supported. -- Out of the box, zenbot is an AI-powered trade advisor for GDAX (gives you buy or sell signals while watching live data). -- Enable trades by simply giving it your GDAX API key. -- Trade strategy is exposed in the config file. This allows you to have full control over the bot's actions and logic. For example, instead of trading on GDAX, you could trade on a different exchange or currency pair by implementing a few lines of JavaScript. +- Out of the box, zenbot is an AI-powered trade advisor (gives you buy or sell signals while watching live data). +- Default support for [GDAX](https://gdax.com/) is included, so if you have a GDAX account, enable bot trades by simply putting your GDAX API key in `config.js` and setting what currency pair to trade. +- Default support for other exchanges is ongoing. +- Trade strategy is fully exposed in the config file. This allows you to have full control over the bot's actions and logic. For example, instead of trading on GDAX, you could trade on a different exchange or currency pair by implementing a few lines of JavaScript. - A live candlestick graph is provided via a built-in HTTP server. HOWEVER. BE AWARE that once you hook up zenbot to a live exchange, the damage done is your fault, not mine! **As with buying bitcoin in general, risk is involved and caution is essential. bitcoin is an experiment, and so is zenbot.** ## Screenshot -![screenshot](https://rawgit.com/carlos8f/zenbot/master/assets/zenbot_web_ui.png) +![screenshot](https://raw.githubusercontent.com/carlos8f/zenbot/master/assets/zenbot_web_ui.png) ## Quick-start ### 1. Requirements: [Node.js](https://nodejs.org/) and [MongoDB](https://www.mongodb.com/download-center) +#### Windows - I don't support it. + +If you're having an error on Windows and you're about to give up, it's probaby because Node.js is generally broken on Windows and you should try running on a Linux docker container or a Mac instead. + +If you're still insistent on using Windows, you'll have to fork zenbot, fix it yourself, and I'll accept a Pull Request. + ### 2. Install zenbot 3: -```shell -curl --silent https://raw.githubusercontent.com/carlos8f/zenbot/master/install.sh | /bin/sh +``` +git clone https://github.com/carlos8f/zenbot.git +cd zenbot +npm install ``` -### 3. Edit `zenbot/config.js` with API keys, database credentials, trade logic, etc. +### 3. Edit `config.js` with API keys, database credentials, trade logic, etc. ### 4. Run zenbot on the exchange: -```shell +``` ./run.sh ``` @@ -46,7 +63,7 @@ curl --silent https://raw.githubusercontent.com/carlos8f/zenbot/master/install.s To access the CLI, -```shell +``` zenbot Usage: zenbot [options] [command] @@ -72,17 +89,34 @@ The `./run.sh` script combines `launch map --backfill reduce run server`, so use Once backfill has finished, run a simulation: -```shell -zenbot sim +``` +zenbot sim [--verbose] ``` Zenbot will return you a list of virtual trades, and an ROI figure. Open the URL provided in the console (while running the server) to see the virtual trades plotted on a candlestick graph. Tweak `config.js` for new trade strategies and check your results this way. +Example simulation graph: https://zenbot.s8f.org/?sim_id=9cb6ac63f85168e3&selector=gdax.BTC-USD&period=6h&limit=2000 + +#### About the default trade logic in `default_logic.js` + +- uses [GDAX](https://gdax.com/) API +- watches/trades BTC/USD +- acts at 1m increments (ticks), but you can configure to act quicker or slower. +- computes 14-period 15m RSI +- considers `RSI >= 70` overbought and `RSI <= 20` oversold +- trades 95% of current balance, market price + +You can tweak the JS from there to use bitfinex, or trade ETH, or whatever. After tweaking `default_logic.js`, Use `zenbot sim` to check your strategy against historical trades. + +Note that simulations always end on Wednesday 5pm PST, and run for a max 90 days, to ensure consistency of results. + +Auto-learn support and more exchange support will come soon. Will accept PR's :) With the 3.x plugin architecture, external plugins are possible too (published as their own repo/module). + ### 7. Web console When the server is running, and you have visited the `?secret` URL provided in the console, you can access an aggregated, live feed of log messages at `http://localhost:3013/logs`. Example: -![screenshot](https://rawgit.com/carlos8f/zenbot/master/assets/zenbot_web_logs.png) +![screenshot](https://raw.githubusercontent.com/carlos8f/zenbot/master/assets/zenbot_web_logs.png) ## FAQ @@ -116,32 +150,21 @@ While the advanced mathematical nature of such adaptive systems has kept neural Source: [Wikipedia](https://en.wikipedia.org/wiki/Technical_analysis#Systematic_trading) -- - - +## Donate -### License: MIT +P.S., some have asked for how to donate to Zenbot development. I accept donations at **my Bitcoin address** Here: -- Copyright (C) 2016 Carlos Rodriguez (http://s8f.org/) -- Copyright (C) 2016 Terra Eclipse, Inc. (http://www.terraeclipse.com/) +### carlos8f's BTC -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: +`187rmNSkSvehgcKpBunre6a5wA5hQQop6W` -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +![zenbot logo](https://s8f.org/files/bitcoin.png) -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +thanks! +## Discuss +Join the [discussion on Reddit](https://www.reddit.com/r/Bitcoin/comments/4xqo8q/announcing_zenbot_3_your_new_btcethltc_trading/)! - - - ### License: MIT @@ -166,4 +189,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/config.js b/config.js index ede12e4aad..334116694f 100644 --- a/config.js +++ b/config.js @@ -1,24 +1,15 @@ var c = module.exports = {} + +c.key = '' // TO ENABLE BOT TRADING: set this to GDAX api key, +c.secret = '' // set this to GDAX api secret, +c.passphrase = '' // set this to GDAX api passphrase. + +// mongo stuff c.mongo_url = "mongodb://localhost:27017/zenbrain" // change if your mongo server isn't local c.mongo_username = null // normally not needed c.mongo_password = null -c.bucket_size = "1m" -c.reducer_limit = 500 // how many thoughts to process per reduce run -c.reducer_sizes = ["1m", "5m", "15m", "1h", "6h", "1d"] -c.save_state_interval = 10000 // save state -c.parallel_limit = 8 // run this many concurrent tasks -c.reduce_timeout = 200 -c.run_limit = 100 -c.lock_timeout = 60000 -c.lock_backoff = 20 -c.lock_tries = 100 -c.passive_update_timeout = 5000 -c.return_timeout = 60000 -c.brain_speed_ms = 200 -c.twitter_key = "" // create a twitter app, generate an access token, and add it here -c.twitter_secret = "" -c.twitter_access_token = "" -c.twitter_access_token_secret = "" + +// add assets/currencies you want to track c.assets = [ "BTC", //"ETH", @@ -29,14 +20,23 @@ c.currencies = [ //"EUR", "USD" ] +// will require(plugins/{name}/_codemap or {name}/_codemap) c.enabled_plugins = [ //"bitfinex", "gdax", //"poloniex", "server" ] -c.default_graph_period = "1h" -c.default_graph_limit = 300 + +// twitter stuff +c.twitter_key = "" // create a twitter app, generate an access token, and add it here +c.twitter_secret = "" +c.twitter_access_token = "" +c.twitter_access_token_secret = "" + +// graph server +c.default_graph_period = "15m" +c.default_graph_limit = 1000 c.graph_limits = [50, 100, 150, 200, 300, 500, 1000, 2000] c.graph_selectors = [ "gdax.BTC-USD", @@ -52,193 +52,19 @@ c.graph_selectors = [ //"bitfinex.ETC-BTC", //"bitfinex.ETC-USD" ] +c.log_query_limit = 200 +c.tracking_scripts = '' + +// RSI indicator config +c.rsi_sizes = ['15m'] +c.rsi_reporter_selector = "gdax.BTC-USD" c.rsi_query_limit = 100 c.rsi_periods = 14 -c.rsi_reporter_selector = "gdax.BTC-USD" -c.rsi_sizes = ['15m', '1h'] -c.key = '' // TO ENABLE BOT TRADING: set this to GDAX api key, -c.secret = '' // set this to GDAX api secret, -c.passphrase = '' // set this to GDAX api passphrase. -var first_run = true -var last_balance_sig -c.logic = function container (get, set, clear) { - var o = get('utils.object_get') - var n = require('numbro') - var sig = require('sig') - var format_currency = get('utils.format_currency') - var get_timestamp = get('utils.get_timestamp') - var CoinbaseExchange = require('coinbase-exchange') - var client = new CoinbaseExchange.AuthenticatedClient(c.key, c.secret, c.passphrase) - var asset = 'BTC' - var currency = 'USD' - var rsi_period = '15m' - var exchange = 'gdax' - var selector = 'data.trades.' + exchange + '.' + asset + '-' + currency - function onOrder (err, resp, order) { - if (err) return get('logger').error('order err', err, resp, order, {feed: 'errors'}) - if (resp.statusCode !== 200) { - console.error(order) - return get('logger').error('non-200 status: ' + resp.statusCode, {data: {statusCode: resp.statusCode, body: order}}) - } - get('logger').info(exchange, ('order-id: ' + order.id).cyan, {data: {order: order}}) - function getStatus () { - client.getOrder(order.id, function (err, resp, order) { - if (err) return get('logger').error('getOrder err', err) - if (resp.statusCode !== 200) { - console.error(order) - return get('logger').error('non-200 status from getOrder: ' + resp.statusCode, {data: {statusCode: resp.statusCode, body: order}}) - } - if (order.status === 'done') { - return get('logger').info(exchange, ('order ' + order.id + ' done: ' + order.done_reason).cyan, {data: {order: order}}) - } - else { - get('logger').info(exchange, ('order ' + order.id + ' ' + order.status).cyan, {data: {order: order}}) - setTimeout(getStatus, 5000) - } - }) - } - getStatus() - } - return [ - // BEGIN DEFAULT TRADE LOGIC - // sync balance - function (tick, trigger, rs, cb) { - if (get('command') !== 'run' || !c.key) { - return cb() - } - client.getAccounts(function (err, resp, accounts) { - if (err) throw err - if (resp.statusCode !== 200) { - console.error(accounts) - get('logger').error('non-200 status from exchange: ' + resp.statusCode, {data: {statusCode: resp.statusCode, body: accounts}}) - return cb && cb() - } - rs.balance = {} - accounts.forEach(function (account) { - if (account.currency === currency) { - rs.balance[currency] = n(account.balance).value() - } - else if (account.currency === asset) { - rs.balance[asset] = n(account.balance).value() - } - }) - var balance_sig = sig(rs.balance) - if (balance_sig !== last_balance_sig) { - get('logger').info(exchange, 'balance'.grey, n(rs.balance[asset]).format('0.000').white, asset.grey, n(rs.balance[currency]).format('0.00').yellow, currency.grey, {feed: 'exchange'}) - first_run = false - last_balance_sig = balance_sig - } - cb && cb() - }) - }, - function (tick, trigger, rs, cb) { - // note the last close price - var market_price = o(tick, selector + '.close') - if (market_price) { - rs.market_price = market_price - } - if (!rs.market_price) return cb() - if (!rs.balance) { - // start with $1000, neutral position - rs.balance = {} - rs.balance[currency] = 500 - rs.balance[asset] = n(500).divide(rs.market_price).value() - } - rs.ticks || (rs.ticks = 0) - rs.ticks++ - if (tick.size !== rsi_period) return cb() - // get rsi - var rsi = o(tick, selector + '.rsi') - // require minimum data - // overbought/oversold - // sanity check - if (rsi && rsi.samples >= c.rsi_periods) { - rs.rsi = Math.round(rsi.value) - rs.rsi_ansi = rsi.ansi - if (rsi.value > 70) { - rs.overbought = true - rs.oversold = false - } - else if (rsi.value < 30) { - rs.oversold = true - rs.overbought = false - } - } - cb() - }, - // @todo MACD - function (tick, trigger, rs, cb) { - cb() - }, - // trigger trade signals - function (tick, trigger, rs, cb) { - if ((rs.overbought || rs.oversold) && rs.balance && rs.market_price) { - var size, new_balance = {} - if (rs.overbought) { - get('logger').info('trader', 'RSI:'.grey + rs.rsi_ansi, 'anticipating a reversal DOWN. sell at market. (' + format_currency(rs.market_price, currency) + ')', {feed: 'trader'}) - size = rs.balance[asset] - } - else if (rs.oversold) { - get('logger').info('trader', 'RSI:'.grey + rs.rsi_ansi, 'anticipating a reversal UP. buy at market. (' + format_currency(rs.market_price, currency) + ')', {feed: 'trader'}) - size = n(rs.balance[currency]).divide(rs.market_price).value() - } - // scale down size a little, to prevent out-of-balance errors - size = n(size || 0).multiply(0.95).value() - // min size - if (!size || size < 0.01) { - if (rs.overbought) { - get('logger').info('trader', 'RSI:'.grey + rs.rsi_ansi, ('not enough ' + asset + ' to execute sell!').red, {feed: 'trader'}) - } - else if (rs.oversold) { - get('logger').info('trader', 'RSI:'.grey + rs.rsi_ansi, ('not enough ' + currency + ' to execute buy!').red, {feed: 'trader'}) - } - rs.overbought = rs.oversold = false - return cb() - } - if (rs.overbought) { - new_balance[currency] = n(rs.balance[currency]).add(n(size).multiply(rs.market_price)).value() - new_balance[asset] = n(rs.balance[asset]).subtract(size).value() - } - else if (rs.oversold) { - new_balance[asset] = n(rs.balance[asset]).add(size).value() - new_balance[currency] = n(rs.balance[currency]).subtract(n(size).multiply(rs.market_price)).value() - } - // consolidate balance - var new_end_balance = n(new_balance[currency]).add(n(new_balance[asset]).multiply(rs.market_price)).value() - var new_roi = n(new_end_balance).divide(1000).value() - rs.balance = new_balance - rs.end_balance = new_end_balance - rs.roi = new_roi - rs.trades || (rs.trades = 0) - rs.trades++ - trigger({ - type: rs.overbought ? 'sell' : 'buy', - asset: asset, - currency: currency, - exchange: exchange, - price: rs.market_price, - market: true, - size: size, - rsi: rs.rsi, - roi: rs.roi - }) - if (get('command') === 'run' && c.key) { - var params = { - type: 'market', - size: n(size).format('0.000000'), - product_id: asset + '-' + currency - } - client[rs.overbought ? 'sell' : 'buy'](params, function (err, resp, order) { - onOrder(err, resp, order) - }) - } - rs.overbought = rs.oversold = false - } - cb() - } - // END DEFAULT TRADE LOGIC - ] -} + +// trade logic +c.logic = require('./default_logic') + +// reporter c.reporter_sizes = ['15m'] c.price_reporter_selector = "gdax.BTC-USD" c.price_reporter_length = 9 @@ -248,15 +74,32 @@ c.reporter_cols = [ "timestamp", "rsi", "volume", - "price" + "price", + "progress" ] +c.reducer_report_interval = 30000 +c.trade_report_interval = 30000 + +// backfiller c.backfill_days = 91 c.record_timeout = 20000 c.backfill_timeout = 5000 -c.reducer_report_interval = 2000 -c.trade_report_interval = 10000 + +// simulator c.sim_input_unit = "7d" c.sim_input_limit = 12 -c.log_query_limit = 200 -c.tracking_scripts = '' +// zenbrain engine stuff +c.bucket_size = "1m" +c.reducer_limit = 500 // how many thoughts to process per reduce run +c.reducer_sizes = ["1m", "5m", "15m", "1h", "6h", "1d"] +c.save_state_interval = 10000 // save state +c.parallel_limit = 8 // run this many concurrent tasks +c.reduce_timeout = 200 +c.run_limit = 100 +c.lock_timeout = 60000 +c.lock_backoff = 20 +c.lock_tries = 100 +c.passive_update_timeout = 5000 +c.return_timeout = 60000 +c.brain_speed_ms = 200 diff --git a/core/action_handler.js b/core/action_handler.js index 0a48b650a7..bd7afd68c1 100644 --- a/core/action_handler.js +++ b/core/action_handler.js @@ -6,7 +6,7 @@ module.exports = function container (get, set, clear) { var get_tick_str = get('utils.get_tick_str') var map = get('map') return function action_handler (tick, action, rs, cb) { - get('logger').info('action', get_tick_str(tick.id), action.type.grey) + get('logger').info('action', get_tick_str(tick.id), action.type.grey, action, {feed: 'actions'}) if (get('command') === 'run') { if (action.type === 'buy' || action.type === 'sell') { // @todo trade api diff --git a/core/indicators/_codemap.js b/core/indicators/_codemap.js index c45ce81d3d..32567f9966 100644 --- a/core/indicators/_codemap.js +++ b/core/indicators/_codemap.js @@ -3,6 +3,7 @@ module.exports = { _maps: [ require('./num_trades/_codemap'), require('./price/_codemap'), + require('./progress/_codemap'), require('./rsi/_codemap'), require('./tick_id/_codemap'), require('./timestamp/_codemap'), diff --git a/core/indicators/progress/_codemap.js b/core/indicators/progress/_codemap.js new file mode 100644 index 0000000000..f6749927c1 --- /dev/null +++ b/core/indicators/progress/_codemap.js @@ -0,0 +1,4 @@ +module.exports = { + _ns: 'zenbrain', + 'reporter_cols.progress': require('./reporter_col') +} \ No newline at end of file diff --git a/core/indicators/progress/reporter_col.js b/core/indicators/progress/reporter_col.js new file mode 100644 index 0000000000..4739b54b61 --- /dev/null +++ b/core/indicators/progress/reporter_col.js @@ -0,0 +1,12 @@ +var colors = require('colors') + , n = require('numbro') + +module.exports = function container (get, set, clear) { + var c = get('config') + var z = get('utils.zero_fill') + return function reporter_col (g, cb) { + var line = z(4, n(g.rs.progress).format('0%')).yellow + g.cols.push(line) + cb() + } +} \ No newline at end of file diff --git a/core/indicators/rsi/_codemap.js b/core/indicators/rsi/_codemap.js index ac538d3ccd..f499d7dd5a 100644 --- a/core/indicators/rsi/_codemap.js +++ b/core/indicators/rsi/_codemap.js @@ -1,26 +1,7 @@ module.exports = { _ns: 'zenbrain', - 'mappers[]': require('./backfiller'), + 'mappers.rsi_backfiller': require('./backfiller'), + 'mappers[]': '#mappers.rsi_backfiller', 'reporter_cols.rsi': require('./reporter_col'), - 'tick_reducers[10]': require('./tick_reducer'), - '@commands.map': function container (get, set, clear) { - return function alter (command) { - command.options || (command.options = []) - command.options.push({ - name: 'backfill_rsi', - description: 'backfill RSI indicator (expensive)' - }) - return command - } - }, - '@commands.reduce': function container (get, set, clear) { - return function alter (command) { - command.options || (command.options = []) - command.options.push({ - name: 'backfill_rsi', - description: 'backfill RSI indicator (expensive)' - }) - return command - } - } + 'tick_reducers[10]': require('./tick_reducer') } \ No newline at end of file diff --git a/core/indicators/rsi/backfiller.js b/core/indicators/rsi/backfiller.js index b56e0dbb4d..ab41b5b5ec 100644 --- a/core/indicators/rsi/backfiller.js +++ b/core/indicators/rsi/backfiller.js @@ -10,7 +10,7 @@ module.exports = function container (get, set, clear) { var z = get('utils.zero_fill') return function mapper () { var options = get('options') - if (!options.backfill_rsi) return + if (!options.backfill) return var min_time, num_marked = 0 function getNext () { var params = { @@ -47,10 +47,7 @@ module.exports = function container (get, set, clear) { setImmediate(getNext) } else { - get('logger').info('RSI', 'marked'.grey, num_marked, 'ticks for RSI backfill'.grey) - get('app').close(function () { - process.exit() - }) + get('logger').info('RSI', 'marked'.grey, num_marked, c.rsi_sizes.join(',') + ' ticks for RSI backfill'.grey) } }) } diff --git a/core/indicators/rsi/tick_reducer.js b/core/indicators/rsi/tick_reducer.js index 30c92450ad..12a29355a0 100644 --- a/core/indicators/rsi/tick_reducer.js +++ b/core/indicators/rsi/tick_reducer.js @@ -13,7 +13,7 @@ module.exports = function container (get, set, clear) { return function tick_reducer (g, cb) { var options = get('options') var tick = g.tick - if (c.rsi_sizes.indexOf(tick.size) === -1 || !tick.data.trades || (tick.time < start && !options.backfill_rsi)) return cb() + if (c.rsi_sizes.indexOf(tick.size) === -1 || !tick.data.trades) return cb() //console.error('computing RSI', tick.id) var bucket = tb(tick.time).resize(tick.size) var d = tick.data.trades diff --git a/core/reporter.js b/core/reporter.js index c4a967baeb..be0243a5e3 100644 --- a/core/reporter.js +++ b/core/reporter.js @@ -12,7 +12,8 @@ module.exports = function container (get, set, clear) { if (c.reporter_sizes.indexOf(tick.size) === -1 || !tick.data.trades) return cb() var g = { tick: tick, - cols: [] + cols: [], + rs: rs } apply_funcs(g, reporter_cols, function (err, g) { if (err) return cb(err) diff --git a/default_logic.js b/default_logic.js new file mode 100644 index 0000000000..a50d0a8635 --- /dev/null +++ b/default_logic.js @@ -0,0 +1,229 @@ +var first_run = true +var last_balance_sig + +module.exports = function container (get, set, clear) { + var c = get('config') + var o = get('utils.object_get') + var n = require('numbro') + var tb = require('timebucket') + var sig = require('sig') + var format_currency = get('utils.format_currency') + var get_timestamp = get('utils.get_timestamp') + var CoinbaseExchange = require('coinbase-exchange') + var client + var asset = 'BTC' + var currency = 'USD' + var rsi_period = '15m' + var rsi_overbought = 70 + var rsi_oversold = 20 + var check_period = '1m' + var exchange = 'gdax' + var selector = 'data.trades.' + exchange + '.' + asset + '-' + currency + var recovery_ticks = 300 + var trade_pct = 0.95 + var min_trade = 0.01 + var start_balance = 1000 + function onOrder (err, resp, order) { + if (err) return get('logger').error('order err', err, resp, order, {feed: 'errors'}) + if (resp.statusCode !== 200) { + console.error(order) + return get('logger').error('non-200 status: ' + resp.statusCode, {data: {statusCode: resp.statusCode, body: order}}) + } + get('logger').info(exchange, ('order-id: ' + order.id).cyan, {data: {order: order}}) + function getStatus () { + client.getOrder(order.id, function (err, resp, order) { + if (err) return get('logger').error('getOrder err', err) + if (resp.statusCode !== 200) { + console.error(order) + return get('logger').error('non-200 status from getOrder: ' + resp.statusCode, {data: {statusCode: resp.statusCode, body: order}}) + } + if (order.status === 'done') { + return get('logger').info(exchange, ('order ' + order.id + ' done: ' + order.done_reason).cyan, {data: {order: order}}) + } + else { + get('logger').info(exchange, ('order ' + order.id + ' ' + order.status).cyan, {data: {order: order}}) + setTimeout(getStatus, 5000) + } + }) + } + getStatus() + } + return [ + // BEGIN DEFAULT TRADE LOGIC + // sync balance + function (tick, trigger, rs, cb) { + if (get('command') !== 'run' || !c.key) { + return cb() + } + if (!client) { + client = new CoinbaseExchange.AuthenticatedClient(c.key, c.secret, c.passphrase) + } + client.getAccounts(function (err, resp, accounts) { + if (err) throw err + if (resp.statusCode !== 200) { + console.error(accounts) + get('logger').error('non-200 status from exchange: ' + resp.statusCode, {data: {statusCode: resp.statusCode, body: accounts}}) + return cb && cb() + } + rs.balance = {} + accounts.forEach(function (account) { + if (account.currency === currency) { + rs.balance[currency] = n(account.balance).value() + } + else if (account.currency === asset) { + rs.balance[asset] = n(account.balance).value() + } + }) + var balance_sig = sig(rs.balance) + if (balance_sig !== last_balance_sig) { + get('logger').info(exchange, 'balance'.grey, n(rs.balance[asset]).format('0.000').white, asset.grey, n(rs.balance[currency]).format('0.00').yellow, currency.grey, {feed: 'exchange'}) + first_run = false + last_balance_sig = balance_sig + } + cb && cb() + }) + }, + function (tick, trigger, rs, cb) { + // note the last close price + var market_price = o(tick, selector + '.close') + if (market_price) { + rs.market_price = market_price + } + rs.ticks || (rs.ticks = 0) + rs.progress || (rs.progress = 0) + if (!rs.market_price) return cb() + if (!rs.balance) { + // start with start_balance, neutral position + rs.balance = {} + rs.balance[currency] = start_balance/2 + rs.balance[asset] = n(start_balance/2).divide(rs.market_price).value() + } + rs.ticks++ + if (tick.size !== check_period) { + return cb() + } + rs.progress = 1 + if (rs.recovery_ticks) { + rs.recovery_ticks-- + } + // what % are we to a decision? + rs.progress = recovery_ticks ? n(1).subtract(n(rs.recovery_ticks).divide(recovery_ticks)).value() : 1 + if (rs.recovery_ticks) { + return cb() + } + // check price diff + var close = o(tick || {}, selector + '.close') + // get rsi + var rsi_tick_id = tb(tick.time).resize(rsi_period).toString() + get('ticks').load(get('app_name') + ':' + rsi_tick_id, function (err, rsi_tick) { + if (err) return cb(err) + var rsi = o(rsi_tick || {}, selector + '.rsi') + var rsi_open = o(rsi_tick || {}, selector + '.open') + // require minimum data + // overbought/oversold + // sanity check + close || (close = o(rsi_tick || {}, selector + '.close')) + rs.check_diff = close ? n(close).subtract(rsi_open || close).value() : rs.check_diff || null + rs.last_close = close + if (!rsi) { + get('logger').info('trader', ('no ' + rsi_period + ' RSI').red, {feed: 'trader'}) + } + else if (rsi.samples < c.rsi_periods) { + get('logger').info('trader', (rsi_period + ' RSI: not enough samples: ' + rsi.samples).red, {feed: 'trader'}) + } + else if (!close) { + get('logger').info('trader', ('no close price').red, {feed: 'trader'}) + } + else if (rs.check_diff === null) { + get('logger').info('trader', ('not enough ticks to make decision').red, {feed: 'trader'}) + } + else { + rs.rsi = Math.round(rsi.value) + rs.rsi_ansi = rsi.ansi + if (rsi.value >= rsi_overbought && !rs.recovery_ticks) { + rs.overbought = true + } + else if (rsi.value <= rsi_oversold && !rs.recovery_ticks) { + rs.oversold = true + } + else { + get('logger').info('trader', (rsi_period + ' RSI: ').grey + rsi.ansi + ' diff: '.grey + format_currency(rs.check_diff, currency).grey, {feed: 'trader'}) + } + } + rs.recovery_ticks = recovery_ticks + 1 + cb() + }) + }, + // @todo MACD + function (tick, trigger, rs, cb) { + cb() + }, + // trigger trade signals + function (tick, trigger, rs, cb) { + if ((rs.overbought || rs.oversold) && rs.balance && rs.market_price) { + var size, new_balance = {} + if (rs.overbought) { + get('logger').info('trader', 'RSI:'.grey + rs.rsi_ansi, 'anticipating a reversal DOWN. sell at market. (' + format_currency(rs.market_price, currency) + ') diff: ' + format_currency(rs.check_diff, currency), {feed: 'trader'}) + size = rs.balance[asset] + } + else if (rs.oversold) { + get('logger').info('trader', 'RSI:'.grey + rs.rsi_ansi, 'anticipating a reversal UP. buy at market. (' + format_currency(rs.market_price, currency) + ') diff: ' + format_currency(rs.check_diff, currency), {feed: 'trader'}) + size = n(rs.balance[currency]).divide(rs.market_price).value() + } + // scale down size a little, to prevent out-of-balance errors + size = n(size || 0).multiply(trade_pct).value() + // min size + if (!size || size < min_trade) { + if (rs.overbought) { + get('logger').info('trader', 'RSI:'.grey + rs.rsi_ansi, ('not enough ' + asset + ' to execute sell!').red, {feed: 'trader'}) + } + else if (rs.oversold) { + get('logger').info('trader', 'RSI:'.grey + rs.rsi_ansi, ('not enough ' + currency + ' to execute buy!').red, {feed: 'trader'}) + } + rs.overbought = rs.oversold = false + return cb() + } + if (rs.overbought) { + new_balance[currency] = n(rs.balance[currency]).add(n(size).multiply(rs.market_price)).value() + new_balance[asset] = n(rs.balance[asset]).subtract(size).value() + } + else if (rs.oversold) { + new_balance[asset] = n(rs.balance[asset]).add(size).value() + new_balance[currency] = n(rs.balance[currency]).subtract(n(size).multiply(rs.market_price)).value() + } + // consolidate balance + var new_end_balance = n(new_balance[currency]).add(n(new_balance[asset]).multiply(rs.market_price)).value() + var new_roi = n(new_end_balance).divide(start_balance).value() + rs.balance = new_balance + rs.end_balance = new_end_balance + rs.roi = new_roi + rs.trades || (rs.trades = 0) + rs.trades++ + trigger({ + type: rs.overbought ? 'sell' : 'buy', + asset: asset, + currency: currency, + exchange: exchange, + price: rs.market_price, + market: true, + size: size, + rsi: rs.rsi, + roi: rs.roi + }) + if (get('command') === 'run' && c.key) { + var params = { + type: 'market', + size: n(size).format('0.000000'), + product_id: asset + '-' + currency + } + client[rs.overbought ? 'sell' : 'buy'](params, function (err, resp, order) { + onOrder(err, resp, order) + }) + } + rs.overbought = rs.oversold = false + } + cb() + } + // END DEFAULT TRADE LOGIC + ] +} diff --git a/install.sh b/install.sh deleted file mode 100755 index eeaa62538c..0000000000 --- a/install.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -git clone https://github.com/carlos8f/zenbot.git -cd zenbot -git checkout 3.x -git pull -npm install -npm install zenbrain -sudo rm -Rf /usr/local/bin/zenbot -sudo npm link diff --git a/package.json b/package.json index 79ba502423..c45b71ffb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenbot_trader", - "version": "2.8.10", + "version": "3.1.2", "description": "A machine-learning cryptocurrency trading bot", "bin": { "zenbot": "./zenbot" @@ -28,7 +28,7 @@ "sig": "^1.0.0", "timebucket": "^0.3.4", "twit": "^2.2.4", - "zenbrain": "git+https://github.com/carlos8f/zenbrain.git#d90d42ae9c07ef6ad50a04c2fe9ecfae4a026e01", + "zenbrain": "git+https://github.com/carlos8f/zenbrain.git#e3d5b6430da645f44a420007fb90d34753ee67cf", "zero-fill": "^2.2.3" } } diff --git a/plugins/gdax/backfiller.js b/plugins/gdax/backfiller.js index 2299bc2a10..4e8fea4bf0 100644 --- a/plugins/gdax/backfiller.js +++ b/plugins/gdax/backfiller.js @@ -40,7 +40,7 @@ module.exports = function container (get, set, clear) { map('trade', obj) return obj }) - //log_trades(x.name, trades) + log_trades(x.name, trades) if (is_backfilled(trades)) { get('logger').info(x.name, (product.asset + '/' + product.currency + ' backfill complete').grey) } diff --git a/plugins/gdax/exchange.json b/plugins/gdax/exchange.json index 0ffa65f0b2..d6a704bc4d 100644 --- a/plugins/gdax/exchange.json +++ b/plugins/gdax/exchange.json @@ -2,7 +2,7 @@ "name": "gdax", "rest_url": "https://api.gdax.com", "backfill_limit": 100, - "backfill_timeout": 0, + "backfill_timeout": 1000, "products": [ { "id":"BTC-USD", diff --git a/plugins/gdax/recorder.js b/plugins/gdax/recorder.js index fe6013086c..df3f98c4b5 100644 --- a/plugins/gdax/recorder.js +++ b/plugins/gdax/recorder.js @@ -38,7 +38,7 @@ module.exports = function container (get, set, clear) { map('trade', obj) return obj }) - //log_trades(x.name, trades) + log_trades(x.name, trades) retry() } var uri = x.rest_url + '/products/' + product.id + '/trades' + (s.recorder_id ? '?before=' + s.recorder_id : '') diff --git a/plugins/kraken/_codemap.js b/plugins/kraken/_codemap.js new file mode 100644 index 0000000000..a08f834ec3 --- /dev/null +++ b/plugins/kraken/_codemap.js @@ -0,0 +1,9 @@ +module.exports = { + _ns: 'zenbrain', + 'exchanges.kraken': require('./exchange.json'), + 'exchanges[]': '#exchanges.kraken', + 'mappers[]': [ + require('./backfiller'), + require('./recorder') + ] +} diff --git a/plugins/kraken/backfiller.js b/plugins/kraken/backfiller.js index 3e14233570..3af6c8c215 100644 --- a/plugins/kraken/backfiller.js +++ b/plugins/kraken/backfiller.js @@ -1,3 +1,100 @@ -https://www.kraken.com/help/api -https://api.kraken.com/0/public/Trades?pair=XBTUSD \ No newline at end of file +// https://www.kraken.com/help/api +// https://api.kraken.com/0/public/Trades?pair=XBTUSD +/* +id: 'gdax-8809631', + trade_id: 8809631, + time: 1464316978837, + asset: 'BTC', + currency: 'USD', + size: 0.01470572, + price: 467.45, + side: 'sell', + exchange: 'gdax' } +*/ +var request = require('micro-request') + , n = require('numbro') + +module.exports = function container (get, set, clear) { + var x = get('exchanges.kraken') + var c = get('config') + var log_trades = get('utils.log_trades') + var get_products = get('utils.get_products') + var is_backfilled = get('utils.is_backfilled') + var map = get('map') + return function mapper () { + var products = get_products(x) + var options = get('options') + if (!options.backfill || !products.length) return + var rs = get('run_state') + rs[x.name] || (rs[x.name] = {}) + rs = rs[x.name] + products.forEach(function (product) { + rs[product.id] || (rs[product.id] = {}) + var s = rs[product.id] + //s.backfiller_id = null // start from scratch + function retry () { + setTimeout(getNext, x.backfill_timeout) + } + function getNext () { + function withResult (result) { + // , ,