stock-indicators/lib/indicator/average-directional-index.js
2025-03-31 11:20:04 +02:00

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;