From 255a0577cbd243c45f50cb8c42cdeacec8c9d4a6 Mon Sep 17 00:00:00 2001 From: Chase Date: Fri, 28 Jul 2017 12:20:09 +0200 Subject: [PATCH] Strategies: TA (ema+macd) and Trust/Distrust (#285) node-gyp and node v8.x are not friends, stick to boron for now Rename talib's MACD (#2) * Rename talib's MACD so it doesn't overlap with original macd --- Dockerfile | 2 +- extensions/strategies/ta_ema/_codemap.js | 6 + extensions/strategies/ta_ema/strategy.js | 96 +++++++++++ extensions/strategies/ta_macd/_codemap.js | 6 + extensions/strategies/ta_macd/strategy.js | 86 +++++++++ .../strategies/trust_distrust/_codemap.js | 6 + .../strategies/trust_distrust/strategy.js | 136 +++++++++++++++ lib/_codemap.js | 2 + lib/ta_ema.js | 51 ++++++ lib/ta_macd.js | 56 ++++++ package-lock.json | 11 +- package.json | 1 + .../backtester_trust_distrust.js | 163 ++++++++++++++++++ 13 files changed, 618 insertions(+), 4 deletions(-) create mode 100644 extensions/strategies/ta_ema/_codemap.js create mode 100644 extensions/strategies/ta_ema/strategy.js create mode 100644 extensions/strategies/ta_macd/_codemap.js create mode 100644 extensions/strategies/ta_macd/strategy.js create mode 100644 extensions/strategies/trust_distrust/_codemap.js create mode 100644 extensions/strategies/trust_distrust/strategy.js create mode 100644 lib/ta_ema.js create mode 100644 lib/ta_macd.js create mode 100644 scripts/auto_backtester/backtester_trust_distrust.js diff --git a/Dockerfile b/Dockerfile index 34c7be96b2..110df0095b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:latest +FROM node:boron RUN mkdir -p /app WORKDIR /app diff --git a/extensions/strategies/ta_ema/_codemap.js b/extensions/strategies/ta_ema/_codemap.js new file mode 100644 index 0000000000..aeb3111b62 --- /dev/null +++ b/extensions/strategies/ta_ema/_codemap.js @@ -0,0 +1,6 @@ +module.exports = { + _ns: 'zenbot', + + 'strategies.ta_ema': require('./strategy'), + 'strategies.list[]': '#strategies.ta_ema' +} diff --git a/extensions/strategies/ta_ema/strategy.js b/extensions/strategies/ta_ema/strategy.js new file mode 100644 index 0000000000..086506dbd8 --- /dev/null +++ b/extensions/strategies/ta_ema/strategy.js @@ -0,0 +1,96 @@ +var z = require('zero-fill') + , n = require('numbro') + +module.exports = function container (get, set, clear) { + return { + name: 'ta_ema', + description: 'Buy when (EMA - last(EMA) > 0) and sell when (EMA - last(EMA) < 0). Optional buy on low RSI.', + + getOptions: function () { + this.option('period', 'period length', String, '10m') + this.option('min_periods', 'min. number of history periods', Number, 52) + this.option('trend_ema', 'number of periods for trend EMA', Number, 20) + this.option('neutral_rate', 'avoid trades if abs(trend_ema) under this float (0 to disable, "auto" for a variable filter)', Number, 0.06) + this.option('oversold_rsi_periods', 'number of periods for oversold RSI', Number, 20) + this.option('oversold_rsi', 'buy when RSI reaches this value', Number, 30) + }, + + calculate: function (s) { + get('lib.ta_ema')(s, 'trend_ema', s.options.trend_ema) + if (s.options.oversold_rsi) { + // sync RSI display with oversold RSI periods + s.options.rsi_periods = s.options.oversold_rsi_periods + get('lib.rsi')(s, 'oversold_rsi', s.options.oversold_rsi_periods) + if (!s.in_preroll && s.period.oversold_rsi <= s.options.oversold_rsi && !s.oversold && !s.cancel_down) { + s.oversold = true + if (s.options.mode !== 'sim' || s.options.verbose) console.log(('\noversold at ' + s.period.oversold_rsi + ' RSI, preparing to buy\n').cyan) + } + } + if (s.period.trend_ema && s.lookback[0] && s.lookback[0].trend_ema) { + s.period.trend_ema_rate = (s.period.trend_ema - s.lookback[0].trend_ema) / s.lookback[0].trend_ema * 100 + } + if (s.options.neutral_rate === 'auto') { + get('lib.stddev')(s, 'trend_ema_stddev', Math.floor(s.options.trend_ema / 2), 'trend_ema_rate') + } + else { + s.period.trend_ema_stddev = s.options.neutral_rate + } + }, + + onPeriod: function (s, cb) { + if (!s.in_preroll && typeof s.period.oversold_rsi === 'number') { + if (s.oversold) { + s.oversold = false + s.trend = 'oversold' + s.signal = 'buy' + s.cancel_down = true + return cb() + } + } + if (typeof s.period.trend_ema_stddev === 'number') { + if (s.period.trend_ema_rate > s.period.trend_ema_stddev) { + if (s.trend !== 'up') { + s.acted_on_trend = false + } + s.trend = 'up' + s.signal = !s.acted_on_trend ? 'buy' : null + s.cancel_down = false + } + else if (!s.cancel_down && s.period.trend_ema_rate < (s.period.trend_ema_stddev * -1)) { + if (s.trend !== 'down') { + s.acted_on_trend = false + } + s.trend = 'down' + s.signal = !s.acted_on_trend ? 'sell' : null + } + } + cb() + }, + + onReport: function (s) { + var cols = [] + if (typeof s.period.trend_ema_stddev === 'number') { + var color = 'grey' + if (s.period.trend_ema_rate > s.period.trend_ema_stddev) { + color = 'green' + } + else if (s.period.trend_ema_rate < (s.period.trend_ema_stddev * -1)) { + color = 'red' + } + cols.push(z(8, n(s.period.trend_ema_rate).format('0.0000'), ' ')[color]) + if (s.period.trend_ema_stddev) { + cols.push(z(8, n(s.period.trend_ema_stddev).format('0.0000'), ' ').grey) + } + } + else { + if (s.period.trend_ema_stddev) { + cols.push(' ') + } + else { + cols.push(' ') + } + } + return cols + } + } +} diff --git a/extensions/strategies/ta_macd/_codemap.js b/extensions/strategies/ta_macd/_codemap.js new file mode 100644 index 0000000000..e1c4ffb8e5 --- /dev/null +++ b/extensions/strategies/ta_macd/_codemap.js @@ -0,0 +1,6 @@ +module.exports = { + _ns: 'zenbot', + + 'strategies.ta_macd': require('./strategy'), + 'strategies.list[]': '#strategies.ta_macd' +} diff --git a/extensions/strategies/ta_macd/strategy.js b/extensions/strategies/ta_macd/strategy.js new file mode 100644 index 0000000000..f2ab7f88de --- /dev/null +++ b/extensions/strategies/ta_macd/strategy.js @@ -0,0 +1,86 @@ +var z = require('zero-fill') + , n = require('numbro') + +module.exports = function container (get, set, clear) { + return { + name: 'ta_macd', + description: 'Buy when (MACD - Signal > 0) and sell when (MACD - Signal < 0).', + + getOptions: function () { + this.option('period', 'period length', String, '1h') + this.option('min_periods', 'min. number of history periods', Number, 52) + this.option('ema_short_period', 'number of periods for the shorter EMA', Number, 12) + this.option('ema_long_period', 'number of periods for the longer EMA', Number, 26) + this.option('signal_period', 'number of periods for the signal EMA', Number, 9) + this.option('up_trend_threshold', 'threshold to trigger a buy signal', Number, 0) + this.option('down_trend_threshold', 'threshold to trigger a sold signal', Number, 0) + this.option('overbought_rsi_periods', 'number of periods for overbought RSI', Number, 25) + this.option('overbought_rsi', 'sold when RSI exceeds this value', Number, 70) + }, + + calculate: function (s) { + if (s.options.overbought_rsi) { + // sync RSI display with overbought RSI periods + s.options.rsi_periods = s.options.overbought_rsi_periods + get('lib.rsi')(s, 'overbought_rsi', s.options.overbought_rsi_periods) + if (!s.in_preroll && s.period.overbought_rsi >= s.options.overbought_rsi && !s.overbought) { + s.overbought = true + if (s.options.mode === 'sim' && s.options.verbose) console.log(('\noverbought at ' + s.period.overbought_rsi + ' RSI, preparing to sold\n').cyan) + } + } + + // compture MACD + /*get('lib.ema')(s, 'ema_short', s.options.ema_short_period) + get('lib.ema')(s, 'ema_long', s.options.ema_long_period) + if (s.period.ema_short && s.period.ema_long) { + s.period.macd = (s.period.ema_short - s.period.ema_long) + get('lib.ema')(s, 'signal', s.options.signal_period, 'macd') + if (s.period.signal) { + s.period.macd_histogram = s.period.macd - s.period.signal + } + }*/ + get('lib.ta_macd')(s,'macd','macd_histogram','macd_signal',s.options.ema_long_period,s.options.ema_short_period,s.options.signal_period) + }, + + onPeriod: function (s, cb) { + if (!s.in_preroll && typeof s.period.overbought_rsi === 'number') { + if (s.overbought) { + s.overbought = false + s.trend = 'overbought' + s.signal = 'sold' + return cb() + } + } + + if (typeof s.period.macd_histogram === 'number' && typeof s.lookback[0].macd_histogram === 'number') { + if ((s.period.macd_histogram - s.options.up_trend_threshold) > 0 && (s.lookback[0].macd_histogram - s.options.up_trend_threshold) <= 0) { + s.signal = 'buy'; + } else if ((s.period.macd_histogram + s.options.down_trend_threshold) < 0 && (s.lookback[0].macd_histogram + s.options.down_trend_threshold) >= 0) { + s.signal = 'sell'; + } else { + s.signal = null; // hold + } + } + cb() + }, + + onReport: function (s) { + var cols = [] + if (typeof s.period.macd_histogram === 'number') { + var color = 'grey' + if (s.period.macd_histogram > 0) { + color = 'green' + } + else if (s.period.macd_histogram < 0) { + color = 'red' + } + cols.push(z(8, n(s.period.macd_histogram).format('+00.0000'), ' ')[color]) + cols.push(z(8, n(s.period.overbought_rsi).format('00'), ' ').cyan) + } + else { + cols.push(' ') + } + return cols + } + } +} diff --git a/extensions/strategies/trust_distrust/_codemap.js b/extensions/strategies/trust_distrust/_codemap.js new file mode 100644 index 0000000000..458193ad30 --- /dev/null +++ b/extensions/strategies/trust_distrust/_codemap.js @@ -0,0 +1,6 @@ +module.exports = { + _ns: 'zenbot', + + 'strategies.trust_distrust': require('./strategy'), + 'strategies.list[]': '#strategies.trust_distrust' +} diff --git a/extensions/strategies/trust_distrust/strategy.js b/extensions/strategies/trust_distrust/strategy.js new file mode 100644 index 0000000000..ff5198b11b --- /dev/null +++ b/extensions/strategies/trust_distrust/strategy.js @@ -0,0 +1,136 @@ +var z = require('zero-fill') + , n = require('numbro') + +module.exports = function container (get, set, clear) { + return { + name: 'trust_distrust', + description: 'Sell when price higher than $sell_min% and highest point - $sell_threshold% is reached. Buy when lowest price point + $buy_threshold% reached.', + + getOptions: function () { + this.option('period', 'period length', String, '30m') + this.option('min_periods', 'min. number of history periods', Number, 52) + this.option('sell_threshold', 'sell when the top drops at least below this percentage', Number, 2) + this.option('sell_threshold_max', 'sell when the top drops lower than this max, regardless of sell_min (panic sell, 0 to disable)', Number, 0) + this.option('sell_min', 'do not act on anything unless the price is this percentage above the original price', Number, 1) + this.option('buy_threshold', 'buy when the bottom increased at least above this percentage', Number, 2) + this.option('buy_threshold_max', 'wait for multiple buy signals before buying (kill whipsaw, 0 to disable)', Number, 0) + this.option('greed', 'sell if we reach this much profit (0 to be greedy and either win or lose)', Number, 0) + }, + + calculate: function (s) { + if (typeof s.trust_distrust_start_greed === 'undefined') { + s.trust_distrust_start_greed = s.period.high + } + if (typeof s.trust_distrust_start === 'undefined') { + s.trust_distrust_start = s.period.high + } + if (typeof s.trust_distrust_highest === 'undefined') { + s.trust_distrust_highest = s.period.high + } + if (typeof s.trust_distrust_lowest === 'undefined') { + s.trust_distrust_lowest = s.period.high + } + if (typeof s.trust_distrust_last_action === 'undefined') { + s.trust_distrust_last_action = null + } + if (typeof s.trust_distrust_buy_threshold_max === 'undefined') { + s.trust_distrust_buy_threshold_max = 0 + } + + // when our current price is higher than what we recorded, overwrite + if (s.period.high > s.trust_distrust_highest) { + s.trust_distrust_highest = s.period.high + } + + // when our current price is lower than what we recorded, overwrite + if (s.trust_distrust_lowest > s.period.high) { + s.trust_distrust_lowest = s.period.high + } + }, + + onPeriod: function (s, cb) { + if (s.greedy) { + s.signal = s.trust_distrust_last_action + return cb() + } + + // sell logic + if (s.trust_distrust_last_action !== 'sell') { + if ( s.period.high > (s.trust_distrust_start + (s.trust_distrust_start / 100 * s.options.sell_min))) { // we are above minimum we want to sell for, or going so low we should "panic sell" + if (s.period.high < (s.trust_distrust_highest - (s.trust_distrust_highest / 100 * s.options.sell_threshold))) { // we lost sell_threshold from highest point + s.signal = 'sell' + + s.trust_distrust_last_action = 'sell' + s.trust_distrust_start = s.period.high + s.trust_distrust_highest = s.period.high + s.trust_distrust_lowest = s.period.high + + return cb() + } + } + + if (s.options.sell_threshold_max > 0 && s.period.high < (s.trust_distrust_highest - (s.trust_distrust_highest / 100 * s.options.sell_threshold_max))) { // we panic sell + s.signal = 'sell' + + s.trust_distrust_last_action = 'sell' + s.trust_distrust_start = s.period.high + s.trust_distrust_highest = s.period.high + s.trust_distrust_lowest = s.period.high + + return cb() + } + } + + if (s.options.greed > 0 && s.period.high > (s.trust_distrust_start_greed + (s.trust_distrust_start_greed / 100 * s.options.greed))) { // we are not greedy, sell if this profit is reached + s.signal = 'sell' + + s.trust_distrust_last_action = 'sell' + s.trust_distrust_start = s.period.high + s.trust_distrust_highest = s.period.high + s.trust_distrust_lowest = s.period.high + s.greedy = true + + return cb() + } + + // buy logic + if (s.trust_distrust_last_action !== 'buy') { + if(s.period.high < s.trust_distrust_start && s.period.high > (s.trust_distrust_lowest + (s.trust_distrust_lowest / 100 * s.options.buy_threshold))) { // we grew above buy threshold from lowest point + if (s.options.buy_threshold_max > 0 && s.trust_distrust_buy_threshold_max < s.options.buy_threshold_max) { + s.trust_distrust_buy_threshold_max++ + return cb() + } + s.trust_distrust_buy_threshold_max = 0 + s.signal = 'buy' + + s.trust_distrust_last_action = 'buy' + s.trust_distrust_start = s.period.high + s.trust_distrust_highest = s.period.high + s.trust_distrust_lowest = s.period.high + + return cb() + } + } + + // repeat last signal + if (s.signal === null) { + s.signal = s.trust_distrust_last_action + } + return cb() + }, + + onReport: function (s) { + var cols = [] + var color = 'grey' + if (s.period.high > s.trust_distrust_start) { + color = 'green' + } + else if (s.period.high < s.trust_distrust_lowest) { + color = 'red' + } + cols.push(z(8, n(s.period.high).format('0.0000'), ' ')[color]) + cols.push(z(8, n(s.trust_distrust_start).format('0.0000'), ' ').grey) + return cols + } + } +} diff --git a/lib/_codemap.js b/lib/_codemap.js index 81a50904b3..d591669916 100644 --- a/lib/_codemap.js +++ b/lib/_codemap.js @@ -2,6 +2,8 @@ module.exports = { _ns: 'zenbot', _folder: 'lib', + 'ta_ema': require('./ta_ema'), + 'ta_macd': require('./ta_macd'), 'cci': require('./cci'), 'ema': require('./ema'), 'engine': require('./engine'), diff --git a/lib/ta_ema.js b/lib/ta_ema.js new file mode 100644 index 0000000000..a53fe237ac --- /dev/null +++ b/lib/ta_ema.js @@ -0,0 +1,51 @@ +var talib = require('talib') + +module.exports = function container (get, set, clear) { + return function ta_ema (s, key, length, source_key) { + //create object for talib. only close is used for now but rest might come in handy + if (!s.marketData) { + s.marketData = { open: [], close: [], high: [], low: [], volume: [] }; + } + if (s.lookback.length > s.marketData.close.length) { + for (var i = (s.lookback.length - s.marketData.close.length) - 1; i >= 0; i--) { + //console.log('add data') + s.marketData.close.push(s.lookback[i].close); + } + } + //dont calculate until we have enough data + if (s.marketData.close.length >= length) { + //fillup marketData for talib. + //this might need improvment for performance. + //for (var i = 0; i < length; i++) { + // s.marketData.close.push(s.lookback[i].close); + //} + //fillup marketData for talib. + var tmpMarket = JSON.parse(JSON.stringify(s.marketData.close)); + //add current period + tmpMarket.push(s.period.close) + + //doublecheck length. + if (tmpMarket.length >= length) { + talib.execute({ + name: "EMA", + startIdx: 0, + endIdx: tmpMarket.length -1, + inReal: tmpMarket, + optInTimePeriod: length + }, function (err, result) { + if (err) { + console.log(err); + return; + } + //Result format: (note: outReal can have multiple items in the array) + // { + // begIndex: 8, + // nbElement: 1, + // result: { outReal: [ 1820.8621111111108 ] } + // } + s.period[key] = result.result.outReal[(result.nbElement - 1)]; + }); + } + } + } +} diff --git a/lib/ta_macd.js b/lib/ta_macd.js new file mode 100644 index 0000000000..73f19e84be --- /dev/null +++ b/lib/ta_macd.js @@ -0,0 +1,56 @@ +var talib = require('talib') + +module.exports = function container (get, set, clear) { + return function macd (s, macd_key,hist_key,sig_key, slow_period,fast_period,signal_period) { + //check parameters + // if (fast_period > slow_period) { + // console.log('incorrect parameters MACD. (fast_period < slow_period || signal_period > fast_period)') + // return; + // } + //create object for talib. only close is used for now but rest might come in handy + if (!s.marketData) { + s.marketData = { open: [], close: [], high: [], low: [], volume: [] }; + } + if (s.lookback.length > s.marketData.close.length) { + for (var i = (s.lookback.length - s.marketData.close.length) - 1; i >= 0; i--) { + //console.log('add data') + s.marketData.close.push(s.lookback[i].close); + } + } + var periods_necessary = slow_period + signal_period - 1 + //dont calculate until we have enough data + if (s.marketData.close.length >= periods_necessary) { + //fillup marketData for talib. + var tmpMarket = JSON.parse(JSON.stringify(s.marketData.close)); + //add current period + tmpMarket.push(s.period.close) + + talib.execute({ + name: "MACD", + startIdx: 0, + endIdx: tmpMarket.length -1, + inReal: tmpMarket, + optInFastPeriod: fast_period, + optInSlowPeriod: slow_period, + optInSignalPeriod: signal_period + }, function (err, result) { + if (err) { + console.log(err); + return; + } + //Result format: (note: outReal can have multiple items in the array) + // { + // begIndex: 8, + // nbElement: 1, + // result: { outReal: [ 1820.8621111111108 ] } + // } + // console.log(JSON.stringify(marketData)) + // console.log(JSON.stringify(result.result)) + s.period[macd_key] = result.result.outMACD[(result.nbElement - 1)]; + s.period[hist_key] = result.result.outMACDHist[(result.nbElement - 1)]; + s.period[sig_key] = result.result.outMACDSignal[(result.nbElement - 1)]; + }); + + } + } +} diff --git a/package-lock.json b/package-lock.json index 89a220b84a..067fe982c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -620,9 +620,9 @@ "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=" }, "iconv-lite": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.17.tgz", - "integrity": "sha1-T9qjs4rLwsAxsEXQ7c3+HsqxjI0=", + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", + "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==", "dev": true }, "idgen": { @@ -1527,6 +1527,11 @@ } } }, + "talib": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/talib/-/talib-1.0.3.tgz", + "integrity": "sha1-6wp+P1MBsyOwrwL7M41DHSubc1o=" + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index 586fd04544..dec673b2e1 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "semver": "^5.3.0", "sosa_mongo": "^1.0.3", "timebucket": "^0.4.0", + "talib": "^1.0.3", "zero-fill": "^2.2.3" }, "devDependencies": { diff --git a/scripts/auto_backtester/backtester_trust_distrust.js b/scripts/auto_backtester/backtester_trust_distrust.js new file mode 100644 index 0000000000..267901dbae --- /dev/null +++ b/scripts/auto_backtester/backtester_trust_distrust.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node + +/* Zenbot 4.04 Backtester v0.2 + * Ali Anari + * 05/30/2017 + * + * Usage: Pass in the same parameters as you would to "zenbot sim", EXCEPT for: + * EMA Parameters: "trend_ema", "neutral_rate" + * RSI Parameters: "oversold_rsi", "oversold_rsi_periods" + * + * Example: ./backtester.js gdax.ETH-USD --days=10 --currency_capital=5 --period=1m +*/ + +let shell = require('shelljs'); +let parallel = require('run-parallel-limit'); +let json2csv = require('json2csv'); +let roundp = require('round-precision'); +let fs = require('fs'); + +let VERSION = 'Zenbot 4.04 Backtester v0.2'; + +let PARALLEL_LIMIT = require('os').cpus().length; + +let SELL_THRESHOLD_MIN = 0; +let SELL_THRESHOLD_MAX = 10; + +let SELL_THRESHOLD_MAX_MIN = 0; +let SELL_THRESHOLD_MAX_MAX = 10; + +let BUY_THRESHOLD_MIN = 0; +let BUY_THRESHOLD_MAX = 10; + +let SELL_MIN_MIN = 0; +let SELL_MIN_MAX = 10; + +let PERIOD_MIN = 27; +let PERIOD_MAX = 27; + +let countArr = []; + +let range = (start, end) => { + return Array(end - start + 1).fill().map((_, idx) => start + idx) +}; + +let product = args => { + if(!args.length) + return [[]]; + var prod = product(args.slice(1)), r = []; + args[0].forEach(function(x) { + prod.forEach(function(p) { + r.push([x].concat(p)); + }); + }); + return r; +}; + +let objectProduct = obj => { + var keys = Object.keys(obj), + values = keys.map(function(x) { return obj[x] }); + + return product(values).map(function(p) { + var e = {}; + keys.forEach(function(k, n) { e[k] = p[n] }); + return e; + }); +}; + +let runCommand = (strategy, cb) => { + countArr.push(1); + let command = `./zenbot.sh sim ${simArgs} --strategy=trust_distrust --period=${strategy.period}m --sell_threshold=${strategy.sell_threshold} --sell_threshold_max=${strategy.sell_threshold_max} --sell_min=${strategy.sell_min} --buy_threshold=${strategy.buy_threshold} --days=30`; + console.log(`[ ${countArr.length}/${strategies.length} ] ${command}`); + + shell.exec(command, {silent:true, async:true}, (code, stdout, stderr) => { + if (code) { + console.error(command) + console.error(stderr) + return cb(null, null) + } + cb(null, processOutput(stdout)); + }); +}; + +let processOutput = output => { + let jsonRegexp = /(\{[\s\S]*?\})\send balance/g; + let endBalRegexp = /end balance: (\d+\.\d+) \(/g; + let wlRegexp = /win\/loss: (\d+)\/(\d+)/g; + let errRegexp = /error rate: (.*)%/g; + + let output2 = output.substr(output.length - 3000); + + let rawParams = jsonRegexp.exec(output2)[1]; + let params = JSON.parse(rawParams); + let endBalance = endBalRegexp.exec(output2)[1]; + let wlMatch = wlRegexp.exec(output2); + let wins = parseInt(wlMatch[1]); + let losses = parseInt(wlMatch[2]); + let errorRate = errRegexp.exec(output2)[1]; + let days = parseInt(params.days); + + let roi = roundp( + ((endBalance - params.currency_capital) / params.currency_capital) * 100, + 3 + ); + + return { + params: rawParams.replace(/[\r\n]/g, ''), + endBalance: parseFloat(endBalance), + wins: wins, + losses: losses, + errorRate: parseFloat(errorRate), + sellThreshold: params.sell_threshold, + sellThresholdMax: params.sell_threshold_max, + sellMin: params.sell_min, + buyThreshold: params.buy_threshold, + days: days, + period: params.period, + roi: roi, + wlRatio: losses > 0 ? roundp(wins / losses, 3) : 'Infinity', + frequency: roundp((wins + losses) / days, 3) + }; +}; + +let strategies = objectProduct({ + sell_threshold: range(SELL_THRESHOLD_MIN, SELL_THRESHOLD_MAX), + sell_threshold_max: range(SELL_THRESHOLD_MAX_MIN, SELL_THRESHOLD_MAX_MAX), + sell_min: range(SELL_MIN_MIN, SELL_MIN_MAX), + buy_threshold: range(BUY_THRESHOLD_MIN, BUY_THRESHOLD_MAX), + period: range(PERIOD_MIN, PERIOD_MAX) +}); + +let tasks = strategies.map(strategy => { + return cb => { + runCommand(strategy, cb) + } +}); + +let args = process.argv; +args.shift(); +args.shift(); +let simArgs = args.join(' '); + +console.log(`\n--==${VERSION}==--`); +console.log(new Date().toUTCString()); +console.log(`\nBacktesting [${strategies.length}] iterations...\n`); + +parallel(tasks, PARALLEL_LIMIT, (err, results) => { + console.log("\nBacktesting complete, saving results..."); + results = results.filter(function (r) { + return !!r + }) + results.sort((a,b) => (a.roi < b.roi) ? 1 : ((b.roi < a.roi) ? -1 : 0)); + let fileName = `backtesting_${Math.round(+new Date()/1000)}.csv`; + let csv = json2csv({ + data: results, + fields: ['roi', 'errorRate', 'wlRatio', 'frequency', 'endBalance', 'wins', 'losses', 'period', 'days', 'sellThreshold', 'sellThresholdMax', 'sellMin', 'buyThreshold', 'params'], + fieldNames: ['ROI (%)', 'Error Rate (%)', 'Win/Loss Ratio', '# Trades/Day', 'Ending Balance ($)', '# Wins', '# Losses', 'Period', '# Days', 'Sell Threshold', 'Sell Threshold Max', 'Sell Min', 'Buy Threshold', 'Full Parameters'] + }); + + fs.writeFile(fileName, csv, err => { + if (err) throw err; + console.log(`\nResults successfully saved to ${fileName}!\n`); + }); +});