stock-indicators/lib/indicator/money-flow-index.js
2025-03-31 11:20:04 +02:00

142 lines
4.2 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';
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 MFI(options = {}) {
if (!new.target) throw new Error('ERROR: MFI() 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 = [];
};
MFI.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 { periods, startIndex, endIndex, sliceOffset, lazyEvaluation } = this._options;
let convertToResultItem = (typicalPrice, rawMoneyFlow, positiveMoneyFlow, negativMoneyFlow, periodPositiveMoneyFlow = 0, periodNegativeMoneyFlow = 0) => {
let moneyFlowRatio = 0;
let moneyFlowIndex = 0;
if (periodPositiveMoneyFlow && periodNegativeMoneyFlow) {
moneyFlowRatio = periodPositiveMoneyFlow / periodNegativeMoneyFlow;
moneyFlowIndex = 100 - (100 / (1 + moneyFlowRatio));
//alternative
//moneyFlowIndex = 100 * ( periodPositiveMoneyFlow / ( periodPositiveMoneyFlow + periodNegativeMoneyFlow ) );
}
return {
typicalPrice: NumberUtil.roundTo(typicalPrice, 2),
rawMoneyFlow,
positiveMoneyFlow,
negativMoneyFlow,
moneyFlowRatio: NumberUtil.roundTo(moneyFlowRatio, 2),
moneyFlowIndex: NumberUtil.roundTo(moneyFlowIndex, 2),
periodPositiveMoneyFlow,
periodNegativeMoneyFlow,
};
};
if (!NumberUtil.isNumeric(startIndex)) startIndex = 0;
if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1;
let prevTypicalPrice;
for (let i = startIndex; i <= endIndex; i++) {
let item = this._collection[i];
let typicalPrice = (item.high + item.low + item.close) / 3;
let rawMoneyFlow = Math.round(typicalPrice * item.volume);
let positiveMoneyFlow = 0;
let negativMoneyFlow = 0;
if (i > startIndex) {
if (typicalPrice > prevTypicalPrice) positiveMoneyFlow = rawMoneyFlow;
if (typicalPrice < prevTypicalPrice) negativMoneyFlow = rawMoneyFlow;
}
let periodPositiveMoneyFlow = 0;
let periodNegativeMoneyFlow = 0;
if (i >= startIndex + periods) {
let items = results.slice(results.length - periods + 1);
let { sumPositiveMoneyFlow, sumNegativeMoneyFlow } = this._calcPeriodMoneyFlow(items);
periodPositiveMoneyFlow = sumPositiveMoneyFlow + positiveMoneyFlow;
periodNegativeMoneyFlow = sumNegativeMoneyFlow + negativMoneyFlow;
}
let resultItem = convertToResultItem(typicalPrice, rawMoneyFlow, positiveMoneyFlow, negativMoneyFlow, periodPositiveMoneyFlow, periodNegativeMoneyFlow);
resultItem.high = item.high;
resultItem.low = item.low;
resultItem.close = item.close;
resultItem.volume = item.volume;
results.push(resultItem);
if (lazyEvaluation) {
if (sliceOffset) {
if (i >= startIndex + periods) yield resultItem;
}
else yield resultItem;
}
prevTypicalPrice = typicalPrice;
}
return (sliceOffset) ? results.slice(startIndex + periods) : results;
},
_calcPeriodMoneyFlow(collection) {
let sumPositiveMoneyFlow = 0;
let sumNegativeMoneyFlow = 0;
for (let i = 0; i < collection.length; i++) {
let item = collection[i];
sumPositiveMoneyFlow += item.positiveMoneyFlow;
sumNegativeMoneyFlow += item.negativMoneyFlow;
}
return { sumPositiveMoneyFlow, sumNegativeMoneyFlow };
},
};
export default MFI;