'use strict'; import _ from 'underscore'; import NumberUtil from '../utils/number.js'; import ValidateMixin from '../mixin/validate.js'; import SetOptionsMixin from '../mixin/set-options.js'; import HandleGeneratorMixin from '../mixin/handle-generator.js'; /** * @param {object} [options] * @param {number} [optons.periods=14] * @param {number} [options.startIndex] * @param {number} [options.endIndex}] * @param {boolean} [options.sliceOffset=False] * @param {boolean} [options.lazyEvaluation=true] * @param {number} [option.maxTickDuration=10] */ function ADI(options = {}) { if (!new.target) throw new Error('ERROR: ADI() must be called with new'); this._options = { periods: 14, startIndex: null, endIndex: null, sliceOffset: false, lazyEvaluation: true, maxTickDuration: 10, } let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin]; Object.assign(this, ...m); this.setOptions(options); this._collection = []; } ADI.prototype = { setValues(values) { if (!Array.isArray(values)) { throw new Error('ERROR: values param is not an array'); } this._collection = values; }, calculate() { this._validate(this._collection, this._options); return this._handleGenerator(this._compute()); }, _compute: function* () { let results = []; let { periods, startIndex, endIndex, lazyEvaluation, sliceOffset } = this._options; let convertToResultItem = (tr, pdm, ndm, trPeriod, pdmPeriod, ndmPeriod, pdi, ndi, dx, adx) => { return { tr: NumberUtil.roundTo(tr, 2), pdm: NumberUtil.roundTo(pdm, 2), ndm: NumberUtil.roundTo(ndm, 2), trPeriod: NumberUtil.roundTo(trPeriod, 2), pdmPeriod: NumberUtil.roundTo(pdmPeriod, 2), ndmPeriod: NumberUtil.roundTo(ndmPeriod, 2), pdi: NumberUtil.roundTo(pdi, 2), ndi: NumberUtil.roundTo(ndi, 2), dx: NumberUtil.roundTo(dx, 2), adx: NumberUtil.roundTo(adx, 2), }; }; if (_.isNull(startIndex)) startIndex = 0; if (_.isNull(endIndex)) endIndex = this._collection.length - 1; for (let i = startIndex; i <= endIndex; i++) { let tr = 0; let pdm = 0; let ndm = 0; let trPeriod = 0; let pdmPeriod = 0; let ndmPeriod = 0; let dx = 0; let adx = 0; let pdi = 0; let ndi = 0; if (i > startIndex) { let currItem = this._collection[i]; let prevItem = this._collection[i - 1]; tr = this._getTrueRange(currItem, prevItem); let positiveDirectionalMovement = currItem.high - prevItem.high; let negativeDirectionalMovement = prevItem.low - currItem.low; if (positiveDirectionalMovement > negativeDirectionalMovement && positiveDirectionalMovement > 0) { pdm = positiveDirectionalMovement; } else if (negativeDirectionalMovement > positiveDirectionalMovement && negativeDirectionalMovement > 0) { ndm = negativeDirectionalMovement; } if (i > startIndex + periods - 1) { let obj = null; if (i == startIndex + periods) { let items = results.slice(i - periods); items.push({ tr, pdm, ndm }); obj = this._calcSmoothedFirstPeriod(items); } if (i > startIndex + periods) { obj = this._calcSmoothedSubsequentPeriod(results[results.length - 1], tr, pdm, ndm); } trPeriod = obj.tr; pdmPeriod = obj.pdm; ndmPeriod = obj.ndm pdi = (pdmPeriod / trPeriod) * 100; ndi = (ndmPeriod / trPeriod) * 100; let di_diff = Math.abs(pdi - ndi); let di_sum = pdi + ndi; dx = (di_diff / di_sum) * 100; if (i == startIndex + periods + periods - 1) { adx = this._calcFirstADX(results.slice(-1 * (periods - 1)), dx); } if (i > startIndex + periods + periods - 1) { adx = this._calcSubsequentADX(results[results.length - 1].adx, dx); } } } let resultItem = convertToResultItem(tr, pdm, ndm, trPeriod, pdmPeriod, ndmPeriod, pdi, ndi, dx, adx); results.push(resultItem); if (lazyEvaluation) { if (sliceOffset) { if (i >= startIndex + periods + periods - 1) yield resultItem; } else yield resultItem; } } if (sliceOffset) return results.slice(startIndex + periods + periods - 1); return results; }, _getTrueRange(currItem, prevItem = null) { if (!_.isNull(prevItem) && !_.isObject(currItem)) { throw new Error('ERROR: invalid param, not an object'); } if (!_.has(currItem, 'high') || !_.has(currItem, 'low') || !_.has(currItem, 'close')) { throw new Error('ERROR: invalid oject property'); } let diff_1 = (prevItem) ? Math.abs(currItem.high - prevItem.close) : 0; let diff_2 = (prevItem) ? Math.abs(currItem.low - prevItem.close) : 0; let diff_3 = currItem.high - currItem.low; return Math.max(diff_1, diff_2, diff_3); }, _calcSmoothedFirstPeriod(items) { let sum_tr = 0; let sum_pdm = 0; let sum_ndm = 0; for (let i = 0; i < items.length; i++) { sum_tr += items[i].tr; sum_pdm += items[i].pdm; sum_ndm += items[i].ndm; } return { tr: sum_tr, pdm: sum_pdm, ndm: sum_ndm }; }, _calcSmoothedSubsequentPeriod(prevItem, tr, pdm, ndm) { let div = this._options.periods; tr = prevItem.trPeriod - (prevItem.trPeriod / div) + tr; pdm = prevItem.pdmPeriod - (prevItem.pdmPeriod / div) + pdm; ndm = prevItem.ndmPeriod - (prevItem.ndmPeriod / div) + ndm; return { tr, pdm, ndm }; }, _calcFirstADX(items, dx) { let sum = 0; for (let i = 0; i < items.length; i++) sum += items[i].dx; sum += dx; return sum / this._options.periods; }, _calcSubsequentADX(prevADX, currentDX) { return (prevADX * (this._options.periods - 1) + currentDX) / this._options.periods; } }; export default ADI;