'use strict'; import _ from 'underscore'; import NumberUtil from '../utils/number.js'; import ValidateMixin from '../mixin/validate.js'; import SetOptionsMixin from '../mixin/set-options.js'; const DIRECTION_UPTREND = 'uptrend'; const DIRECTION_DOWNTREND = 'downtrend'; /** * @param {object} [options] * @param {number} [options.startIndex] * @param {number} [options.endIndex}] * @param {number} [options.accelerationStep=0.02] * @param {number} [options.accelerationMax=0.2] * @param {boolean} [option.excludeFirst=true] */ function SAR(options = {}) { if (!new.target) throw new Error('ERROR: SAR() must be called with new'); this._options = { startIndex: null, endIndex: null, accelerationStep: 0.02, accelerationMax: 0.2, excludeFirst: true, }; Object.assign(this, SetOptionsMixin); this.setOptions(options); this._collection = []; Object.assign(this, ValidateMixin); } SAR.prototype = { setValues(values) { if (!Array.isArray(values)) { throw new Error('ERROR: values param is not an array'); } this._collection = values; }, clear() { this._collection = []; }, calculate(direction) { direction = direction.toLowerCase() || ''; let idx = _.indexOf([DIRECTION_UPTREND, DIRECTION_DOWNTREND], direction); if (idx < 0) { throw new Error('ERROR: Invalid direction argument '); } this._validate(); return this._compute(direction); }, _compute(direction) { let { startIndex, endIndex } = this._options; if (!NumberUtil.isNumeric(startIndex)) startIndex = 0; if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1; if (!_.has(this._collection[0], 'high') || !_.has(this._collection[0], 'low')) { throw new Error('ERROR: Missing high or low property'); } switch (direction) { case DIRECTION_UPTREND: return this._upTrend(startIndex, endIndex); case DIRECTION_DOWNTREND: return this._downTrend(startIndex, endIndex); } }, _upTrend(startIndex, endIndex) { if (!NumberUtil.isNumeric(startIndex) || !NumberUtil.isNumeric(endIndex)) { throw new Error('ERROR: Invalid Argument'); } let results = []; let ep = []; let { accelerationStep: ep_multiplier, accelerationMax } = this._options; if (!this._options.excludeFirst) { results.push({ sar: 0, ep: 0, ep_sar: 0, af: 0, af_ep_sar: 0, }); } for (let i = startIndex + 1; i <= endIndex; i++) { let af = 0; let prevCollectionItem = this._collection[i - 1]; let currentCollectionItem = this._collection[i]; if (i == startIndex + 1) { af = ep_multiplier; ep.push(currentCollectionItem.high); results.push({ sar: prevCollectionItem.low, ep: currentCollectionItem.high, ep_sar: NumberUtil.roundTo(currentCollectionItem.high - prevCollectionItem.low, 2), af: af, af_ep_sar: NumberUtil.roundTo(af * (currentCollectionItem.high - prevCollectionItem.low), 2), }); continue; } let newExtremePoint = false; if (currentCollectionItem.high > ep[ep.length - 1]) { ep.push(currentCollectionItem.high); newExtremePoint = true; } af = ep.length * ep_multiplier; if (af > accelerationMax) af = accelerationMax; let priorAF = (newExtremePoint) ? (ep.length - 1) * ep_multiplier : ep.length * ep_multiplier; let priorEP = (newExtremePoint) ? ep[ep.length - 2] : ep[ep.length - 1]; let priorResultItem = results[results.length - 1]; let sar = priorResultItem.sar + (priorAF * (priorEP - priorResultItem.sar)); results.push({ sar: NumberUtil.roundTo(sar, 2), ep: ep[ep.length - 1], ep_sar: NumberUtil.roundTo(ep[ep.length - 1] - sar, 2), af: af, af_ep_sar: NumberUtil.roundTo(af * (ep[ep.length - 1] - sar), 2), trend: 'bullish', }); } return results; }, _downTrend(startIndex, endIndex) { if (!NumberUtil.isNumeric(startIndex) || !NumberUtil.isNumeric(endIndex)) { throw new Error('ERROR: Invalid Argument'); } let results = []; let ep = []; let { accelerationStep: ep_multiplier, accelerationMax } = this._options; if (!this._options.excludeFirst) { results.push({ sar: 0, ep: 0, sar_ep: 0, af: 0, af_sar_ep: 0, }); } for (let i = startIndex + 1; i <= endIndex; i++) { let af = 0; let prevCollectionItem = this._collection[i - 1]; let currentCollectionItem = this._collection[i]; if (i == startIndex + 1) { af = ep_multiplier; ep.push(currentCollectionItem.low); results.push({ sar: prevCollectionItem.high, ep: currentCollectionItem.low, sar_ep: NumberUtil.roundTo(prevCollectionItem.high - currentCollectionItem.low, 2), af: af, af_sar_ep: NumberUtil.roundTo(af * (prevCollectionItem.high - currentCollectionItem.low), 2), }); continue; } let newExtremePoint = false; if (currentCollectionItem.low < ep[ep.length - 1]) { ep.push(currentCollectionItem.low); newExtremePoint = true; } af = ep.length * ep_multiplier; if (af > accelerationMax) af = accelerationMax; let priorAF = (newExtremePoint) ? (ep.length - 1) * ep_multiplier : ep.length * ep_multiplier; let priorEP = (newExtremePoint) ? ep[ep.length - 2] : ep[ep.length - 1]; let priorResultItem = results[results.length - 1]; let sar = priorResultItem.sar - (priorAF * (priorResultItem.sar - priorEP)); results.push({ sar: NumberUtil.roundTo(sar, 2), ep: ep[ep.length - 1], sar_ep: NumberUtil.roundTo(sar - ep[ep.length - 1], 2), af: af, af_sar_ep: NumberUtil.roundTo(af * (sar - ep[ep.length - 1]), 2), trend: 'bearish', }); } return results; }, } export default SAR;