diff --git a/extensions/strategies/kc/strategy.js b/extensions/strategies/kc/strategy.js new file mode 100644 index 0000000000..5cb15ebd15 --- /dev/null +++ b/extensions/strategies/kc/strategy.js @@ -0,0 +1,81 @@ +var z = require('zero-fill') + , n = require('numbro') + , kc = require('../../../lib/kc') + , Phenotypes = require('../../../lib/phenotype') + +module.exports = { + name: 'kc', + description: 'Buy when (Signal ≤ Lower Keltner Channel) and sell when (Signal ≥ Upper Keltner Channel).', + + getOptions: function () { + this.option('period', 'period length, same as --period_length', String, '1h') + this.option('period_length', 'period length, same as --period', String, '1h') + this.option('kc_size', 'period size', Number, 20) + this.option('kc_multiplier', 'multiplier for the average true range', Number, 1) + this.option('kc_upper_channel_pct', 'pct the current price should be near the keltner upper channel before we sell', Number, 0) + this.option('kc_lower_channel_pct', 'pct the current price should be near the keltner lower channel before we buy', Number, 0) + }, + + calculate: function (s) { + // calculate Keltner Channels + kc(s, 'kc', s.options.kc_size) + }, + + onPeriod: function (s, cb) { + if (s.period.kc) { + if (s.period.kc.upper && s.period.kc.lower) { + let upperChannel = s.period.kc.upper[s.period.kc.upper.length-1] + let lowerChannel = s.period.kc.lower[s.period.kc.lower.length-1] + if (s.period.close > (upperChannel / 100) * (100 - s.options.kc_upper_channel_pct)) { + s.signal = 'sell' + } else if (s.period.close < (lowerChannel / 100) * (100 + s.options.kc_lower_channel_pct)) { + s.signal = 'buy' + } else { + s.signal = null // hold + } + } + } + cb() + }, + + onReport: function (s) { + var cols = [] + if (s.period.kc) { + if (s.period.kc.upper && s.period.kc.lower) { + let upperChannel = s.period.kc.upper[s.period.kc.upper.length-1] + let lowerChannel = s.period.kc.lower[s.period.kc.lower.length-1] + var color = 'grey' + if (s.period.close > (upperChannel / 100) * (100 - s.options.kc_upper_channel_pct)) { + color = 'green' + } else if (s.period.close < (lowerChannel / 100) * (100 + s.options.kc_lower_channel_pct)) { + color = 'red' + } + cols.push(z(8, n(s.period.close).format('+00.0000'), ' ')[color]) + cols.push(z(8, n(lowerChannel).format('0.000000').substring(0,7), ' ').cyan) + cols.push(z(8, n(upperChannel).format('0.000000').substring(0,7), ' ').cyan) + } + } + else { + cols.push(' ') + } + return cols + }, + + phenotypes: { + // -- common + period_length: Phenotypes.RangePeriod(1, 120, 'm'), + markdown_buy_pct: Phenotypes.RangeFloat(-1, 5), + markup_sell_pct: Phenotypes.RangeFloat(-1, 5), + order_type: Phenotypes.ListOption(['maker', 'taker']), + sell_stop_pct: Phenotypes.Range0(1, 50), + buy_stop_pct: Phenotypes.Range0(1, 50), + profit_stop_enable_pct: Phenotypes.Range0(1, 20), + profit_stop_pct: Phenotypes.Range(1,20), + + // -- strategy + kc_size: Phenotypes.Range(1, 40), + kc_upper_channel_pct: Phenotypes.RangeFloat(-1, 30), + kc_lower_channel_pct: Phenotypes.RangeFloat(-1, 30) + } +} + diff --git a/lib/bollinger.js b/lib/bollinger.js index d6373bea1d..7c823f94c8 100644 --- a/lib/bollinger.js +++ b/lib/bollinger.js @@ -1,4 +1,4 @@ -// Linear Regression Curve +// Bollinger Bands var bollingerbands = require('bollinger-bands') module.exports = function bollinger (s, key, length, source_key) { if (!source_key) source_key = 'close' diff --git a/lib/kc.js b/lib/kc.js new file mode 100644 index 0000000000..6111b7ea81 --- /dev/null +++ b/lib/kc.js @@ -0,0 +1,18 @@ +// Keltner Channels +var keltnerchannel = require('keltnerchannel').kc +module.exports = function kc (s, key, length, source_key) { + if (!source_key) source_key = 'close' + if (s.lookback.length > length) { + let data = [] + for (var i=length-1; i>=0; i--) { + data.push({ + high: s.lookback[i].high, + low: s.lookback[i].low, + close: s.lookback[i].close + }) + } + let result = keltnerchannel(data, s.options.kc_size, s.options.kc_multiplier) + s.period[key] = result + } +} + diff --git a/package-lock.json b/package-lock.json index 394b5de877..e44d15208b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6976,6 +6976,11 @@ "verror": "1.10.0" } }, + "keltnerchannel": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/keltnerchannel/-/keltnerchannel-1.4.2.tgz", + "integrity": "sha512-0UiEdLg5tHHQpcLu2lueKnYpolsg9/IisbTzRwrrnOUCS/2YUPDgWmw71foGMs8Smgo/ctc7kwULG0e38MKvCQ==" + }, "keyv": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", diff --git a/package.json b/package.json index 3b233dd04d..26595c6835 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "ip": "~1.1.5", "jasmine": "^2.8.0", "jquery": "^3.2.1", + "keltnerchannel": "^1.4.2", "kraken-api": "^1.0.0", "lint-staged": "^7.0.0", "lodash": "^4.17.4",