212 lines
5.7 KiB
JavaScript
Executable File
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;
|