stock-indicators/lib/indicator/parabolic-stop-and-reverse.js
2025-03-31 11:20:04 +02:00

212 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';
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;