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

124 lines
3.3 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=20]
* @param {number} [options.startIndex]
* @param {number} [options.endIndex}]
* @param {boolean} [options.lazyEvaluation=true]
* @param {number} [option.maxTickDuration=10]
*/
function ATR(options = {}) {
if (!new.target) throw new Error('ERROR: ATR() must be called with new');
this._options = {
periods: 20,
startIndex: null,
endIndex: null,
lazyEvaluation: true,
maxTickDuration: 10,
}
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
Object.assign(this, ...m);
this.setOptions(options);
this._collection = [];
}
ATR.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 } = this._options;
let convertToResultItem = (tr, atr) => {
return { tr: NumberUtil.roundTo(tr, 2), atr: NumberUtil.roundTo(atr, 2) };
};
if (_.isNull(startIndex)) startIndex = 0;
if (_.isNull(endIndex)) endIndex = this._collection.length - 1;
for (let i = startIndex; i <= endIndex; i++) {
let item = this._collection[i];
let prevItem = (i > startIndex) ? this._collection[i - 1] : null;
let tr = this._getTrueRange(item, prevItem);
let atr = 0;
if (i < (startIndex + periods - 1)) {
//do nothing
}
else if (i == (startIndex + periods - 1)) {
let trCollection = results.slice(startIndex, startIndex + periods + 2);
atr = this._calcFirstATR(trCollection);
}
else {
let prevATR = results[results.length - 1].atr;
atr = this._calcRemainingATR(prevATR, tr, periods);
}
let resultItem = convertToResultItem(tr, atr);
results.push(resultItem);
if (lazyEvaluation) yield resultItem;
};
if (!lazyEvaluation) 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) ? NumberUtil.roundTo(Math.abs(currItem.high - prevItem.close), 2) : 0;
let diff_2 = (prevItem) ? NumberUtil.roundTo(Math.abs(currItem.low - prevItem.close), 2) : 0;
let diff_3 = NumberUtil.roundTo(currItem.high - currItem.low, 2);
let max = diff_1;
if (max < diff_2) max = diff_2;
if (max < diff_3) max = diff_3;
return max;
},
_calcFirstATR(trueRangeCollection) {
let sum = 0;
trueRangeCollection.forEach((item, idx) => {
sum += item.tr;
});
return (sum / trueRangeCollection.length);
},
_calcRemainingATR(prevATR, currTR, periods) {
return (prevATR * (periods - 1) + currTR) / periods;
},
};
export default ATR;