'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 RSI(options = {}) { if (!new.target) throw new Error('ERROR: RSI() must be called with new'); this._options = { startIndex: null, endIndex: null, periods: 14, sliceOffset: false, lazyEvaluation: true, maxTickDuration: 10, }; let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin]; Object.assign(this, ...m); this.setOptions(options); this._collection = []; }; RSI.prototype = { setValues(values) { if (!Array.isArray(values)) { throw new Error('ERROR: values param is not an array'); } this._collection = values; }, clear() { this._collection = []; }, calculate() { this._validate(this._collection, this._options); return this._handleGenerator(this._compute()); }, _compute: function* () { let results = []; let { periods, startIndex, endIndex, sliceOffset, lazyEvaluation } = this._options; let convertToResultItem = (price, gain = null, loss = null, avg_gain = null, avg_loss = null) => { let rs = null; let rsi = null; if (NumberUtil.isNumeric(avg_gain) && NumberUtil.isNumeric(avg_loss)) { rs = avg_gain / avg_loss; rsi = 100 - (100 / (1 + rs)); } return { price, gain: NumberUtil.isNumeric(gain) ? NumberUtil.roundTo(gain, 2) : null, loss: NumberUtil.isNumeric(loss) ? NumberUtil.roundTo(loss, 2) : null, avg_gain: NumberUtil.isNumeric(avg_gain) ? NumberUtil.roundTo(avg_gain, 2) : null, avg_loss: NumberUtil.isNumeric(avg_loss) ? NumberUtil.roundTo(avg_loss, 2) : null, rs: NumberUtil.isNumeric(rs) ? NumberUtil.roundTo(rs, 2) : null, rsi: NumberUtil.isNumeric(rsi) ? NumberUtil.roundTo(rsi, 2) : null, }; }; if (!NumberUtil.isNumeric(startIndex)) startIndex = 0; if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1; let sumPeriodGain = 0; let sumPeriodLoss = 0; for (let i = startIndex, endPeriodIndex = startIndex + periods - 1; i <= endPeriodIndex; i++) { let resultItem = null; if (i == startIndex) { resultItem = convertToResultItem(this._collection[i], 0, 0); } else { let { gain, loss } = this._calcGainLoss(i); sumPeriodGain += gain; sumPeriodLoss += loss; resultItem = convertToResultItem(this._collection[i], gain, loss); } if (!sliceOffset) { results.push(resultItem); if (lazyEvaluation) yield resultItem; } } let ag = sumPeriodGain / periods; let al = sumPeriodLoss / periods; let { gain, loss } = this._calcGainLoss(startIndex + periods); let resultItem = convertToResultItem(this._collection[startIndex + periods], gain, loss, ag, al); results.push(resultItem); if (lazyEvaluation) yield resultItem; for (let i = startIndex + periods + 1; i <= endIndex; i++) { let { gain, loss } = this._calcGainLoss(i); let prev_avg_gain = results[results.length - 1].avg_gain; let prev_avg_loss = results[results.length - 1].avg_loss; let ag = ((prev_avg_gain * (periods - 1)) + gain) / periods; let al = ((prev_avg_loss * (periods - 1)) + loss) / periods; let resultItem = convertToResultItem(this._collection[i], gain, loss, ag, al); results.push(resultItem); if (lazyEvaluation) yield resultItem; } if (!lazyEvaluation) { return results; } }, _calcGainLoss(collectionIndex) { let result = { gain: 0, loss: 0 }; if (collectionIndex >= 0 && collectionIndex < this._collection.length) { if (collectionIndex > 0) { let diff = this._collection[collectionIndex] - this._collection[collectionIndex - 1]; if (diff > 0) result.gain = diff; else result.loss = Math.abs(diff); } } else throw new Error('ERROR: index is outside the collection range'); return result; }, } export default RSI;