diff --git a/.gitignore b/.gitignore index 223ebccc65..77f44f5f2a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ conf_* sim_result* *_test backtesting_* +models/*.json +models/*.html diff --git a/commands/_codemap.js b/commands/_codemap.js index 2f16f627d5..16ec536bf0 100644 --- a/commands/_codemap.js +++ b/commands/_codemap.js @@ -6,6 +6,7 @@ module.exports = { 'list-strategies': require('./list-strategies'), 'backfill': require('./backfill'), 'sim': require('./sim'), + 'train': require('./train'), 'balance': require('./balance'), 'trade': require('./trade'), 'buy': require('./buy'), @@ -15,6 +16,7 @@ module.exports = { 'list[30]': '#commands.list-strategies', 'list[50]': '#commands.backfill', 'list[60]': '#commands.sim', + 'list[62]': '#commands.train', 'list[65]': '#commands.balance', 'list[70]': '#commands.trade', 'list[80]': '#commands.buy', diff --git a/commands/sim.js b/commands/sim.js index 0de558004a..9a08d3a3bd 100644 --- a/commands/sim.js +++ b/commands/sim.js @@ -16,7 +16,7 @@ module.exports = function container (get, set, clear) { .option('--conf ', 'path to optional conf overrides file') .option('--strategy ', 'strategy to use', String, c.strategy) .option('--order_type ', 'order type to use (maker/taker)', /^(maker|taker)$/i, c.order_type) - .option('--filename ', 'filename for the result output (ex: result.html)', String, c.filename) + .option('--filename ', 'filename for the result output (ex: result.html). "none" to disable', String, c.filename) .option('--start ', 'start at timestamp') .option('--end ', 'end at timestamp') .option('--days ', 'set duration by day count', Number, c.days) @@ -35,6 +35,7 @@ module.exports = function container (get, set, clear) { .option('--max_slippage_pct ', 'avoid selling at a slippage pct above this float', c.max_slippage_pct) .option('--symmetrical', 'reverse time at the end of the graph, normalizing buy/hold to 0', Boolean, c.symmetrical) .option('--rsi_periods ', 'number of periods to calculate RSI at', Number, c.rsi_periods) + .option('--disable_options', 'disable printing of options') .option('--enable_stats', 'enable printing order stats') .option('--verbose', 'print status lines on every period') .action(function (selector, cmd) { @@ -63,6 +64,7 @@ module.exports = function container (get, set, clear) { so.start = d.subtract(so.days).toMilliseconds() } so.stats = !!cmd.enable_stats + so.show_options = !cmd.disable_options so.verbose = !!cmd.verbose so.selector = get('lib.normalize-selector')(selector || c.selector) so.mode = 'sim' @@ -93,8 +95,10 @@ module.exports = function container (get, set, clear) { option_keys.forEach(function (k) { options[k] = so[k] }) - var options_json = JSON.stringify(options, null, 2) - output_lines.push(options_json) + if (so.show_options) { + var options_json = JSON.stringify(options, null, 2) + output_lines.push(options_json) + } if (s.my_trades.length) { s.my_trades.push({ price: s.period.close, @@ -159,9 +163,11 @@ module.exports = function container (get, set, clear) { .replace('{{output}}', html_output) .replace(/\{\{symbol\}\}/g, so.selector + ' - zenbot ' + require('../package.json').version) - var out_target = so.filename || 'simulations/sim_result_' + so.selector +'_' + new Date().toISOString().replace(/T/, '_').replace(/\..+/, '').replace(/-/g, '').replace(/:/g, '').replace(/20/, '') + '_UTC.html' - fs.writeFileSync(out_target, out) - console.log('wrote', out_target) + if (so.filename !== 'none') { + var out_target = so.filename || 'simulations/sim_result_' + so.selector +'_' + new Date().toISOString().replace(/T/, '_').replace(/\..+/, '').replace(/-/g, '').replace(/:/g, '').replace(/20/, '') + '_UTC.html' + fs.writeFileSync(out_target, out) + console.log('wrote', out_target) + } process.exit(0) } diff --git a/commands/train.js b/commands/train.js new file mode 100644 index 0000000000..69e109ae9f --- /dev/null +++ b/commands/train.js @@ -0,0 +1,411 @@ +var tb = require('timebucket') + , minimist = require('minimist') + , n = require('numbro') + , fs = require('fs') + , path = require('path') + , spawn = require('child_process').spawn + , moment = require('moment') + , colors = require('colors') + , analytics = require('forex.analytics') + , ProgressBar = require('progress') + , crypto = require('crypto') + +var fa_defaultIndicators = [ + 'CCI', + 'MACD', + 'RSI', + 'SAR', + 'Stochastic' +] + +var fa_availableIndicators = [ + 'ATR', + 'BOP', + 'CCI', + 'MACD', + 'MACD_Signal', + 'MACD_Histogram', + 'Momentum', + 'RSI', + 'SAR', + 'SMA15_SMA50', + 'Stochastic' +] + + +function fa_getTrainOptions (so) { + if (typeof(so) === "undefined") so = {} + + return { + populationCount: so.populationCount || 100, + generationCount: so.generationCount || 100, + selectionAmount: so.selectionAmount || 10, + leafValueMutationProbability: so.leafValueMutationProbability || 0.5, + leafSignMutationProbability: so.leafSignMutationProbability || 0.3, + logicalNodeMutationProbability: so.logicalNodeMutationProbability || 0.3, + leafIndicatorMutationProbability: so.leafIndicatorMutationProbability || 0.2, + crossoverProbability: so.crossoverProbability || 0.03, + indicators: so.indicators ? so.indicators.split(',') : fa_defaultIndicators + } +} + +module.exports = function container (get, set, clear) { + var c = get('conf') + + return function (program) { + program + .command('train [selector]') + .allowUnknownOption() + .description('Train the binary buy/sell decision tree for the forex.analytics strategy') + .option('--conf ', 'path to optional conf overrides file') + .option('--period ', 'period length of a candlestick (default: 30m)', String, '30m') + .option('--start_training ', 'start training at timestamp') + .option('--end_training ', 'end training at timestamp') + .option('--days_training ', 'set duration of training dataset by day count', Number, c.days) + .option('--days_test ', 'set duration of test dataset to use with simulation, appended AFTER the training dataset (default: 0)', Number) + .option('--populationCount ', 'population count (default: ' + fa_getTrainOptions().populationCount + ')', Number) + .option('--generationCount ', 'generation count (default: ' + fa_getTrainOptions().generationCount + ')', Number) + .option('--selectionAmount ', 'selection amount (default: ' + fa_getTrainOptions().selectionAmount + ')', Number) + .option('--leafValueMutationProbability ', 'leaf value mutation probability (default: ' + fa_getTrainOptions().leafValueMutationProbability + ')', Number) + .option('--leafSignMutationProbability ', 'leaf sign mutation probability (default: ' + fa_getTrainOptions().leafSignMutationProbability + ')', Number) + .option('--logicalNodeMutationProbability ', 'logical node mutation probability (default: ' + fa_getTrainOptions().logicalNodeMutationProbability + ')', Number) + .option('--leafIndicatorMutationProbability ', 'leaf indicator mutation probability (default: ' + fa_getTrainOptions().leafIndicatorMutationProbability + ')', Number) + .option('--crossoverProbability ', 'crossover probability (default: ' + fa_getTrainOptions().crossoverProbability + ')', Number) + .option('--indicators ', 'comma separated list of TA-lib indicators (default: ' + fa_defaultIndicators.toString() + ', available: ' + fa_availableIndicators.toString() + ')', String) + + .action(function (selector, cmd) { + var s = {options: minimist(process.argv)} + var so = s.options + delete so._ + Object.keys(c).forEach(function (k) { + if (typeof cmd[k] !== 'undefined') { + so[k] = cmd[k] + } + }) + + if (!so.days_test) { so.days_test = 0 } + so.strategy = 'noop' + + unknownIndicators = [] + if (so.indicators) { + so.indicators.split(',').forEach(function(indicator) { + if (!fa_availableIndicators.includes(indicator)) + unknownIndicators.push(indicator) + }) + } + if (unknownIndicators) { + console.error(('ERROR: The following indicators are not in forex.analytics: ').red + (unknownIndicators.toString()).yellow) + console.error('Available indicators: ' + fa_availableIndicators.toString()) + process.exit(1) + } + + if (so.start_training) { + so.start_training = moment(so.start_training).valueOf() + if (so.days_training && !so.end_training) { + so.end_training = tb(so.start_training).resize('1d').add(so.days_training).toMilliseconds() + } + } + if (so.end_training) { + so.end_training = moment(so.end_training).valueOf() + if (so.days_training && !so.start_training) { + so.start_training = tb(so.end_training).resize('1d').subtract(so.days_training).toMilliseconds() + } + } + if (!so.start_training && so.days_training) { + var d = tb('1d') + so.start_training = d.subtract(so.days_test).subtract(so.days_training).toMilliseconds() + } + if (so.days_test > 0) { + var d = tb('1d') + so.end_training = d.subtract(so.days_test).toMilliseconds() + } + so.selector = get('lib.normalize-selector')(selector || c.selector) + so.mode = 'train' + if (cmd.conf) { + var overrides = require(path.resolve(process.cwd(), cmd.conf)) + Object.keys(overrides).forEach(function (k) { + so[k] = overrides[k] + }) + } + var engine = get('lib.engine')(s) + + if (!so.min_periods) so.min_periods = 1 + var cursor, reversing, reverse_point + var query_start = so.start_training ? tb(so.start_training).resize(so.period).subtract(so.min_periods + 2).toMilliseconds() : null + + function writeTempModel (strategy) { + var tempModelString = JSON.stringify( + { + "selector": so.selector, + "period": so.period, + "start_training": moment(so.start_training), + "end_training": moment(so.end_training), + "options": fa_getTrainOptions(so), + "strategy": strategy + }, null, 4) + + var tempModelHash = crypto.createHash('sha256').update(tempModelString).digest('hex') + var tempModelFile = 'models/temp.' + tempModelHash + '-' + moment(so.start_training).utc().format('YYYYMMDD_HHmmssZZ') + '.json'; + + fs.writeFileSync( + tempModelFile, + tempModelString + ) + + return tempModelFile + } + + function writeFinalModel (strategy, end_training, trainingResult, testResult) { + var finalModelString = JSON.stringify( + { + "selector": so.selector, + "period": so.period, + "start_training": moment(so.start_training).utc(), + "end_training": moment(end_training).utc(), + "result_training": trainingResult, + "start_test": so.days_test > 0 ? moment(end_training).utc() : undefined, + "result_test": testResult, + "options": fa_getTrainOptions(so), + "strategy": strategy + }, null, 4) + + var testVsBuyHold = typeof(testResult) !== "undefined" ? testResult.vsBuyHold : 'noTest' + + var finalModelFile = 'models/forex.model_' + so.selector + + '_period=' + so.period + + '_from=' + moment(so.start_training).utc().format('YYYYMMDD_HHmmssZZ') + + '_to=' + moment(end_training).utc().format('YYYYMMDD_HHmmssZZ') + + '_trainingVsBuyHold=' + trainingResult.vsBuyHold + + '_testVsBuyHold=' + testVsBuyHold + + '_created=' + moment().utc().format('YYYYMMDD_HHmmssZZ') + + '.json' + + fs.writeFileSync( + finalModelFile, + finalModelString + ) + + return finalModelFile + } + + function parseSimulation (simulationResultFile) { + var endBalance = new RegExp(/end balance: .* \((.*)%\)/) + var buyHold = new RegExp(/buy hold: .* \((.*)%\)/) + var vsBuyHold = new RegExp(/vs\. buy hold: (.*)%/) + var trades = new RegExp(/([0-9].* trades over .* days \(avg (.*) trades\/day\))/) + var errorRate = new RegExp(/error rate: (.*)%/) + + var simulationResult = fs.readFileSync(simulationResultFile).toString() + simulationResult = simulationResult.substr(simulationResult.length - 512); + + result = {} + if (simulationResult.match(endBalance)) { result.endBalance = simulationResult.match(endBalance)[1] } + if (simulationResult.match(buyHold)) { result.buyHold = simulationResult.match(buyHold)[1] } + if (simulationResult.match(vsBuyHold)) { result.vsBuyHold = simulationResult.match(vsBuyHold)[1] } + if (simulationResult.match(trades)) { + result.trades = simulationResult.match(trades)[1] + result.avgTradesPerDay = simulationResult.match(trades)[2] + } + if (simulationResult.match(errorRate)) { result.errorRate = simulationResult.match(errorRate)[1] } + + return result + } + + function trainingDone (strategy, lastPeriod) { + var tempModelFile = writeTempModel(strategy) + console.log("\nModel temporarily written to " + tempModelFile) + + if (typeof(so.end_training) === 'undefined') { + so.end_training = lastPeriod.time * 1000 + } + + console.log( + "\nRunning simulation on training data from " + + moment(so.start_training).format('YYYY-MM-DD HH:mm:ss ZZ') + ' to ' + + moment(so.end_training).format('YYYY-MM-DD HH:mm:ss ZZ') + ".\n" + ) + + var zenbot_cmd = process.platform === 'win32' ? 'zenbot.bat' : 'zenbot.sh'; // Use 'win32' for 64 bit windows too + var trainingArgs = [ + 'sim', + so.selector, + '--strategy', 'forex_analytics', + '--disable_options', + '--modelfile', path.resolve(__dirname, '..', tempModelFile), + '--start', so.start_training, + '--end', so.end_training, + '--period', so.period, + '--filename', path.resolve(__dirname, '..', tempModelFile) + '-simTrainingResult.html' + ] + var trainingSimulation = spawn(path.resolve(__dirname, '..', zenbot_cmd), trainingArgs, { stdio: 'inherit' }) + + trainingSimulation.on('exit', function (code, signal) { + if (code) { + console.log('Child process exited with code ' + code + ' and signal ' + signal) + process.exit(code) + } + + var trainingResult = parseSimulation(path.resolve(__dirname, '..', tempModelFile) + '-simTrainingResult.html') + + if (so.days_test > 0) { + console.log( + "\nRunning simulation on test data from " + + moment(so.end_training).format('YYYY-MM-DD HH:mm:ss ZZ') + " onwards.\n" + ) + + var testArgs = [ + 'sim', + so.selector, + '--strategy', 'forex_analytics', + '--disable_options', + '--modelfile', path.resolve(__dirname, '..', tempModelFile), + '--start', so.end_training, + '--period', so.period, + '--filename', path.resolve(__dirname, '..', tempModelFile) + '-simTestResult.html', + ] + var testSimulation = spawn(path.resolve(__dirname, '..', zenbot_cmd), testArgs, { stdio: 'inherit' }) + + testSimulation.on('exit', function (code, signal) { + if (code) { + console.log('Child process exited with code ' + code + ' and signal ' + signal) + } + + var testResult = parseSimulation(path.resolve(__dirname, '..', tempModelFile) + '-simTestResult.html') + + var finalModelFile = writeFinalModel(strategy, so.end_training, trainingResult, testResult) + fs.rename(path.resolve(__dirname, '..', tempModelFile) + '-simTrainingResult.html', path.resolve(__dirname, '..', finalModelFile) + '-simTrainingResult.html') + fs.rename(path.resolve(__dirname, '..', tempModelFile) + '-simTestResult.html', path.resolve(__dirname, '..', finalModelFile) + '-simTestResult.html') + fs.unlink(path.resolve(__dirname, '..', tempModelFile)) + console.log("\nFinal model with results written to " + finalModelFile) + + process.exit(0) + }) + } else { + var finalModelFile = writeFinalModel(strategy, so.end_training, trainingResult, undefined) + fs.rename(path.resolve(__dirname, '..', tempModelFile) + '-simTrainingResult.html', path.resolve(__dirname, '..', finalModelFile) + '-simTrainingResult.html') + fs.unlink(path.resolve(__dirname, '..', tempModelFile)) + console.log("\nFinal model with results written to " + finalModelFile) + + process.exit(0) + } + }) + } + + function createStrategy (candlesticks) { + var bar = new ProgressBar( + 'Training [:bar] :percent :etas - Fitness: :fitness', + { + width: 80, + total: fa_getTrainOptions(so).generationCount, + incomplete: ' ' + } + ) + + return analytics.findStrategy(candlesticks, fa_getTrainOptions(so), function(strategy, fitness, generation) { + bar.tick({ + 'fitness': fitness + }) + }) + } + + function createCandlesticks () { + console.log() + + if (!s.period) { + console.error('no trades found! try running `zenbot backfill ' + so.selector + '` first') + process.exit(1) + } + + var option_keys = Object.keys(so) + var output_lines = [] + option_keys.sort(function (a, b) { + if (a < b) return -1 + return 1 + }) + var options = {} + option_keys.forEach(function (k) { + options[k] = so[k] + }) + + var candlesticks = [] + s.lookback.unshift(s.period) + s.lookback.slice(0, s.lookback.length - so.min_periods).map(function (period) { + var candlestick = { + open: period.open, + high: period.high, + low: period.low, + close: period.close, + time: period.time / 1000 + } + candlesticks.unshift(candlestick) + }) + + createStrategy(candlesticks) + .then(function(strategy) { + trainingDone(strategy, candlesticks[candlesticks.length - 1]) + }) + .catch(function(err) { + console.log(('Training error. Aborting.').red) + console.log(err) + process.exit(1) + }) + } + + function getTrades () { + var opts = { + query: { + selector: so.selector + }, + sort: {time: 1}, + limit: 1000 + } + if (so.end_training) { + opts.query.time = {$lte: so.end_training} + } + if (cursor) { + if (reversing) { + opts.query.time = {} + opts.query.time['$lt'] = cursor + if (query_start) { + opts.query.time['$gte'] = query_start + } + opts.sort = {time: -1} + } + else { + if (!opts.query.time) opts.query.time = {} + opts.query.time['$gt'] = cursor + } + } + else if (query_start) { + if (!opts.query.time) opts.query.time = {} + opts.query.time['$gte'] = query_start + } + get('db.trades').select(opts, function (err, trades) { + if (err) throw err + if (!trades.length) { + if (so.symmetrical && !reversing) { + reversing = true + reverse_point = cursor + return getTrades() + } + return createCandlesticks() + } + if (so.symmetrical && reversing) { + trades.forEach(function (trade) { + trade.orig_time = trade.time + trade.time = reverse_point + (reverse_point - trade.time) + }) + } + engine.update(trades, function (err) { + if (err) throw err + cursor = trades[trades.length - 1].time + setImmediate(getTrades) + }) + }) + } + + console.log('Generating training candlesticks from database...') + getTrades() + }) + } +} diff --git a/docs/strategy_forex_analytics.md b/docs/strategy_forex_analytics.md new file mode 100644 index 0000000000..a32666128c --- /dev/null +++ b/docs/strategy_forex_analytics.md @@ -0,0 +1,68 @@ +# Strategy: forex_analytics + +The forex_analytics is based on [forex.analytics by mkmarek](https://github.com/mkmarek/forex.analytics): + +> The result of technical analysis are two binary trees describing strategies for buy and sell signals which produced profit in a certain period of time specified by the input OHLC data set [i.e. candlesticks]. + +In a nutshell, the genetic optimization algorithm used by forex.analytics permutates thresholds used for a limited set of TA-Lib indicators to build a decision tree of different parameters and thresholds, which then result in either a `buy` or `sell` signal. In the end, different combinations of parameters and thresholds are used for either a `buy` or `sell` decision. + +Please also refer to [https://github.com/mkmarek/forex.analytics/blob/master/README.md](https://github.com/mkmarek/forex.analytics/blob/master/README.md). + +## Training +The strategy comes with a new `train` command to train a model on backfilled data. It is recommended to train the model first on a large _training dataset_ and then test the model on previously unseen data to evaluate performance and potential overfitting. This can be done by the `train` command, e.g. + +`./zenbot.sh train bitfinex.ETH-USD --days_training 42 --days_test 14` + +You can specify the following parameters for training: + +``` +--conf path to optional conf overrides file +--period period length of a candlestick (default: 30m) +--start_training start training at timestamp +--end_training end training at timestamp +--days_training set duration of training dataset by day count +--days_test set duration of test dataset to use with simulation, appended AFTER the training dataset (default: 0) +--populationCount population count (default: 100) +--generationCount generation count (default: 100) +--selectionAmount selection amount (default: 10) +--leafValueMutationProbability leaf value mutation probability (default: 0.5) +--leafSignMutationProbability leaf sign mutation probability (default: 0.3) +--logicalNodeMutationProbability logical node mutation probability (default: 0.3) +--leafIndicatorMutationProbability leaf indicator mutation probability (default: 0.2) +--crossoverProbability crossover probability (default: 0.03) +--indicators comma separated list of TA-lib indicators (default: CCI,MACD,RSI,SAR,Stochastic, available: ATR,BOP,CCI,MACD,MACD_Signal,MACD_Histogram,Momentum,RSI,SAR,SMA15_SMA50,Stochastic) +-h, --help output usage information +``` + +You'll have to play around with the different settings for the genetic optimization algorithm, because we're also still in the process of learning them. + +The `train` command first generates the raw candlestick data, performs the training, saves the model to a temporary file and runs the simulations on the training and test dataset. + +You should expect that the _training dataset_ shows extremely good results, since the model has been tailored to it. The results from the _test dataset_ therefore show a more realistic picture. **Do choose a large enough _test dataset_ using `--days_test`.** + +The model, along with the HTML simulation results of both _training_ and _test dataset_, are then saved to the `models/` folder. + +## Trading + +You can use the trained model for training using the normal `trade` command: + +`./zenbot.sh trade bitfinex.ETH-USD --strategy forex_analytics --modelfile models/trained_model_file.json --paper` + +There are not many options to specify. Usually, you only need to adapt `--modelfile` and `--period`. The period must be the same as in training/testing and zenbot will complain if it is not. + +Please note that you can share the model JSON files. zenbot is Open Source, we do share. When you find the holy model grail, please give back to the community ;) + +``` +forex_analytics + description: + Apply the trained forex analytics model. + options: + --modelfile= modelfile (generated by running `train`), should be in models/ (default: none) + --period= period length of a candlestick (default: 30m) (default: 30m) + --min_periods= min. number of history periods (default: 100) +``` + +## Limitations + +- Please be aware that you're fitting the model to retrospective data of a single exchange. There might be (a lot of) overfitting. As discussed, please use a large enough _test dataset_. +- The original forex.anayltics does not optimize for the length of the lookback periods for the TA-Lib indicators. This still needs work. We [filed an issue](https://github.com/mkmarek/forex.analytics/issues/11), but the project seems to be idle - a pull request would be highly appreciated. diff --git a/extensions/strategies/forex_analytics/_codemap.js b/extensions/strategies/forex_analytics/_codemap.js new file mode 100644 index 0000000000..579f20d919 --- /dev/null +++ b/extensions/strategies/forex_analytics/_codemap.js @@ -0,0 +1,6 @@ +module.exports = { + _ns: 'zenbot', + + 'strategies.forex_analytics': require('./strategy'), + 'strategies.list[]': '#strategies.forex_analytics' +} \ No newline at end of file diff --git a/extensions/strategies/forex_analytics/strategy.js b/extensions/strategies/forex_analytics/strategy.js new file mode 100644 index 0000000000..520835c2af --- /dev/null +++ b/extensions/strategies/forex_analytics/strategy.js @@ -0,0 +1,87 @@ +var z = require('zero-fill') + , n = require('numbro') + , fs = require('fs') + , path = require('path') + , analytics = require('forex.analytics') + +module.exports = function container (get, set, clear) { + return { + name: 'forex_analytics', + description: 'Apply the trained forex analytics model.', + + getOptions: function (s) { + this.option('modelfile', 'modelfile (generated by running `train`), should be in models/', String, 'none') + this.option('period', 'period length of a candlestick (default: 30m)', String, '30m') + this.option('min_periods', 'min. number of history periods', Number, 100) + + if (s.options) { + if (!s.options.modelfile) { + console.error('No modelfile specified. Please train a model and specify the resulting file.') + process.exit(1) + } + + if (path.isAbsolute(s.options.modelfile)) { + modelfile = s.options.modelfile + } else { + modelfile = path.resolve(__dirname, '../../../', s.options.modelfile) + } + + if (fs.existsSync(modelfile)) { + model = require(modelfile) + } else { + console.error('Modelfile ' + modelfile + ' does not exist.') + process.exit(1) + } + + if (s.options.period !== model.period) { + console.error(('Error: Period in model training was ' + model.period + ', now you specified ' + s.options.period + '.').red) + process.exit(1) + } + } + }, + + calculate: function (s) { + // Calculations only done at the end of each period + }, + + onPeriod: function (s, cb) { + if (s.lookback.length > s.options.min_periods) { + var candlesticks = [] + + var candlestick = { + open: s.period.open, + high: s.period.high, + low: s.period.low, + close: s.period.close, + time: s.period.time / 1000 + } + candlesticks.unshift(candlestick) + + s.lookback.slice(0, s.lookback.length).map(function (period) { + var candlestick = { + open: period.open, + high: period.high, + low: period.low, + close: period.close, + time: period.time / 1000 + } + candlesticks.unshift(candlestick) + }) + + var result = analytics.getMarketStatus(candlesticks, {"strategy": model.strategy}) + if (result.shouldSell) { + s.signal = "sell" + } else if (result.shouldBuy) { + s.signal = "buy" + } + } + + cb() + }, + + onReport: function (s) { + var cols = [] + return cols + } + } +} \ No newline at end of file diff --git a/extensions/strategies/noop/_codemap.js b/extensions/strategies/noop/_codemap.js new file mode 100644 index 0000000000..74979297a3 --- /dev/null +++ b/extensions/strategies/noop/_codemap.js @@ -0,0 +1,5 @@ +module.exports = { + _ns: 'zenbot', + + 'strategies.noop': require('./strategy') +} \ No newline at end of file diff --git a/extensions/strategies/noop/strategy.js b/extensions/strategies/noop/strategy.js new file mode 100644 index 0000000000..ea4ef4debe --- /dev/null +++ b/extensions/strategies/noop/strategy.js @@ -0,0 +1,22 @@ +module.exports = function container (get, set, clear) { + return { + name: 'noop', + description: 'Just do nothing. Can be used to e.g. generate candlesticks for training the genetic forex strategy.', + + getOptions: function () { + this.option('period', 'period length', String, '30m') + }, + + calculate: function (s) { + }, + + onPeriod: function (s, cb) { + cb() + }, + + onReport: function (s) { + var cols = [] + return cols + } + } +} diff --git a/lib/engine.js b/lib/engine.js index 1ac7b3587c..0d17a9c38e 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -64,7 +64,7 @@ module.exports = function container (get, set, clear) { if (so.strategy) { s.strategy = get('strategies.' + so.strategy) if (s.strategy.getOptions) { - s.strategy.getOptions.call(s.ctx) + s.strategy.getOptions.call(s.ctx, s) } } @@ -332,7 +332,7 @@ module.exports = function container (get, set, clear) { } function getQuote (cb) { - if (so.mode === 'sim') { + if (so.mode === 'sim' || so.mode === 'train') { return cb(null, { bid: s.period.close, ask: s.period.close @@ -626,7 +626,7 @@ module.exports = function container (get, set, clear) { } function writeReport (is_progress, blink_off) { - if (so.mode === 'sim' && !so.verbose) { + if ((so.mode === 'sim' || so.mode === 'train') && !so.verbose) { is_progress = true } else if (is_progress && typeof blink_off === 'undefined' && s.vol_since_last_blink) { diff --git a/models/README.md b/models/README.md new file mode 100644 index 0000000000..7ad571bc10 --- /dev/null +++ b/models/README.md @@ -0,0 +1,4 @@ + +## Models + +Trained models will be placed here! diff --git a/package-lock.json b/package-lock.json index cb54331afd..89a220b84a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -457,6 +457,9 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, + "forex.analytics": { + "version": "git+https://github.com/mkmarek/forex.analytics.git#7bc278987700d4204e959af17de61495941d1a14" + }, "form-data": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", @@ -999,6 +1002,11 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -1256,8 +1264,7 @@ "progress": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", - "dev": true + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=" }, "propagate": { "version": "0.3.1", diff --git a/package.json b/package.json index a6992a314f..a952e69b61 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "codemap": "^1.3.1", "colors": "^1.1.2", "commander": "^2.9.0", + "forex.analytics": "git+https://github.com/mkmarek/forex.analytics.git#7bc278987700d4204e959af17de61495941d1a14", "gdax": "^0.4.2", "glob": "^7.1.1", "har-validator": "^5.0.3", @@ -28,6 +29,7 @@ "number-abbreviate": "^2.0.0", "numbro": "git+https://github.com/carlos8f/numbro.git", "poloniex.js": "0.0.7", + "progress": "^2.0.0", "pusher-js": "^4.1.0", "quadrigacx": "0.0.7", "run-parallel": "^1.1.6",