203 lines
5.7 KiB
JavaScript
Executable File
203 lines
5.7 KiB
JavaScript
Executable File
'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;
|