stock-indicators/lib/indicator/stochastic-oscillator.js
2025-03-31 11:20:04 +02:00

140 lines
3.8 KiB
JavaScript
Executable File

'use strict';
import SMA from './simple-moving-average.js';
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.smaPeriods=3]
* @param {number} [options.startIndex]
* @param {number} [options.endIndex}]
* @param {boolean} [options.sliceOffset=false]
* @param {boolean} [options.lazyEvaluation=true]
* @param {number} [option.maxTickDuration=10]
*/
function SO(options = {}) {
if (!new.target) throw new Error('ERROR: SO() must be called with new');
this._options = {
periods: 14,
smaPeriods: 3,
startIndex: null,
endIndex: null,
sliceOffset: false,
lazyEvaluation: true,
maxTickDuration: 10,
}
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
Object.assign(this, ...m);
this.setOptions(options);
this._collection = [];
}
SO.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 k_results = [];
let { periods, smaPeriods, startIndex, endIndex, sliceOffset, lazyEvaluation } = this._options;
if (!NumberUtil.isNumeric(startIndex)) startIndex = 0;
if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1;
if (!sliceOffset) {
for (let i = startIndex; i < startIndex + periods; i++) {
let resultItem = { k: 0, d: 0, price: this._collection[i] };
results.push(resultItem);
if (lazyEvaluation) yield resultItem;
}
}
if (startIndex + periods > endIndex) {
throw new Error('ERROR: Out of Range');
}
for (let i = startIndex + periods; i <= endIndex; i++) {
let currentPrice = this._collection[i];
let pastRangeStart = i - periods;
let pastRangeEnd = i - 1;
let resultLowest = this._findPrice('lowest', pastRangeStart, pastRangeEnd);
let resultHighest = this._findPrice('highest', pastRangeStart, pastRangeEnd);
let k = ((currentPrice - resultLowest) / (resultHighest - resultLowest)) * 100;
k = NumberUtil.roundTo(k, 2);
let d = 0;
if (k_results.length > smaPeriods) {
let smaStartRangeIndex = k_results.length - smaPeriods;
let smaEndRangeIndex = k_results.length - 1;
let sma = new SMA({ periods: smaPeriods, lazyEvaluation: false, sliceOffset: true });
let smaCollection = k_results.slice(smaStartRangeIndex, smaEndRangeIndex + 1);
sma.setValues(smaCollection);
let smaResult = sma.calculate();
if (Array.isArray(smaResult) && smaResult.length == 1) {
d = NumberUtil.roundTo(smaResult[0].sma, 2);
}
else {
throw new Error('ERROR: calculated SMA value invalid');
}
}
k_results.push(k);
let resultItem = { k, d, price: currentPrice };
results.push(resultItem);
if (lazyEvaluation) yield resultItem;
}
return results;
},
_findPrice(type, startIndex, endIndex) {
let idx = startIndex;
let resultPrice = null;
while (idx <= endIndex) {
let currentPrice = this._collection[idx];
if (idx == startIndex) {
resultPrice = currentPrice;
idx++;
continue;
}
switch (type) {
case 'lowest':
if (currentPrice < resultPrice) resultPrice = currentPrice;
break;
case 'highest':
if (currentPrice > resultPrice) resultPrice = currentPrice;
break;
}
idx++;
}
return resultPrice;
},
}
export default SO;