init commit
This commit is contained in:
commit
4486bbe6d5
21
LICENSE
Executable file
21
LICENSE
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
Copyright (c) 2019 Lukas B.
|
||||||
|
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
150
README.md
Executable file
150
README.md
Executable file
@ -0,0 +1,150 @@
|
|||||||
|
# Stock-Indicators
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
A Node.js toolkit with various indicators and oscillators for the technical stock analysis. This package contains only the mathematical calculations without any visual output. Internally the iteration and calculation over the passed datasets is done trought (async) generators.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```js
|
||||||
|
let run = async () => {
|
||||||
|
try {
|
||||||
|
let lwma = new Indicator.LWMA({ periods: 4 })
|
||||||
|
let bb = new Indicator.BB({ periods: 4 })
|
||||||
|
let data = [25.5, 26.75, 27.0, 26.5, 27.25]
|
||||||
|
lwma.setValues(data)
|
||||||
|
bb.setValues(data)
|
||||||
|
let lwmaCollection = await lwma.calculate()
|
||||||
|
let bbCollection = await bb.calculate()
|
||||||
|
for (let i = 0, len = lwmaCollection.length; i < len; i++) {
|
||||||
|
console.log(
|
||||||
|
`Price: ${lwmaCollection[i].price}, LWMA: ${lwmaCollection[i].lwma}, BB Upper: ${bbCollection[i].upper}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run()
|
||||||
|
```
|
||||||
|
|
||||||
|
### List of indicators
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
See the list below for all available indicators in the package. Retrieve the indicator module via the accessor property or with the alias.
|
||||||
|
|
||||||
|
| Indicator | Module accessor | Alias |
|
||||||
|
| ------------------------------------- | ---------------------------------- | ----- |
|
||||||
|
| Average True Range | AverageTrueRange | ATR |
|
||||||
|
| Bollinger Bands | BollingerBands | BB |
|
||||||
|
| Exponential Moving Average | ExponentialMovingAverage | EMA |
|
||||||
|
| Linearly Weighted Moving Average | LinearlyWeightedMovingAverage | LWMA |
|
||||||
|
| Money Flow Index | MoneyFlowIndex | MFI |
|
||||||
|
| Moving Average Convergence Divergence | MovingAverageConvergenceDivergence | MACD |
|
||||||
|
| On Balance Volume | OnBalanceVolume | OBV |
|
||||||
|
| Rate Of Change | RateOfChange | ROC |
|
||||||
|
| Relative Strength Index | RelativeStrengthIndex | RSI |
|
||||||
|
| Simple Moving Average | SimpleMovingAverage | SMA |
|
||||||
|
| Smoothed Moving Average | SmoothedMovingAverage | SMMA |
|
||||||
|
| Stochastic Oscillator | StochasticOscillator | SO |
|
||||||
|
| Weighted Moving Average | WeightedMovingAverage | WMA |
|
||||||
|
|
||||||
|
### Data serie item
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The type of the item in the data serie that will be passed into the `setValues` function
|
||||||
|
|
||||||
|
| Indicator | Type | Usage |
|
||||||
|
| ------------------------------------- | ------ | ------------------------------------------------------------------------ |
|
||||||
|
| Average True Range | Number |
|
||||||
|
| Bollinger Bands | Number |
|
||||||
|
| Exponential Moving Average | Number |
|
||||||
|
| Linearly Weighted Moving Average | Number |
|
||||||
|
| Money Flow Index | Object | {high:\<Number\>, low:\<Number\>, close:\<Number\>, volume:\<Number\> }; |
|
||||||
|
| Moving Average Convergence Divergence | Number |
|
||||||
|
| On Balance Volume | Object | {price:\<Number\>, volume:\<Number\>} |
|
||||||
|
| Rate Of Change | Number |
|
||||||
|
| Relative Strength Index | Number |
|
||||||
|
| Simple Moving Average | Number |
|
||||||
|
| Smoothed Moving Average | Number |
|
||||||
|
| Stochastic Oscillator | Number |
|
||||||
|
| Weighted Moving Average | Number |
|
||||||
|
|
||||||
|
### Result collection item
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Each item in the result collection contains several object properties. See the list below which properties belongs to the particular indicator. All values are numbers except where noted.
|
||||||
|
|
||||||
|
| Indicator | Collection Item properties |
|
||||||
|
| ------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||||
|
| Average True Range | {tr, atr} |
|
||||||
|
| Bollinger Bands | {upper:\<Array\>, middle:\<Array\>, lower:\<Array\>, price:\<Array\>} |
|
||||||
|
| Exponential Moving Average | {price, ema} |
|
||||||
|
| Linearly Weighted Moving Average | {price, lmwa} |
|
||||||
|
| Moving Average Convergence Divergence | {slow_ema:\<Array\>, fast_ema:\<Array\>, signal_ema:\<Array\>, macd:\<Array\>, prices:\<Array\>} |
|
||||||
|
| On Balance Volume | {price, obv} |
|
||||||
|
| Rate Of Change | {price, roc} |
|
||||||
|
| Relative Strength Index | {price, gain, loss, avg_gain, avg_loss, rs, rsi} |
|
||||||
|
| Simple Moving Average | {price, sma} |
|
||||||
|
| Smoothed Moving Average | {price, smma} |
|
||||||
|
| Stochastic Oscillator | {k,v, price} |
|
||||||
|
| Weighted Moving Average | {price, wma} |
|
||||||
|
|
||||||
|
### Indicator options
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To configure the indicator with different settings, you can pass an optional configuration object into the indicator constructor.
|
||||||
|
|
||||||
|
| Indicator | Option properties |
|
||||||
|
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Average True Range | periods, startIndex, endIndex, lazyEvaluation, maxTickDuration |
|
||||||
|
| Bollinger Bands | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| Exponential Moving Average | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration, emaResultsOnly, startWithFirst |
|
||||||
|
| Linearly Weighted Moving Average | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| Money Flow Index | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| Moving Average Convergence Divergence | fastPeriods, slowPeriods, signalPeriods, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| On Balance Volume | startIndex, endIndex, lazyEvaluation, maxTickDuration |
|
||||||
|
| Rate Of Change | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| Relative Strength Index | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| Simple Moving Average | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| Smoothed Moving Average | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| Stochastic Oscillator | periods, startIndex, endIndex, smaPeriods, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
| Weighted Moving Average | periods, startIndex, endIndex, sliceOffset, lazyEvaluation, maxTickDuration |
|
||||||
|
|
||||||
|
See the table below for a description of the particular option property.
|
||||||
|
|
||||||
|
| Option property | Type | Description |
|
||||||
|
| --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| periods | Number | The time periods to calculate the indicator |
|
||||||
|
| startIndex | Number | The index for the passed data serie to start the calulation |
|
||||||
|
| endIndex | Number | The index for the passed data serie to end the calculation |
|
||||||
|
| sliceOffset | Boolean | Omit items in result collection used for inital period calculation |
|
||||||
|
| fastPeriods | Number | The time periods for the fast moving average |
|
||||||
|
| slowPeriods | Number | The time periods for the slow moving average |
|
||||||
|
| signalPeriods | Number | The time periods for the signal average |
|
||||||
|
| smaPeriods | Number | The time periods for the simple moving average |
|
||||||
|
| lazyEvaluation | Boolean | Do the computation of passed values in an asynchronous fashion |
|
||||||
|
| maxTickDuration | Number | The computation tick duration in milliseconds. If the computation is not completed, it will be continued in the tick of the next event loop. |
|
||||||
|
|
||||||
|
## Run Tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
All tests are inside `test` folder
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
$ npm test
|
||||||
|
|
||||||
|
Run code coverage report:
|
||||||
|
|
||||||
|
$ npm run test:coverage
|
||||||
|
|
||||||
23
example/simple.js
Executable file
23
example/simple.js
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import Indicator from '../index.js';
|
||||||
|
|
||||||
|
let run = async () => {
|
||||||
|
try {
|
||||||
|
let lwma = new Indicator.LWMA({ periods: 4 });
|
||||||
|
let bb = new Indicator.BB({ periods: 4 });
|
||||||
|
let data = [25.5, 26.75, 27.0, 26.5, 27.25];
|
||||||
|
lwma.setValues(data);
|
||||||
|
bb.setValues(data);
|
||||||
|
let lwmaCollection = await lwma.calculate();
|
||||||
|
let bbCollection = await bb.calculate();
|
||||||
|
for (let i = 0, len = lwmaCollection.length; i < len; i++) {
|
||||||
|
console.log(`Price: ${lwmaCollection[i].price}, LWMA: ${lwmaCollection[i].lwma}, BB Upper: ${bbCollection[i].upper}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
44
index.js
Executable file
44
index.js
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import ATR from './lib/indicator/average-true-range.js';
|
||||||
|
import BB from './lib/indicator/bollinger-bands.js';
|
||||||
|
import EMA from './lib/indicator/exponential-moving-average.js';
|
||||||
|
import LWMA from './lib/indicator/linearly-weighted-moving-average.js';
|
||||||
|
import MACD from './lib/indicator/moving-average-convergence-divergence.js';
|
||||||
|
import OBV from './lib/indicator/on-balance-volume.js';
|
||||||
|
import RSI from './lib/indicator/relative-strength-index.js';
|
||||||
|
import SMA from './lib/indicator/simple-moving-average.js';
|
||||||
|
import SO from './lib/indicator/stochastic-oscillator.js';
|
||||||
|
import ROC from './lib/indicator/rate-of-change.js';
|
||||||
|
import MFI from './lib/indicator/money-flow-index.js';
|
||||||
|
import SMMA from './lib/indicator/smoothed-moving-average.js';
|
||||||
|
import WMA from './lib/indicator/weighted-moving-average.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
AverageTrueRange: ATR,
|
||||||
|
ATR,
|
||||||
|
BollingerBands: BB,
|
||||||
|
BB,
|
||||||
|
ExponentialMovingAverage: EMA,
|
||||||
|
EMA,
|
||||||
|
LinearlyWeightedMovingAverage: LWMA,
|
||||||
|
LWMA,
|
||||||
|
MovingAverageConvergenceDivergence: MACD,
|
||||||
|
MACD,
|
||||||
|
MoneyFlowIndex: MFI,
|
||||||
|
MFI,
|
||||||
|
OnBalanceVolume: OBV,
|
||||||
|
OBV,
|
||||||
|
RelativeStrengthIndex: RSI,
|
||||||
|
RSI,
|
||||||
|
SimpleMovingAverage: SMA,
|
||||||
|
SMA,
|
||||||
|
SmoothedMovingAverage: SMMA,
|
||||||
|
SMMA,
|
||||||
|
StochasticOscillator: SO,
|
||||||
|
SO,
|
||||||
|
RateOfChange: ROC,
|
||||||
|
ROC,
|
||||||
|
WeightedMovingAverage: WMA,
|
||||||
|
WMA
|
||||||
|
}
|
||||||
83
lib/indicator/accumulation-distribution-line.js
Executable file
83
lib/indicator/accumulation-distribution-line.js
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
'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} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function ADL(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: ADL() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ADL.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 convertToResultItem = (close, mfMultiplier, mfVolume, adl) => {
|
||||||
|
return {
|
||||||
|
price: close,
|
||||||
|
mfMultiplier,
|
||||||
|
mfVolume,
|
||||||
|
adl: Math.round(adl),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let { startIndex, endIndex, lazyEvaluation } = this._options;
|
||||||
|
|
||||||
|
if (_.isNull(startIndex)) startIndex = 0;
|
||||||
|
if (_.isNull(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
let item = this._collection[i];
|
||||||
|
let mfMultiplier = ((item.close - item.low) - (item.high - item.close)) / (item.high - item.low);
|
||||||
|
mfMultiplier = NumberUtil.roundTo(mfMultiplier, 4);
|
||||||
|
let mfVolume = Math.round(mfMultiplier * item.volume);
|
||||||
|
|
||||||
|
let prevAdl = 0;
|
||||||
|
if (i != startIndex) prevAdl = results[results.length - 1].adl;
|
||||||
|
let adl = prevAdl + mfVolume;
|
||||||
|
let resultItem = convertToResultItem(item.close, mfMultiplier, mfVolume, adl);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ADL;
|
||||||
202
lib/indicator/average-directional-index.js
Executable file
202
lib/indicator/average-directional-index.js
Executable file
@ -0,0 +1,202 @@
|
|||||||
|
'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 ADI(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: ADI() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
periods: 14,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ADI.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, lazyEvaluation, sliceOffset } = this._options;
|
||||||
|
|
||||||
|
let convertToResultItem = (tr, pdm, ndm, trPeriod, pdmPeriod, ndmPeriod, pdi, ndi, dx, adx) => {
|
||||||
|
return {
|
||||||
|
tr: NumberUtil.roundTo(tr, 2),
|
||||||
|
pdm: NumberUtil.roundTo(pdm, 2),
|
||||||
|
ndm: NumberUtil.roundTo(ndm, 2),
|
||||||
|
trPeriod: NumberUtil.roundTo(trPeriod, 2),
|
||||||
|
pdmPeriod: NumberUtil.roundTo(pdmPeriod, 2),
|
||||||
|
ndmPeriod: NumberUtil.roundTo(ndmPeriod, 2),
|
||||||
|
pdi: NumberUtil.roundTo(pdi, 2),
|
||||||
|
ndi: NumberUtil.roundTo(ndi, 2),
|
||||||
|
dx: NumberUtil.roundTo(dx, 2),
|
||||||
|
adx: NumberUtil.roundTo(adx, 2),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (_.isNull(startIndex)) startIndex = 0;
|
||||||
|
if (_.isNull(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
|
||||||
|
let tr = 0;
|
||||||
|
let pdm = 0;
|
||||||
|
let ndm = 0;
|
||||||
|
let trPeriod = 0;
|
||||||
|
let pdmPeriod = 0;
|
||||||
|
let ndmPeriod = 0;
|
||||||
|
let dx = 0;
|
||||||
|
let adx = 0;
|
||||||
|
let pdi = 0;
|
||||||
|
let ndi = 0;
|
||||||
|
|
||||||
|
if (i > startIndex) {
|
||||||
|
let currItem = this._collection[i];
|
||||||
|
let prevItem = this._collection[i - 1];
|
||||||
|
|
||||||
|
tr = this._getTrueRange(currItem, prevItem);
|
||||||
|
|
||||||
|
let positiveDirectionalMovement = currItem.high - prevItem.high;
|
||||||
|
let negativeDirectionalMovement = prevItem.low - currItem.low;
|
||||||
|
|
||||||
|
if (positiveDirectionalMovement > negativeDirectionalMovement && positiveDirectionalMovement > 0) {
|
||||||
|
pdm = positiveDirectionalMovement;
|
||||||
|
}
|
||||||
|
else if (negativeDirectionalMovement > positiveDirectionalMovement && negativeDirectionalMovement > 0) {
|
||||||
|
ndm = negativeDirectionalMovement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > startIndex + periods - 1) {
|
||||||
|
|
||||||
|
let obj = null;
|
||||||
|
if (i == startIndex + periods) {
|
||||||
|
let items = results.slice(i - periods);
|
||||||
|
items.push({ tr, pdm, ndm });
|
||||||
|
obj = this._calcSmoothedFirstPeriod(items);
|
||||||
|
}
|
||||||
|
if (i > startIndex + periods) {
|
||||||
|
obj = this._calcSmoothedSubsequentPeriod(results[results.length - 1], tr, pdm, ndm);
|
||||||
|
}
|
||||||
|
|
||||||
|
trPeriod = obj.tr;
|
||||||
|
pdmPeriod = obj.pdm;
|
||||||
|
ndmPeriod = obj.ndm
|
||||||
|
|
||||||
|
pdi = (pdmPeriod / trPeriod) * 100;
|
||||||
|
ndi = (ndmPeriod / trPeriod) * 100;
|
||||||
|
let di_diff = Math.abs(pdi - ndi);
|
||||||
|
let di_sum = pdi + ndi;
|
||||||
|
|
||||||
|
dx = (di_diff / di_sum) * 100;
|
||||||
|
|
||||||
|
if (i == startIndex + periods + periods - 1) {
|
||||||
|
adx = this._calcFirstADX(results.slice(-1 * (periods - 1)), dx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > startIndex + periods + periods - 1) {
|
||||||
|
adx = this._calcSubsequentADX(results[results.length - 1].adx, dx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultItem = convertToResultItem(tr, pdm, ndm, trPeriod, pdmPeriod, ndmPeriod, pdi, ndi, dx, adx);
|
||||||
|
results.push(resultItem);
|
||||||
|
|
||||||
|
if (lazyEvaluation) {
|
||||||
|
if (sliceOffset) {
|
||||||
|
if (i >= startIndex + periods + periods - 1) yield resultItem;
|
||||||
|
}
|
||||||
|
else yield resultItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sliceOffset) return results.slice(startIndex + periods + periods - 1);
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getTrueRange(currItem, prevItem = null) {
|
||||||
|
if (!_.isNull(prevItem) && !_.isObject(currItem)) {
|
||||||
|
throw new Error('ERROR: invalid param, not an object');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.has(currItem, 'high') || !_.has(currItem, 'low') || !_.has(currItem, 'close')) {
|
||||||
|
throw new Error('ERROR: invalid oject property');
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff_1 = (prevItem) ? Math.abs(currItem.high - prevItem.close) : 0;
|
||||||
|
let diff_2 = (prevItem) ? Math.abs(currItem.low - prevItem.close) : 0;
|
||||||
|
let diff_3 = currItem.high - currItem.low;
|
||||||
|
|
||||||
|
return Math.max(diff_1, diff_2, diff_3);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
_calcSmoothedFirstPeriod(items) {
|
||||||
|
let sum_tr = 0;
|
||||||
|
let sum_pdm = 0;
|
||||||
|
let sum_ndm = 0;
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
sum_tr += items[i].tr;
|
||||||
|
sum_pdm += items[i].pdm;
|
||||||
|
sum_ndm += items[i].ndm;
|
||||||
|
}
|
||||||
|
return { tr: sum_tr, pdm: sum_pdm, ndm: sum_ndm };
|
||||||
|
},
|
||||||
|
|
||||||
|
_calcSmoothedSubsequentPeriod(prevItem, tr, pdm, ndm) {
|
||||||
|
let div = this._options.periods;
|
||||||
|
tr = prevItem.trPeriod - (prevItem.trPeriod / div) + tr;
|
||||||
|
pdm = prevItem.pdmPeriod - (prevItem.pdmPeriod / div) + pdm;
|
||||||
|
ndm = prevItem.ndmPeriod - (prevItem.ndmPeriod / div) + ndm;
|
||||||
|
return { tr, pdm, ndm };
|
||||||
|
},
|
||||||
|
|
||||||
|
_calcFirstADX(items, dx) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < items.length; i++) sum += items[i].dx;
|
||||||
|
sum += dx;
|
||||||
|
return sum / this._options.periods;
|
||||||
|
},
|
||||||
|
|
||||||
|
_calcSubsequentADX(prevADX, currentDX) {
|
||||||
|
return (prevADX * (this._options.periods - 1) + currentDX) / this._options.periods;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ADI;
|
||||||
123
lib/indicator/average-true-range.js
Executable file
123
lib/indicator/average-true-range.js
Executable file
@ -0,0 +1,123 @@
|
|||||||
|
'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=20]
|
||||||
|
* @param {number} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function ATR(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: ATR() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
periods: 20,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ATR.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, lazyEvaluation } = this._options;
|
||||||
|
|
||||||
|
let convertToResultItem = (tr, atr) => {
|
||||||
|
return { tr: NumberUtil.roundTo(tr, 2), atr: NumberUtil.roundTo(atr, 2) };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_.isNull(startIndex)) startIndex = 0;
|
||||||
|
if (_.isNull(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
let item = this._collection[i];
|
||||||
|
let prevItem = (i > startIndex) ? this._collection[i - 1] : null;
|
||||||
|
let tr = this._getTrueRange(item, prevItem);
|
||||||
|
let atr = 0;
|
||||||
|
|
||||||
|
if (i < (startIndex + periods - 1)) {
|
||||||
|
//do nothing
|
||||||
|
}
|
||||||
|
else if (i == (startIndex + periods - 1)) {
|
||||||
|
let trCollection = results.slice(startIndex, startIndex + periods + 2);
|
||||||
|
atr = this._calcFirstATR(trCollection);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let prevATR = results[results.length - 1].atr;
|
||||||
|
atr = this._calcRemainingATR(prevATR, tr, periods);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultItem = convertToResultItem(tr, atr);
|
||||||
|
results.push(resultItem);
|
||||||
|
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!lazyEvaluation) return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getTrueRange(currItem, prevItem = null) {
|
||||||
|
if (!_.isNull(prevItem) && !_.isObject(currItem)) {
|
||||||
|
throw new Error('ERROR: invalid param, not an object');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.has(currItem, 'high') || !_.has(currItem, 'low') || !_.has(currItem, 'close')) {
|
||||||
|
throw new Error('ERROR: invalid oject property');
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff_1 = (prevItem) ? NumberUtil.roundTo(Math.abs(currItem.high - prevItem.close), 2) : 0;
|
||||||
|
let diff_2 = (prevItem) ? NumberUtil.roundTo(Math.abs(currItem.low - prevItem.close), 2) : 0;
|
||||||
|
let diff_3 = NumberUtil.roundTo(currItem.high - currItem.low, 2);
|
||||||
|
|
||||||
|
let max = diff_1;
|
||||||
|
if (max < diff_2) max = diff_2;
|
||||||
|
if (max < diff_3) max = diff_3;
|
||||||
|
|
||||||
|
return max;
|
||||||
|
},
|
||||||
|
|
||||||
|
_calcFirstATR(trueRangeCollection) {
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
trueRangeCollection.forEach((item, idx) => {
|
||||||
|
sum += item.tr;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (sum / trueRangeCollection.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
_calcRemainingATR(prevATR, currTR, periods) {
|
||||||
|
return (prevATR * (periods - 1) + currTR) / periods;
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ATR;
|
||||||
97
lib/indicator/bollinger-bands.js
Executable file
97
lib/indicator/bollinger-bands.js
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import MathUtil from './../utils/math.js';
|
||||||
|
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=20]
|
||||||
|
* @param {number} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.sliceOffset=false]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function BB(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: BB() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
periods: 20,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
BB.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;
|
||||||
|
|
||||||
|
if (_.isNull(startIndex)) startIndex = 0;
|
||||||
|
if (_.isNull(endIndex)) endIndex = this._collection.length - 1 - periods;
|
||||||
|
|
||||||
|
let idx = startIndex;
|
||||||
|
let endRangeIndex = this._collection.length - periods;
|
||||||
|
|
||||||
|
if (!sliceOffset) {
|
||||||
|
let i = idx;
|
||||||
|
while (i < startIndex + periods - 1) {
|
||||||
|
let resultItem = {
|
||||||
|
upper: 0, middle: 0, lower: 0, price: this._collection[i],
|
||||||
|
};
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (idx <= endRangeIndex) {
|
||||||
|
let sma = new SMA({ periods, lazyEvaluation: false });
|
||||||
|
let items = this._collection.slice(idx, idx + periods);
|
||||||
|
sma.setValues(items);
|
||||||
|
let r = sma.calculate();
|
||||||
|
let resultItem = { upper: 0, middle: 0, lower: 0, price: this._collection[idx + periods - 1] };
|
||||||
|
let stdDev = NumberUtil.roundTo(MathUtil.standardDeviation(items, "population"), 2);
|
||||||
|
r.forEach((item) => {
|
||||||
|
resultItem.upper = NumberUtil.roundTo(item.sma + (2 * stdDev), 2);
|
||||||
|
resultItem.middle = item.sma;
|
||||||
|
resultItem.lower = NumberUtil.roundTo(item.sma - (2 * stdDev), 2);
|
||||||
|
});
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BB;
|
||||||
117
lib/indicator/exponential-moving-average.js
Executable file
117
lib/indicator/exponential-moving-average.js
Executable file
@ -0,0 +1,117 @@
|
|||||||
|
'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=12]
|
||||||
|
* @param {number} [options.startIndex=0]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.emaResultsOnly=false]
|
||||||
|
* @param {boolean} [option.startWithFirst=false]
|
||||||
|
* @param {boolean} [options.sliceOffset=false]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function EMA(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: EMA() must be called with new');
|
||||||
|
this._options = {
|
||||||
|
startIndex: 0,
|
||||||
|
endIndex: null,
|
||||||
|
periods: 12,
|
||||||
|
emaResultsOnly: false,
|
||||||
|
startWithFirst: false,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
};
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
EMA.prototype = {
|
||||||
|
|
||||||
|
setValues(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error('ERROR: values param is not an array');
|
||||||
|
}
|
||||||
|
this._collection = values;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._collection = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
this._validate(this._collection, this._options);
|
||||||
|
return this._handleGenerator(this._compute());
|
||||||
|
},
|
||||||
|
|
||||||
|
_compute: function* () {
|
||||||
|
let results = [];
|
||||||
|
let firstVal = 0;
|
||||||
|
|
||||||
|
let { startIndex, endIndex, periods, startWithFirst, lazyEvaluation, floating } = this._options;
|
||||||
|
|
||||||
|
let convertToResultItem = (price, ema) => {
|
||||||
|
return { price, ema: NumberUtil.roundTo(ema, 2) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NumberUtil.isNumeric(startIndex)) startIndex = 0;
|
||||||
|
if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
if (startWithFirst) {
|
||||||
|
firstVal = this._collection[startIndex];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
firstVal = this._avg(startIndex, startIndex + periods - 1);
|
||||||
|
if (!this._options.sliceOffset) {
|
||||||
|
for (let i = startIndex; i < startIndex + periods - 1; i++) {
|
||||||
|
let resultItem = convertToResultItem(this._collection[i], 0);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startIndex = startIndex + periods - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultItem = convertToResultItem(this._collection[startIndex], firstVal);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
|
||||||
|
let z = 1;
|
||||||
|
let multiplier = 2 / (periods + 1);
|
||||||
|
for (let i = startIndex + 1; i <= endIndex; i++) {
|
||||||
|
let ema = (this._collection[i] * multiplier) + (results[results.length - 1].ema * (1 - multiplier));
|
||||||
|
let resultItem = convertToResultItem(this._collection[i], ema);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
z++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lazyEvaluation) {
|
||||||
|
if (this._options.emaResultsOnly) return _.pluck(results, 'ema');
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_avg(indexStart, indexEnd) {
|
||||||
|
if (!_.isNumber(indexStart) || (indexStart >= indexEnd)) throw new Error('Error: Invalid indexStart argument');
|
||||||
|
if (!_.isNumber(indexEnd)) throw new Error('Error: Invalid indexEnd argument');
|
||||||
|
|
||||||
|
let coll_len = this._collection.length;
|
||||||
|
if (coll_len - 1 < indexStart || coll_len - 1 < indexEnd) throw new Error('Error: Invalid collection length');
|
||||||
|
|
||||||
|
let n = 0;
|
||||||
|
for (let i = indexStart; i <= indexEnd; i++) n += this._collection[i];
|
||||||
|
return (n / (indexEnd - indexStart + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EMA;
|
||||||
97
lib/indicator/linearly-weighted-moving-average.js
Executable file
97
lib/indicator/linearly-weighted-moving-average.js
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
'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=20]
|
||||||
|
* @param {number} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.sliceOffset=false]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function LWMA(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: LWMA() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
periods: 20,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
}
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
LWMA.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 = (lwma, price) => {
|
||||||
|
return {
|
||||||
|
lwma: NumberUtil.roundTo(lwma, 2),
|
||||||
|
price,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_.isNull(startIndex)) startIndex = 0;
|
||||||
|
if (_.isNull(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
let endRangeIndex = endIndex - periods + 1;
|
||||||
|
|
||||||
|
if (!sliceOffset) {
|
||||||
|
for (let i = startIndex; i < startIndex + periods - 1; i++) {
|
||||||
|
let resultItem = convertToResultItem(0, this._collection[i]);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let itr_i = 0;
|
||||||
|
for (let i = startIndex; i <= endRangeIndex; i++) {
|
||||||
|
let sum_vals = 0;
|
||||||
|
let sum_weight = 0;
|
||||||
|
itr_i++;
|
||||||
|
let itr_j = 0;
|
||||||
|
let lastIdxInPeriod = 0;
|
||||||
|
for (let j = i, len = i + periods; j < len; j++) {
|
||||||
|
let n = itr_i + itr_j;
|
||||||
|
sum_vals += (this._collection[j] * n);
|
||||||
|
sum_weight += n;
|
||||||
|
itr_j++;
|
||||||
|
lastIdxInPeriod = j;
|
||||||
|
}
|
||||||
|
let resultItem = convertToResultItem(sum_vals / sum_weight, this._collection[lastIdxInPeriod]);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LWMA;
|
||||||
141
lib/indicator/money-flow-index.js
Executable file
141
lib/indicator/money-flow-index.js
Executable file
@ -0,0 +1,141 @@
|
|||||||
|
'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;
|
||||||
139
lib/indicator/moving-average-convergence-divergence.js
Executable file
139
lib/indicator/moving-average-convergence-divergence.js
Executable file
@ -0,0 +1,139 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import EMA from './exponential-moving-average.js';
|
||||||
|
import _ from 'underscore';
|
||||||
|
import NumberUtil from '../utils/number.js';
|
||||||
|
import SetOptionsMixin from '../mixin/set-options.js';
|
||||||
|
import HandleGeneratorMixin from '../mixin/handle-generator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {number} [optons.fastPeriods=12]
|
||||||
|
* @param {number} [options.slowPeriods=26]
|
||||||
|
* @param {number} [options.signalPeriods=9]
|
||||||
|
* @param {boolean} [options.sliceOffset=false]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function MACD(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: MACD() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
fastPeriods: 12,
|
||||||
|
slowPeriods: 26,
|
||||||
|
signalPeriods: 9,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
MACD.prototype = {
|
||||||
|
|
||||||
|
setValues(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error('ERROR: values param is not an array');
|
||||||
|
}
|
||||||
|
this._collection = values;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._collection = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
this._validate();
|
||||||
|
return this._handleAsyncGenerator(this._compute());
|
||||||
|
},
|
||||||
|
|
||||||
|
_validate() {
|
||||||
|
let { slowPeriods, fastPeriods } = this._options;
|
||||||
|
if (slowPeriods < fastPeriods) {
|
||||||
|
throw new Error('ERROR: slowPeriods option must be higher than fastPeriods');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_compute: async function* () {
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
let { slowPeriods, fastPeriods, signalPeriods, sliceOffset, lazyEvaluation } = this._options;
|
||||||
|
|
||||||
|
let results_slow = [];
|
||||||
|
let results_fast_orig = [];
|
||||||
|
|
||||||
|
let slow_ema = new EMA({ periods: slowPeriods, lazyEvaluation, sliceOffset: true });
|
||||||
|
let fast_ema = new EMA({ periods: fastPeriods, lazyEvaluation, sliceOffset: true });
|
||||||
|
|
||||||
|
slow_ema.setValues(self._collection);
|
||||||
|
fast_ema.setValues(self._collection);
|
||||||
|
|
||||||
|
let res_1 = await slow_ema.calculate();
|
||||||
|
let res_2 = await fast_ema.calculate();
|
||||||
|
|
||||||
|
for (let i = 0, len = Math.max(res_1.length, res_2.length); i < len; i++) {
|
||||||
|
if (i < res_1.length) results_slow.push(res_1[i].ema);
|
||||||
|
if (i < res_2.length) results_fast_orig.push(res_2[i].ema);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dif = slowPeriods - fastPeriods;
|
||||||
|
|
||||||
|
let results_fast = results_fast_orig.slice(dif);
|
||||||
|
|
||||||
|
if (results_fast.length != results_slow.length) {
|
||||||
|
throw new Error('ERROR: arrays have not same length');
|
||||||
|
}
|
||||||
|
|
||||||
|
let results_macd = [];
|
||||||
|
|
||||||
|
for (let i = 0, len = results_slow.length; i < len; i++) {
|
||||||
|
let dif = results_fast[i] - results_slow[i];
|
||||||
|
results_macd.push(NumberUtil.roundTo(dif, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
let signal_ema = new EMA({ periods: signalPeriods, lazyEvaluation, sliceOffset: true });
|
||||||
|
|
||||||
|
signal_ema.setValues(results_macd);
|
||||||
|
|
||||||
|
let res_3 = await signal_ema.calculate();
|
||||||
|
|
||||||
|
let prices = this._collection;
|
||||||
|
|
||||||
|
let results_signal = [];
|
||||||
|
for (let i = 0; i < res_3.length; i++) {
|
||||||
|
results_signal.push(res_3[i].ema);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sliceOffset) {
|
||||||
|
for (let i = 0; i < slowPeriods + signalPeriods - 2; i++) {
|
||||||
|
if (i < fastPeriods - 1) results_fast_orig.unshift(0);
|
||||||
|
if (i < slowPeriods - 1) {
|
||||||
|
results_slow.unshift(0);
|
||||||
|
results_macd.unshift(0);
|
||||||
|
}
|
||||||
|
results_signal.unshift(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
results_slow = results_slow.slice(signalPeriods - 1);
|
||||||
|
results_fast = results_fast.slice(signalPeriods - 1);
|
||||||
|
results_macd = results_macd.slice(signalPeriods - 1);
|
||||||
|
prices = this._collection.slice(slowPeriods + signalPeriods - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {
|
||||||
|
slow_ema: results_slow,
|
||||||
|
fast_ema: (!sliceOffset) ? results_fast_orig : results_fast,
|
||||||
|
signal_ema: results_signal,
|
||||||
|
macd: results_macd,
|
||||||
|
prices,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MACD;
|
||||||
92
lib/indicator/on-balance-volume.js
Executable file
92
lib/indicator/on-balance-volume.js
Executable file
@ -0,0 +1,92 @@
|
|||||||
|
'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} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function OBV(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: OBV() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
OBV.prototype = {
|
||||||
|
|
||||||
|
setValues(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error('ERROR: values param is not an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
values.forEach((item) => {
|
||||||
|
if (_.has(item, 'price') && _.has(item, 'volume')) {
|
||||||
|
let { price, volume } = item;
|
||||||
|
this._collection.push({ price, volume });
|
||||||
|
}
|
||||||
|
else throw new Error('ERROR: Invalid value');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
this._validate(this._collection, this._options);
|
||||||
|
return this._handleGenerator(this._compute());
|
||||||
|
},
|
||||||
|
|
||||||
|
_compute: function* () {
|
||||||
|
let result = [];
|
||||||
|
let sum = 0;
|
||||||
|
let { startIndex, endIndex, lazyEvaluation } = this._options;
|
||||||
|
|
||||||
|
if (!NumberUtil.isNumeric(startIndex)) startIndex = 0;
|
||||||
|
if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
let collection = this._collection.slice(startIndex, endIndex + 1);
|
||||||
|
|
||||||
|
for (let i = 0; i < collection.length; i++) {
|
||||||
|
let currItem = collection[i];
|
||||||
|
|
||||||
|
let resultItem = {
|
||||||
|
obv: 0,
|
||||||
|
price: currItem.price,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!i) {
|
||||||
|
result.push(resultItem);
|
||||||
|
yield resultItem;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevItem = collection[i - 1];
|
||||||
|
|
||||||
|
if (prevItem.price > currItem.price) sum -= currItem.volume;
|
||||||
|
else if (prevItem.price < currItem.price) sum += currItem.volume;
|
||||||
|
|
||||||
|
resultItem.obv = sum;
|
||||||
|
result.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OBV;
|
||||||
211
lib/indicator/parabolic-stop-and-reverse.js
Executable file
211
lib/indicator/parabolic-stop-and-reverse.js
Executable file
@ -0,0 +1,211 @@
|
|||||||
|
'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;
|
||||||
79
lib/indicator/rate-of-change.js
Executable file
79
lib/indicator/rate-of-change.js
Executable file
@ -0,0 +1,79 @@
|
|||||||
|
'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 ROC(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: ROC() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
periods: 14,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
};
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ROC.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 { startIndex, endIndex, periods, lazyEvaluation, sliceOffset } = this._options;
|
||||||
|
|
||||||
|
if (!NumberUtil.isNumeric(startIndex)) startIndex = 0;
|
||||||
|
if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
let resultItem = null;
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
let currentPrice = this._collection[i];
|
||||||
|
if (i <= startIndex + periods - 1) {
|
||||||
|
if (!sliceOffset) {
|
||||||
|
resultItem = { roc: 0, price: currentPrice };
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let prevPrice = this._collection[i - periods];
|
||||||
|
let roc = ((currentPrice - prevPrice) / prevPrice) * 100;
|
||||||
|
resultItem = { roc: NumberUtil.roundTo(roc, 2), price: currentPrice };
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ROC;
|
||||||
140
lib/indicator/relative-strength-index.js
Executable file
140
lib/indicator/relative-strength-index.js
Executable file
@ -0,0 +1,140 @@
|
|||||||
|
'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 RSI(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: RSI() 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 = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
RSI.prototype = {
|
||||||
|
|
||||||
|
setValues(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error('ERROR: values param is not an array');
|
||||||
|
}
|
||||||
|
this._collection = values;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._collection = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
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 = (price, gain = null, loss = null, avg_gain = null, avg_loss = null) => {
|
||||||
|
let rs = null;
|
||||||
|
let rsi = null;
|
||||||
|
|
||||||
|
if (NumberUtil.isNumeric(avg_gain) && NumberUtil.isNumeric(avg_loss)) {
|
||||||
|
rs = avg_gain / avg_loss;
|
||||||
|
rsi = 100 - (100 / (1 + rs));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
price,
|
||||||
|
gain: NumberUtil.isNumeric(gain) ? NumberUtil.roundTo(gain, 2) : null,
|
||||||
|
loss: NumberUtil.isNumeric(loss) ? NumberUtil.roundTo(loss, 2) : null,
|
||||||
|
avg_gain: NumberUtil.isNumeric(avg_gain) ? NumberUtil.roundTo(avg_gain, 2) : null,
|
||||||
|
avg_loss: NumberUtil.isNumeric(avg_loss) ? NumberUtil.roundTo(avg_loss, 2) : null,
|
||||||
|
rs: NumberUtil.isNumeric(rs) ? NumberUtil.roundTo(rs, 2) : null,
|
||||||
|
rsi: NumberUtil.isNumeric(rsi) ? NumberUtil.roundTo(rsi, 2) : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!NumberUtil.isNumeric(startIndex)) startIndex = 0;
|
||||||
|
if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
let sumPeriodGain = 0;
|
||||||
|
let sumPeriodLoss = 0;
|
||||||
|
|
||||||
|
for (let i = startIndex, endPeriodIndex = startIndex + periods - 1; i <= endPeriodIndex; i++) {
|
||||||
|
let resultItem = null;
|
||||||
|
|
||||||
|
if (i == startIndex) {
|
||||||
|
resultItem = convertToResultItem(this._collection[i], 0, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let { gain, loss } = this._calcGainLoss(i);
|
||||||
|
sumPeriodGain += gain;
|
||||||
|
sumPeriodLoss += loss;
|
||||||
|
resultItem = convertToResultItem(this._collection[i], gain, loss);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sliceOffset) {
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ag = sumPeriodGain / periods;
|
||||||
|
let al = sumPeriodLoss / periods;
|
||||||
|
let { gain, loss } = this._calcGainLoss(startIndex + periods);
|
||||||
|
let resultItem = convertToResultItem(this._collection[startIndex + periods], gain, loss, ag, al);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
|
||||||
|
for (let i = startIndex + periods + 1; i <= endIndex; i++) {
|
||||||
|
let { gain, loss } = this._calcGainLoss(i);
|
||||||
|
let prev_avg_gain = results[results.length - 1].avg_gain;
|
||||||
|
let prev_avg_loss = results[results.length - 1].avg_loss;
|
||||||
|
let ag = ((prev_avg_gain * (periods - 1)) + gain) / periods;
|
||||||
|
let al = ((prev_avg_loss * (periods - 1)) + loss) / periods;
|
||||||
|
let resultItem = convertToResultItem(this._collection[i], gain, loss, ag, al);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lazyEvaluation) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_calcGainLoss(collectionIndex) {
|
||||||
|
let result = { gain: 0, loss: 0 };
|
||||||
|
if (collectionIndex >= 0 && collectionIndex < this._collection.length) {
|
||||||
|
if (collectionIndex > 0) {
|
||||||
|
let diff = this._collection[collectionIndex] - this._collection[collectionIndex - 1];
|
||||||
|
if (diff > 0) result.gain = diff;
|
||||||
|
else result.loss = Math.abs(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else throw new Error('ERROR: index is outside the collection range');
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RSI;
|
||||||
110
lib/indicator/simple-moving-average.js
Executable file
110
lib/indicator/simple-moving-average.js
Executable file
@ -0,0 +1,110 @@
|
|||||||
|
'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=10]
|
||||||
|
* @param {number} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.sliceOffset=false]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function SMA(options = {}) {
|
||||||
|
if (!new.target) {
|
||||||
|
throw new Error('ERROR: SMA() must be called with new');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
periods: 10,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
SMA.prototype = {
|
||||||
|
|
||||||
|
setValues(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error('ERROR: values param is not an array');
|
||||||
|
}
|
||||||
|
this._collection = values;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._collection = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
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 = (price, sma) => {
|
||||||
|
return { price, sma: NumberUtil.roundTo(sma, 2) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isNull(startIndex)) startIndex = 0;
|
||||||
|
if (_.isNull(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
if (startIndex >= endIndex) {
|
||||||
|
throw new Error('ERROR: startIndex option must be lower than endIndex');
|
||||||
|
}
|
||||||
|
|
||||||
|
let cnt = (endIndex + 1) - (startIndex + 1) - periods + 2;
|
||||||
|
if (cnt <= 0) {
|
||||||
|
throw new Error('ERROR: Invalid range length');
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = startIndex;
|
||||||
|
let endRangeIndex = endIndex - periods + 1;
|
||||||
|
|
||||||
|
if (!sliceOffset) {
|
||||||
|
for (let i = startIndex; i < startIndex + periods - 1; i++) {
|
||||||
|
let resultItem = convertToResultItem(this._collection[i], 0);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (idx <= endRangeIndex) {
|
||||||
|
let endPeriodIndex = idx + periods - 1;
|
||||||
|
let avg = this._avg(idx, endPeriodIndex);
|
||||||
|
let resultItem = convertToResultItem(this._collection[endPeriodIndex], avg);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lazyEvaluation) yield results;
|
||||||
|
},
|
||||||
|
|
||||||
|
_avg(startIndex, endIndex) {
|
||||||
|
let sum = 0;
|
||||||
|
let cnt = 0;
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
sum += this._collection[i];
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
return (parseFloat(sum) / cnt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SMA;
|
||||||
107
lib/indicator/smoothed-moving-average.js
Executable file
107
lib/indicator/smoothed-moving-average.js
Executable file
@ -0,0 +1,107 @@
|
|||||||
|
'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';
|
||||||
|
import SMA from './simple-moving-average.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {number} [optons.periods=20]
|
||||||
|
* @param {number} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.sliceOffset=false]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function SMMA(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: SMMA() must be called with new');
|
||||||
|
this._options = {
|
||||||
|
periods: 20,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
};
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
SMMA.prototype = {
|
||||||
|
|
||||||
|
setValues(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error('ERROR: values param is not an array');
|
||||||
|
}
|
||||||
|
this._collection = values;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._collection = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
this._validate(this._collection, this._options);
|
||||||
|
return this._handleGenerator(this._compute());
|
||||||
|
},
|
||||||
|
|
||||||
|
_compute: function* () {
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
let convertToResultItem = (price, value) => {
|
||||||
|
return {
|
||||||
|
price: price,
|
||||||
|
smma: NumberUtil.roundTo(value, 2),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let { periods, 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 - 1; i++) {
|
||||||
|
let resultItem = convertToResultItem(this._collection[i], 0);
|
||||||
|
results.push(resultItem);
|
||||||
|
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sma = new SMA({ periods, startIndex, endIndex: startIndex + periods - 1, lazyEvaluation: false, sliceOffset: true });
|
||||||
|
sma.setValues(this._collection);
|
||||||
|
let smaResults = sma.calculate();
|
||||||
|
let smma1 = smaResults[0].sma;
|
||||||
|
|
||||||
|
let resultItem = convertToResultItem(smaResults[0].price, smma1);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
|
||||||
|
let price = this._collection[startIndex + periods];
|
||||||
|
let smma2 = (smma1 * (periods - 1) + price) / periods;
|
||||||
|
resultItem = convertToResultItem(price, smma2);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
|
||||||
|
for (let i = startIndex + periods + 1; i <= endIndex; i++) {
|
||||||
|
let prevSMMA = results[results.length - 1].smma;
|
||||||
|
let prevSum = prevSMMA * periods;
|
||||||
|
let currentPrice = this._collection[i];
|
||||||
|
let smma = (prevSum - prevSMMA + currentPrice) / periods;
|
||||||
|
let resultItem = convertToResultItem(currentPrice, smma);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SMMA;
|
||||||
139
lib/indicator/stochastic-oscillator.js
Executable file
139
lib/indicator/stochastic-oscillator.js
Executable file
@ -0,0 +1,139 @@
|
|||||||
|
'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;
|
||||||
68
lib/indicator/triple-exponential-average.1.js
Executable file
68
lib/indicator/triple-exponential-average.1.js
Executable file
@ -0,0 +1,68 @@
|
|||||||
|
'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';
|
||||||
|
import EMA from './exponential-moving-average.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {number} [optons.periods=15]
|
||||||
|
* @param {number} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.sliceOffset=false]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function TRIX(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: TRIX() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
periods: 15,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
TRIX.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 singleEMA = new EMA({ periods, lazyEvaluation: false });
|
||||||
|
singleEMA.setValues(this._collection);
|
||||||
|
let singleEmaResults = singleEMA.calculate();
|
||||||
|
console.log(singleEmaResults);
|
||||||
|
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TRIX;
|
||||||
65
lib/indicator/triple-exponential-average.js
Executable file
65
lib/indicator/triple-exponential-average.js
Executable file
@ -0,0 +1,65 @@
|
|||||||
|
'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';
|
||||||
|
import EMA from './exponential-moving-average.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {number} [optons.periods=15]
|
||||||
|
* @param {number} [options.startIndex]
|
||||||
|
* @param {number} [options.endIndex}]
|
||||||
|
* @param {boolean} [options.sliceOffset=false]
|
||||||
|
* @param {boolean} [options.lazyEvaluation=true]
|
||||||
|
* @param {number} [option.maxTickDuration=10]
|
||||||
|
*/
|
||||||
|
function TRIX(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: TRIX() must be called with new');
|
||||||
|
|
||||||
|
this._options = {
|
||||||
|
periods: 15,
|
||||||
|
startIndex: null,
|
||||||
|
endIndex: null,
|
||||||
|
sliceOffset: false,
|
||||||
|
lazyEvaluation: true,
|
||||||
|
maxTickDuration: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = [SetOptionsMixin, ValidateMixin, HandleGeneratorMixin];
|
||||||
|
Object.assign(this, ...m);
|
||||||
|
this.setOptions(options);
|
||||||
|
this._collection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TRIX.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 singleEMA = new EMA({ periods, lazyEvaluation: false });
|
||||||
|
singleEMA.setValues(this._collection);
|
||||||
|
let singleEmaResults = singleEMA.calculate();
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TRIX;
|
||||||
102
lib/indicator/weighted-moving-average.js
Executable file
102
lib/indicator/weighted-moving-average.js
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
'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 WMA(options = {}) {
|
||||||
|
if (!new.target) throw new Error('ERROR: WMA() 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 = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
WMA.prototype = {
|
||||||
|
|
||||||
|
setValues(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error('ERROR: values param is not an array');
|
||||||
|
}
|
||||||
|
this._collection = values;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._collection = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
this._validate(this._collection, this._options);
|
||||||
|
return this._handleGenerator(this._compute());
|
||||||
|
},
|
||||||
|
|
||||||
|
_compute: function* () {
|
||||||
|
let results = [];
|
||||||
|
let weight = this._getWeight();
|
||||||
|
|
||||||
|
let convertToResultItem = (price, value) => {
|
||||||
|
return {
|
||||||
|
price,
|
||||||
|
wma: NumberUtil.roundTo(value, 2),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let { startIndex, endIndex, periods, sliceOffset, lazyEvaluation } = this._options;
|
||||||
|
|
||||||
|
if (!NumberUtil.isNumeric(startIndex)) startIndex = 0;
|
||||||
|
if (!NumberUtil.isNumeric(endIndex)) endIndex = this._collection.length - 1;
|
||||||
|
|
||||||
|
let resultItem = null;
|
||||||
|
|
||||||
|
if (!sliceOffset) {
|
||||||
|
for (let i = startIndex; i < startIndex + periods - 1; i++) {
|
||||||
|
resultItem = convertToResultItem(this._collection[i], 0);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startIndex + periods - 1; i <= endIndex; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
let cnt = 1;
|
||||||
|
for (let j = i - periods + 1; j <= i; j++) {
|
||||||
|
sum += this._collection[j] * cnt;
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
resultItem = convertToResultItem(this._collection[i], sum / weight);
|
||||||
|
results.push(resultItem);
|
||||||
|
if (lazyEvaluation) yield resultItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getWeight() {
|
||||||
|
let weight = 1;
|
||||||
|
let cnt = 1;
|
||||||
|
while (cnt < this._options.periods) weight += ++cnt;
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WMA;
|
||||||
64
lib/mixin/handle-generator.js
Executable file
64
lib/mixin/handle-generator.js
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import convertHrtime from 'convert-hrtime';
|
||||||
|
import TypeUtil from '../utils/type.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
_handleGenerator(gen) {
|
||||||
|
if (!this._options.lazyEvaluation) return gen.next().value;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let results = [];
|
||||||
|
let maxTickDuration = this._options.maxTickDuration || 10;
|
||||||
|
|
||||||
|
(function handler() {
|
||||||
|
let startHRTime = process.hrtime();
|
||||||
|
let obj = gen.next();
|
||||||
|
let nextCall = false;
|
||||||
|
while (!obj.done) {
|
||||||
|
results.push(obj.value);
|
||||||
|
let { milliseconds } = convertHrtime(process.hrtime(startHRTime));
|
||||||
|
if (milliseconds < maxTickDuration) {
|
||||||
|
obj = gen.next();
|
||||||
|
} else {
|
||||||
|
nextCall = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(nextCall) ? setImmediate(() => handler()) : resolve(results);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleAsyncGenerator(gen) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let results = [];
|
||||||
|
let maxTickDuration = this._options.maxTickDuration || 0.5;
|
||||||
|
|
||||||
|
(function handler() {
|
||||||
|
let startHRTime = process.hrtime();
|
||||||
|
(async function loop() {
|
||||||
|
try {
|
||||||
|
let obj = await gen.next();
|
||||||
|
if (!obj.done) {
|
||||||
|
results.push(obj.value);
|
||||||
|
let { milliseconds } = convertHrtime(process.hrtime(startHRTime));
|
||||||
|
if (milliseconds < maxTickDuration) loop();
|
||||||
|
else setImmediate(() => handler());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!TypeUtil.isUndefined(obj.value)) {
|
||||||
|
results.push(obj.value);
|
||||||
|
}
|
||||||
|
resolve(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
16
lib/mixin/set-options.js
Executable file
16
lib/mixin/set-options.js
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import _ from 'underscore';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setOptions(options) {
|
||||||
|
if (!_.isObject(options)) {
|
||||||
|
throw new Error('ERROR: Invalid Options argument');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isEmpty(options)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._options = _.extend({}, this._options, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
lib/mixin/validate.js
Executable file
71
lib/mixin/validate.js
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import NumberUtil from '../utils/number.js';
|
||||||
|
import _ from 'underscore';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
_validate(collection = null, options = null) {
|
||||||
|
|
||||||
|
let hasCollection = false;
|
||||||
|
let hasOptions = false;
|
||||||
|
|
||||||
|
if (Array.isArray(collection)) {
|
||||||
|
hasCollection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isObject(options)) {
|
||||||
|
hasOptions = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCollection && _.isEmpty(collection)) {
|
||||||
|
throw new Error('ERROR: No data');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOptions && hasCollection && _.has(options, 'periods')) {
|
||||||
|
if (options.periods > collection.length) {
|
||||||
|
throw new Error('ERROR: periods option must be lower than values list length');
|
||||||
|
}
|
||||||
|
if (!NumberUtil.isNumeric(options.periods) || options.periods <= 1) {
|
||||||
|
throw new Error('ERROR: Invalid periods options');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOptions && _.has(options, 'startIndex')) {
|
||||||
|
if (!_.isNull(options.startIndex) && !NumberUtil.isNumeric(options.startIndex)) {
|
||||||
|
throw new Error('ERROR: invalid startIndex option type');
|
||||||
|
}
|
||||||
|
if (NumberUtil.isNumeric(options.startIndex) && options.startIndex < 0) {
|
||||||
|
throw new Error('ERROR: starIndex option must be greater or equal 0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOptions && _.has(options, 'endIndex')) {
|
||||||
|
if (!_.isNull(options.endIndex) && !NumberUtil.isNumeric(options.endIndex)) {
|
||||||
|
throw new Error('ERROR: invalid endIndex option type');
|
||||||
|
}
|
||||||
|
if (NumberUtil.isNumeric(options.endIndex) && options.endIndex < 0) {
|
||||||
|
throw new Error('ERROR: endIndex option must be greater or equal 0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOptions && _.has(options, 'startIndex') && _.has(options, 'endIndex')) {
|
||||||
|
|
||||||
|
if (!_.isNull(options.startIndex) && !_.isNull(options.endIndex) && options.startIndex >= options.endIndex) {
|
||||||
|
throw new Error('ERROR: startIndex must be lower than endIndex');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCollection) {
|
||||||
|
if (!_.isNull(options.startIndex) && options.startIndex > (collection.length - 1)) {
|
||||||
|
throw new Error('ERROR: startIndex out of range');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isNull(options.endIndex) && options.endIndex > (collection.length - 1)) {
|
||||||
|
throw new Error('ERROR: endIndex out of range');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
85
lib/utils/math.js
Executable file
85
lib/utils/math.js
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
max(values) {
|
||||||
|
return Math.max.apply(null, values);
|
||||||
|
},
|
||||||
|
|
||||||
|
min(values) {
|
||||||
|
return Math.min.apply(null, values);
|
||||||
|
},
|
||||||
|
|
||||||
|
range(values) {
|
||||||
|
return this.max(values) - this.min(values);
|
||||||
|
},
|
||||||
|
|
||||||
|
midrange(values) {
|
||||||
|
return this.range(values) / 3;
|
||||||
|
},
|
||||||
|
|
||||||
|
sum(values) {
|
||||||
|
let num = 0;
|
||||||
|
let len = values.length;
|
||||||
|
for (let i = 0; i < len; i++)num += values[i];
|
||||||
|
return num;
|
||||||
|
},
|
||||||
|
|
||||||
|
mean(values) {
|
||||||
|
return (this.sum(values) / values.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
meanDeviation(values) {
|
||||||
|
let mean = this.mean(values);
|
||||||
|
let distance = [];
|
||||||
|
values.forEach((val) => distance.push(Math.abs(val - mean)));
|
||||||
|
return this.mean(distance);
|
||||||
|
},
|
||||||
|
|
||||||
|
average(values) {
|
||||||
|
return this.mean(values);
|
||||||
|
},
|
||||||
|
|
||||||
|
median(values, doSortItems = true) {
|
||||||
|
if (doSortItems) values.sort((a, b) => a - b);
|
||||||
|
let mid = values.length / 2;
|
||||||
|
return mid % 1 ? values[mid - 0.5] : (values[mid - 1] + values[mid]) / 2;
|
||||||
|
},
|
||||||
|
|
||||||
|
modes(values) {
|
||||||
|
let modeMap = {};
|
||||||
|
values.forEach((val) => !modeMap[val] ? modeMap[val] = 1 : modeMap[val]++);
|
||||||
|
let modes = [];
|
||||||
|
let maxCount = 0;
|
||||||
|
for (let key in modeMap) {
|
||||||
|
if (modeMap[key] < maxCount) continue;
|
||||||
|
if (modeMap[key] > maxCount) {
|
||||||
|
modes = [Number(key)];
|
||||||
|
maxCount = modeMap[key];
|
||||||
|
}
|
||||||
|
else if (modeMap[key] == maxCount) {
|
||||||
|
modes.push(Number(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modes;
|
||||||
|
},
|
||||||
|
|
||||||
|
variance(values, mode = "sample") {
|
||||||
|
mode = (mode.toLowerCase() == "sample") ? "sample" : "population";
|
||||||
|
|
||||||
|
let av = this.average(values);
|
||||||
|
let result = [];
|
||||||
|
values.forEach((num) => {
|
||||||
|
let diff = num - av;
|
||||||
|
result.push(Math.pow(diff, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (mode == 'sample') ? this.sum(result) / (result.length - 1) : this.average(result);
|
||||||
|
},
|
||||||
|
|
||||||
|
standardDeviation(values, mode = "sample") {
|
||||||
|
mode = (mode.toLowerCase() == "sample") ? "sample" : "population";
|
||||||
|
return Math.sqrt(this.variance(values, mode));
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
15
lib/utils/misc.js
Executable file
15
lib/utils/misc.js
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
has(object, key) {
|
||||||
|
return object ? hasOwnProperty.call(object, key) : false;
|
||||||
|
},
|
||||||
|
|
||||||
|
extends() {
|
||||||
|
let args = Array.prototype.slice.call(arguments);
|
||||||
|
args.unshift({});
|
||||||
|
return Object.assign.apply(null, Array.prototype.slice.call(arguments));
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
20
lib/utils/number.js
Executable file
20
lib/utils/number.js
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import bankersRounding from "bankers-rounding";
|
||||||
|
|
||||||
|
const isNumeric = (num) => {
|
||||||
|
if (num === null && typeof num === "object") return false;
|
||||||
|
return !isNaN(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
const roundTo = (num, places, type = 'bankers') => {
|
||||||
|
if (type == 'bankers') {
|
||||||
|
return bankersRounding(num, places);
|
||||||
|
}
|
||||||
|
return +(Math.round(num + "e+" + places) + "e-" + places);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isNumeric,
|
||||||
|
roundTo,
|
||||||
|
}
|
||||||
39
lib/utils/type.js
Executable file
39
lib/utils/type.js
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isNull(value) {
|
||||||
|
return (value === null && typeof value === "object");
|
||||||
|
},
|
||||||
|
|
||||||
|
isNumber(value) {
|
||||||
|
return Number.isFinite(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
isString(value) {
|
||||||
|
return (Object.prototype.toString.call(value) == '[object String]');
|
||||||
|
},
|
||||||
|
|
||||||
|
isObject(obj) {
|
||||||
|
return !this.isArray(obj) && (typeof obj === 'object' && !!obj);
|
||||||
|
},
|
||||||
|
|
||||||
|
isBoolean(obj) {
|
||||||
|
return (obj === true || obj === false || toString.call(obj) === '[object Boolean]');
|
||||||
|
},
|
||||||
|
|
||||||
|
isUndefined(obj) {
|
||||||
|
return (obj === void 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
isNaN(obj) {
|
||||||
|
return Number.isNaN(obj);
|
||||||
|
},
|
||||||
|
|
||||||
|
isArray(obj) {
|
||||||
|
return Array.isArray(obj);
|
||||||
|
},
|
||||||
|
|
||||||
|
isFunction(obj) {
|
||||||
|
return typeof obj == 'function' || false;
|
||||||
|
},
|
||||||
|
}
|
||||||
1668
package-lock.json
generated
Normal file
1668
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
package.json
Executable file
57
package.json
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "stockindicators",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Toolkit with various indicators and oscillators for the technical stock analysis",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "cross-env DEBUG=app:* NODE_ENV=testing mocha ./tests.js",
|
||||||
|
"test:coverage": "c8 --reporter=html --reporter=text npm run test"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"underscore": "~1.9.0",
|
||||||
|
"bankers-rounding": "~0.1.0",
|
||||||
|
"convert-hrtime": "~5.0.0",
|
||||||
|
"cross-env": "~7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"mocha": "~9.2.0",
|
||||||
|
"chai": "~4.4.0",
|
||||||
|
"chai-fs": "~2.0.0",
|
||||||
|
"chai-as-promised": "~7.1.0",
|
||||||
|
"c8": "~9.1.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"average-true-range",
|
||||||
|
"linearly-weighted-moving-average",
|
||||||
|
"moving-average-convergence-divergence",
|
||||||
|
"simple-moving-average",
|
||||||
|
"exponential-moving-average",
|
||||||
|
"stochastic-oscillator",
|
||||||
|
"bollinger-bands",
|
||||||
|
"on-balance-volume",
|
||||||
|
"relative-strength-index",
|
||||||
|
"weighted-moving-average",
|
||||||
|
"rate-of-change",
|
||||||
|
"money-flow-index",
|
||||||
|
"standard-deviation",
|
||||||
|
"fintech",
|
||||||
|
"financial-technology",
|
||||||
|
"stock",
|
||||||
|
"math",
|
||||||
|
"technical-analysis",
|
||||||
|
"finance",
|
||||||
|
"invest",
|
||||||
|
"trading",
|
||||||
|
"indicator",
|
||||||
|
"oscillator",
|
||||||
|
"algorithmic",
|
||||||
|
"nyse",
|
||||||
|
"quant"
|
||||||
|
],
|
||||||
|
"author": "Lukas B",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
73
test/fixtures/average-true-range.js
vendored
Executable file
73
test/fixtures/average-true-range.js
vendored
Executable file
@ -0,0 +1,73 @@
|
|||||||
|
const data = [
|
||||||
|
//High, Low, Close
|
||||||
|
[48.70, 47.79, 48.16],
|
||||||
|
[48.72, 48.14, 48.61],
|
||||||
|
[48.90, 48.39, 48.75],
|
||||||
|
[48.87, 48.37, 48.63],
|
||||||
|
[48.82, 48.24, 48.74],
|
||||||
|
[49.05, 48.64, 49.03],
|
||||||
|
[49.20, 48.94, 49.07],
|
||||||
|
[49.35, 48.86, 49.32],
|
||||||
|
[49.92, 49.50, 49.91],
|
||||||
|
[50.19, 49.87, 50.13],
|
||||||
|
[50.12, 49.20, 49.53],
|
||||||
|
[49.66, 48.90, 49.50],
|
||||||
|
[49.88, 49.43, 49.75],
|
||||||
|
[50.19, 49.73, 50.03],
|
||||||
|
[50.36, 49.26, 50.31],
|
||||||
|
[50.57, 50.09, 50.52],
|
||||||
|
[50.65, 50.30, 50.41],
|
||||||
|
[50.43, 49.21, 49.34],
|
||||||
|
[49.63, 48.98, 49.37],
|
||||||
|
[50.33, 49.61, 50.23],
|
||||||
|
[50.29, 49.20, 49.24],
|
||||||
|
[50.17, 49.43, 49.93],
|
||||||
|
[49.32, 48.08, 48.43],
|
||||||
|
[48.50, 47.64, 48.18],
|
||||||
|
[48.32, 41.55, 46.57],
|
||||||
|
[46.80, 44.28, 45.41],
|
||||||
|
[47.80, 47.31, 47.77],
|
||||||
|
[48.39, 47.20, 47.72],
|
||||||
|
[48.66, 47.90, 48.62],
|
||||||
|
[48.79, 47.73, 47.85],
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataResults = [
|
||||||
|
//H-L, H-Cp, L-Cp, TR, ATR
|
||||||
|
[0.91, 0.00, 0.00, 0.91, 0.00],
|
||||||
|
[0.58, 0.56, 0.02, 0.58, 0.00],
|
||||||
|
[0.51, 0.29, 0.22, 0.51, 0.00],
|
||||||
|
[0.50, 0.12, 0.38, 0.50, 0.00],
|
||||||
|
[0.58, 0.19, 0.39, 0.58, 0.00],
|
||||||
|
[0.41, 0.31, 0.11, 0.41, 0.00],
|
||||||
|
[0.26, 0.17, 0.09, 0.26, 0.00],
|
||||||
|
[0.49, 0.28, 0.21, 0.49, 0.00],
|
||||||
|
[0.42, 0.60, 0.18, 0.60, 0.00],
|
||||||
|
[0.32, 0.28, 0.04, 0.32, 0.00],
|
||||||
|
[0.92, 0.01, 0.93, 0.93, 0.00],
|
||||||
|
[0.76, 0.13, 0.63, 0.76, 0.00],
|
||||||
|
[0.45, 0.38, 0.07, 0.45, 0.00],
|
||||||
|
[0.46, 0.44, 0.02, 0.46, 0.56],
|
||||||
|
[1.10, 0.33, 0.77, 1.10, 0.59],
|
||||||
|
[0.48, 0.26, 0.22, 0.48, 0.59],
|
||||||
|
[0.35, 0.13, 0.22, 0.35, 0.57],
|
||||||
|
[1.22, 0.02, 1.20, 1.22, 0.62],
|
||||||
|
[0.65, 0.29, 0.36, 0.65, 0.62],
|
||||||
|
[0.72, 0.96, 0.24, 0.96, 0.64],
|
||||||
|
[1.09, 0.06, 1.03, 1.09, 0.67],
|
||||||
|
[0.74, 0.93, 0.19, 0.93, 0.69],
|
||||||
|
[1.24, 0.61, 1.85, 1.85, 0.78],
|
||||||
|
[0.86, 0.07, 0.79, 0.86, 0.78],
|
||||||
|
[6.77, 0.14, 6.63, 6.77, 1.21],
|
||||||
|
[2.52, 0.23, 2.29, 2.52, 1.30],
|
||||||
|
[0.49, 2.39, 1.90, 2.39, 1.38],
|
||||||
|
[1.19, 0.62, 0.57, 1.19, 1.37],
|
||||||
|
[0.76, 0.94, 0.18, 0.94, 1.34],
|
||||||
|
[1.06, 0.17, 0.89, 1.06, 1.32],
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data,
|
||||||
|
dataResults,
|
||||||
|
}
|
||||||
71
test/fixtures/moving-average-convergence-divergence.js
vendored
Executable file
71
test/fixtures/moving-average-convergence-divergence.js
vendored
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
const data = [
|
||||||
|
39.92,
|
||||||
|
40.32,
|
||||||
|
40.12,
|
||||||
|
37.72,
|
||||||
|
36.01,
|
||||||
|
34.66,
|
||||||
|
35.40,
|
||||||
|
36.78,
|
||||||
|
35.43,
|
||||||
|
38.68,
|
||||||
|
39.76,
|
||||||
|
40.45,
|
||||||
|
41.30,
|
||||||
|
42.35,
|
||||||
|
43.21,
|
||||||
|
42.34,
|
||||||
|
42.47,
|
||||||
|
41.97,
|
||||||
|
41.61,
|
||||||
|
42.27,
|
||||||
|
43.07,
|
||||||
|
43.58,
|
||||||
|
44.16,
|
||||||
|
45.15,
|
||||||
|
45.76,
|
||||||
|
45.15,
|
||||||
|
45.23,
|
||||||
|
44.99,
|
||||||
|
44.35,
|
||||||
|
44.64,
|
||||||
|
45.16,
|
||||||
|
44.74,
|
||||||
|
46.06,
|
||||||
|
45.06,
|
||||||
|
45.33,
|
||||||
|
43.58,
|
||||||
|
44.30,
|
||||||
|
43.62,
|
||||||
|
41.74,
|
||||||
|
43.60,
|
||||||
|
44.90,
|
||||||
|
44.86,
|
||||||
|
44.65,
|
||||||
|
49.54,
|
||||||
|
51.93,
|
||||||
|
52.52,
|
||||||
|
52.76,
|
||||||
|
51.83,
|
||||||
|
52.34,
|
||||||
|
52.07,
|
||||||
|
50.70,
|
||||||
|
49.70,
|
||||||
|
50.24,
|
||||||
|
49.94,
|
||||||
|
48.07,
|
||||||
|
47.11,
|
||||||
|
47.10,
|
||||||
|
47.49,
|
||||||
|
50.62,
|
||||||
|
51.34,
|
||||||
|
51.37,
|
||||||
|
52.26,
|
||||||
|
53.39,
|
||||||
|
53.04,
|
||||||
|
52.67,
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data,
|
||||||
|
}
|
||||||
109
test/indicator/accumulation-distribution-line.js
Executable file
109
test/indicator/accumulation-distribution-line.js
Executable file
@ -0,0 +1,109 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import _ from 'underscore';
|
||||||
|
import ADL from '../../lib/indicator/accumulation-distribution-line.js';
|
||||||
|
|
||||||
|
describe('Accumulation Distribution Line', () => {
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
//high, low, close, volume
|
||||||
|
[62.34, 61.37, 62.15, 7849],
|
||||||
|
[62.05, 60.69, 60.81, 11692],
|
||||||
|
[62.27, 60.10, 60.45, 10575],
|
||||||
|
[60.79, 58.61, 59.18, 13059],
|
||||||
|
[59.93, 58.71, 59.24, 21034],
|
||||||
|
[61.75, 59.86, 60.20, 29630],
|
||||||
|
[60.00, 57.97, 58.48, 17705],
|
||||||
|
[59.00, 58.02, 58.24, 7259],
|
||||||
|
[59.07, 57.48, 58.69, 10475],
|
||||||
|
[59.22, 58.30, 58.65, 5204],
|
||||||
|
[58.75, 57.83, 58.47, 3423],
|
||||||
|
[58.65, 57.86, 58.02, 3962],
|
||||||
|
[58.47, 57.91, 58.17, 4096],
|
||||||
|
[58.25, 57.83, 58.07, 3766],
|
||||||
|
[58.35, 57.53, 58.13, 4239],
|
||||||
|
[59.86, 58.58, 58.94, 8040],
|
||||||
|
[59.53, 58.30, 59.10, 6957],
|
||||||
|
[62.10, 58.53, 61.92, 18172],
|
||||||
|
[62.16, 59.80, 61.37, 22226],
|
||||||
|
[62.67, 60.93, 61.68, 14614],
|
||||||
|
[62.38, 60.15, 62.09, 12320],
|
||||||
|
[63.73, 62.26, 62.89, 15008],
|
||||||
|
[63.85, 63.00, 63.53, 8880],
|
||||||
|
[66.15, 63.58, 64.01, 22694],
|
||||||
|
[65.34, 64.07, 64.77, 10192],
|
||||||
|
[66.48, 65.20, 65.22, 10074],
|
||||||
|
[65.23, 63.21, 63.28, 9412],
|
||||||
|
];
|
||||||
|
|
||||||
|
let expectedResults = [
|
||||||
|
4774,
|
||||||
|
-4855,
|
||||||
|
-12019,
|
||||||
|
-18249,
|
||||||
|
-21006,
|
||||||
|
-39976,
|
||||||
|
-48785,
|
||||||
|
-52785,
|
||||||
|
-47317,
|
||||||
|
-48561,
|
||||||
|
-47216,
|
||||||
|
-49574,
|
||||||
|
-49866,
|
||||||
|
-49354,
|
||||||
|
-47389,
|
||||||
|
-50907,
|
||||||
|
-48813,
|
||||||
|
-32474,
|
||||||
|
-25128,
|
||||||
|
-27144,
|
||||||
|
-18028,
|
||||||
|
-20193,
|
||||||
|
-18000,
|
||||||
|
-33099,
|
||||||
|
-32056,
|
||||||
|
-41816,
|
||||||
|
-50575,
|
||||||
|
];
|
||||||
|
|
||||||
|
let runTest = async (data, expectedResults, options = {}) => {
|
||||||
|
let adl = new ADL(options);
|
||||||
|
adl.setValues(data);
|
||||||
|
let results = await adl.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(results.length == expectedResults.length);
|
||||||
|
|
||||||
|
results.forEach((resultItem, idx) => {
|
||||||
|
assert.closeTo(resultItem.adl, expectedResults[idx], 50);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let convertArrayToCollection = (data) => {
|
||||||
|
return data.map(item => {
|
||||||
|
return { high: item[0], low: item[1], close: item[2], volume: item[3] };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should calculate correctly and return result', () => {
|
||||||
|
let d = convertArrayToCollection(data);
|
||||||
|
(async () => {
|
||||||
|
await runTest(d, expectedResults);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with range and return result', () => {
|
||||||
|
let dataCopy = [...data];
|
||||||
|
dataCopy.unshift([63.73, 62.26, 62.89, 15008]);
|
||||||
|
dataCopy.push([63.73, 62.26, 62.89, 15008])
|
||||||
|
let d = convertArrayToCollection(dataCopy);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let opts = { startIndex: 1, endIndex: d.length - 2 };
|
||||||
|
await runTest(d, expectedResults, opts);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
151
test/indicator/average-directional-index.js
Executable file
151
test/indicator/average-directional-index.js
Executable file
@ -0,0 +1,151 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import _ from 'underscore';
|
||||||
|
import ADI from '../../lib/indicator/average-directional-index.js';
|
||||||
|
|
||||||
|
describe('Average Directional Index', () => {
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
//high, low, close
|
||||||
|
[30.20, 29.41, 29.87],
|
||||||
|
[30.28, 29.32, 30.24],
|
||||||
|
[30.45, 29.96, 30.10],
|
||||||
|
[29.35, 28.74, 28.90],
|
||||||
|
[29.35, 28.56, 28.92],
|
||||||
|
[29.29, 28.41, 28.48],
|
||||||
|
[28.83, 28.08, 28.56],
|
||||||
|
[28.73, 27.43, 27.56],
|
||||||
|
[28.67, 27.66, 28.47],
|
||||||
|
[28.85, 27.83, 28.28],
|
||||||
|
[28.64, 27.40, 27.49],
|
||||||
|
[27.68, 27.09, 27.23],
|
||||||
|
[27.21, 26.18, 26.35],
|
||||||
|
[26.87, 26.13, 26.33],
|
||||||
|
[27.41, 26.63, 27.03],
|
||||||
|
[26.94, 26.13, 26.22],
|
||||||
|
[26.52, 25.43, 26.01],
|
||||||
|
[26.52, 25.35, 25.46],
|
||||||
|
[27.09, 25.88, 27.03],
|
||||||
|
[27.69, 26.96, 27.45],
|
||||||
|
[28.45, 27.14, 28.36],
|
||||||
|
[28.53, 28.01, 28.43],
|
||||||
|
[28.67, 27.88, 27.95],
|
||||||
|
[29.01, 27.99, 29.01],
|
||||||
|
[29.87, 28.76, 29.38],
|
||||||
|
[29.80, 29.14, 29.36],
|
||||||
|
[29.75, 28.71, 28.91],
|
||||||
|
[30.65, 28.93, 30.61],
|
||||||
|
[30.60, 30.03, 30.05],
|
||||||
|
[30.76, 29.39, 30.19],
|
||||||
|
[31.17, 30.14, 31.12],
|
||||||
|
];
|
||||||
|
|
||||||
|
let expectedResults = [
|
||||||
|
//TR +DM 1 -DM 1 TR14 +DM14 -DM14 +DI14 -DI14 DI14DiffDI14Sum DX ADX
|
||||||
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0.96, 0.00, 0.09, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0.48, 0.17, 0.00, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[1.36, 0.00, 1.22, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0.79, 0.00, 0.19, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0.88, 0.00, 0.15, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0.75, 0.00, 0.33, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[1.31, 0.00, 0.65, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[1.11, 0.00, 0.00, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[1.02, 0.19, 0.00, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[1.24, 0.00, 0.44, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0.58, 0.00, 0.31, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[1.05, 0.00, 0.91, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0.73, 0.00, 0.05, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[1.08, 0.54, 0.00, 13.33, 0.90, 4.32, 6.75, 32.42, 25.67, 39.17, 65.54, 0],
|
||||||
|
[0.90, 0.00, 0.49, 13.28, 0.84, 4.51, 6.29, 33.95, 27.65, 40.24, 68.73, 0],
|
||||||
|
[1.09, 0.00, 0.70, 13.42, 0.78, 4.89, 5.78, 36.43, 30.65, 42.21, 72.60, 0],
|
||||||
|
[1.17, 0.00, 0.08, 13.63, 0.72, 4.62, 5.29, 33.89, 28.60, 39.17, 73.01, 0],
|
||||||
|
[1.63, 0.57, 0.00, 14.29, 1.24, 4.29, 8.70, 30.02, 21.32, 38.71, 55.06, 0],
|
||||||
|
[0.72, 0.59, 0.00, 13.99, 1.75, 3.98, 12.49, 28.47, 15.98, 40.96, 39.01, 0],
|
||||||
|
[1.31, 0.76, 0.00, 14.30, 2.38, 3.70, 16.68, 25.87, 9.19, 42.55, 21.60, 0],
|
||||||
|
[0.51, 0.08, 0.00, 13.79, 2.29, 3.43, 16.63, 24.90, 8.27, 41.53, 19.92, 0],
|
||||||
|
[0.78, 0.14, 0.00, 13.59, 2.27, 3.19, 16.69, 23.47, 6.78, 40.16, 16.87, 0],
|
||||||
|
[1.06, 0.35, 0.00, 13.67, 2.45, 2.96, 17.93, 21.65, 3.72, 39.59, 9.40, 0],
|
||||||
|
[1.11, 0.86, 0.00, 13.80, 3.14, 2.75, 22.73, 19.92, 2.81, 42.64, 6.59, 0],
|
||||||
|
[0.66, 0.00, 0.00, 13.48, 2.91, 2.55, 21.61, 18.94, 2.67, 40.55, 6.59, 0],
|
||||||
|
[1.04, 0.00, 0.43, 13.56, 2.71, 2.80, 19.95, 20.64, 0.68, 40.59, 1.69, 0],
|
||||||
|
[1.74, 0.90, 0.00, 14.33, 3.41, 2.60, 23.82, 18.13, 5.69, 41.94, 13.57, 33.58],
|
||||||
|
[0.58, 0.00, 0.00, 13.89, 3.17, 2.41, 22.81, 17.36, 5.45, 40.18, 13.57, 32.15],
|
||||||
|
[1.38, 0.00, 0.64, 14.28, 2.94, 2.88, 20.61, 20.20, 0.41, 40.81, 1.01, 29.93],
|
||||||
|
[1.03, 0.41, 0.00, 14.29, 3.14, 2.68, 21.97, 18.74, 3.23, 40.70, 7.93, 28.36],
|
||||||
|
];
|
||||||
|
|
||||||
|
let convertArrayToCollection = (data) => {
|
||||||
|
return data.map(item => {
|
||||||
|
return { high: item[0], low: item[1], close: item[2] };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let convertExpectedResultsToCollection = (data) => {
|
||||||
|
return data.map(item => {
|
||||||
|
return { tr: item[0], pdm: item[1], ndm: item[2], trPeriod: item[3], pdmPeriod: item[4], ndmPeriod: item[5], pdi: item[6], ndi: item[7], dx: item[10], adx: item[11] };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let runTest = async (d, e, opts = {}) => {
|
||||||
|
let adi = new ADI(opts);
|
||||||
|
adi.setValues(d);
|
||||||
|
let results = await adi.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(results.length == e.length);
|
||||||
|
|
||||||
|
results.forEach((resultItem, idx) => {
|
||||||
|
assert.isObject(resultItem);
|
||||||
|
|
||||||
|
let expectedResultItem = e[idx];
|
||||||
|
|
||||||
|
assert.closeTo(resultItem.tr, expectedResultItem.tr, 0.1);
|
||||||
|
assert.closeTo(resultItem.pdm, expectedResultItem.pdm, 0.1);
|
||||||
|
assert.closeTo(resultItem.ndm, expectedResultItem.ndm, 0.1);
|
||||||
|
assert.closeTo(resultItem.trPeriod, expectedResultItem.trPeriod, 0.1);
|
||||||
|
assert.closeTo(resultItem.pdmPeriod, expectedResultItem.pdmPeriod, 0.1);
|
||||||
|
assert.closeTo(resultItem.ndmPeriod, expectedResultItem.ndmPeriod, 0.1);
|
||||||
|
assert.closeTo(resultItem.pdi, expectedResultItem.pdi, 0.1);
|
||||||
|
assert.closeTo(resultItem.ndi, expectedResultItem.ndi, 0.1);
|
||||||
|
assert.closeTo(resultItem.dx, expectedResultItem.dx, 0.4);
|
||||||
|
assert.closeTo(resultItem.adx, expectedResultItem.adx, 0.4);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should calculate correctly and return result', () => {
|
||||||
|
let d = convertArrayToCollection(data);
|
||||||
|
let e = convertExpectedResultsToCollection(expectedResults);
|
||||||
|
(async () => {
|
||||||
|
runTest(d, e, { sliceOffset: false });
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with options and return result', () => {
|
||||||
|
let dataCopy = [...data];
|
||||||
|
dataCopy.unshift([30.30, 29.40, 29.80]);
|
||||||
|
dataCopy.push([30.20, 29.41, 29.87]);
|
||||||
|
|
||||||
|
let d = convertArrayToCollection(dataCopy);
|
||||||
|
let e = convertExpectedResultsToCollection(expectedResults);
|
||||||
|
let opts = { startIndex: 1, endIndex: d.length - 2, sliceOffset: false };
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
runTest(d, e, opts);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with sliceOffset and return result', () => {
|
||||||
|
let d = convertArrayToCollection(data);
|
||||||
|
let e = convertExpectedResultsToCollection(expectedResults);
|
||||||
|
|
||||||
|
e = e.slice(27);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
runTest(d, e, { sliceOffset: true });
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
135
test/indicator/average-true-range.js
Executable file
135
test/indicator/average-true-range.js
Executable file
@ -0,0 +1,135 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import ATR from '../../lib/indicator/average-true-range.js';
|
||||||
|
import Fixture from '../fixtures/average-true-range.js';
|
||||||
|
|
||||||
|
describe('Average True Range', () => {
|
||||||
|
|
||||||
|
const data = Fixture.data;
|
||||||
|
const expectedResults = Fixture.dataResults;
|
||||||
|
|
||||||
|
it('should calculate true range correctly and return result', () => {
|
||||||
|
let atr = new ATR({ lazyEvaluation: false });
|
||||||
|
function runTest(currentItem, prevItem, expectedResult) {
|
||||||
|
let result = atr._getTrueRange(currentItem, prevItem);
|
||||||
|
|
||||||
|
assert.isNumber(result);
|
||||||
|
assert.closeTo(result, expectedResult, 0.05);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
let currItem = data[i];
|
||||||
|
currItem = { high: currItem[0], low: currItem[1], close: currItem[2] };
|
||||||
|
|
||||||
|
let prevItem = (i) ? data[i - 1] : null;
|
||||||
|
if (prevItem) {
|
||||||
|
prevItem = { high: prevItem[0], low: prevItem[1], close: prevItem[2] };
|
||||||
|
}
|
||||||
|
let expectedResult = expectedResults[i][3];
|
||||||
|
|
||||||
|
runTest(currItem, prevItem, expectedResult);
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should calculate the first average true range in collection correctly and return result', () => {
|
||||||
|
let collection = [];
|
||||||
|
for (let i = 0; i < 14; i++) {
|
||||||
|
collection.push({ tr: expectedResults[i][3] });
|
||||||
|
}
|
||||||
|
|
||||||
|
let atr = new ATR();
|
||||||
|
let result = atr._calcFirstATR(collection);
|
||||||
|
|
||||||
|
assert.isNumber(result);
|
||||||
|
assert.closeTo(result, expectedResults[13][4], 0.01);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate remaining average true range correctly and return results', () => {
|
||||||
|
let prevATR = expectedResults[13][4];
|
||||||
|
let currTR = expectedResults[14][3];
|
||||||
|
let atr = new ATR();
|
||||||
|
let result = atr._calcRemainingATR(prevATR, currTR, 14);
|
||||||
|
|
||||||
|
assert.isNumber(result);
|
||||||
|
assert.closeTo(result, expectedResults[14][4], 0.02);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compute correctly and return results', () => {
|
||||||
|
let runTest = async (options, collection, expectedResults) => {
|
||||||
|
let atr = new ATR(options);
|
||||||
|
atr.setValues(collection);
|
||||||
|
let results = await atr.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(results.length == expectedResults.length);
|
||||||
|
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.isObject(item);
|
||||||
|
assert.closeTo(expectedResults[idx][3], item.tr, 0.02);
|
||||||
|
assert.closeTo(expectedResults[idx][4], item.atr, 0.02);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let collection = [];
|
||||||
|
data.forEach((item, idx) => {
|
||||||
|
collection.push({ high: item[0], low: item[1], close: item[2] });
|
||||||
|
});
|
||||||
|
|
||||||
|
runTest({ periods: 14 }, collection, expectedResults);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid values', () => {
|
||||||
|
assert.throws(() => ATR(), Error);
|
||||||
|
assert.throws(() => (new ATR()).setValues(1), Error);
|
||||||
|
assert.throws(() => (new ATR()).setValues('foo'), Error);
|
||||||
|
|
||||||
|
let atr = new ATR();
|
||||||
|
atr.setValues([{ 'foo': 100 }]);
|
||||||
|
assert.throws(() => atr.calculate(), Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid values when in async', (done) => {
|
||||||
|
let runTest = async () => {
|
||||||
|
try {
|
||||||
|
let atr = new ATR({ lazyEvaluation: true });
|
||||||
|
let r = await atr.calculate();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let b = await runTest();
|
||||||
|
assert.isFalse(b);
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error on invalid options', () => {
|
||||||
|
function runTest(opts) {
|
||||||
|
opts = Object.assign({}, opts, { lazyEvaluation: false });
|
||||||
|
let atr = new ATR(opts);
|
||||||
|
atr.setValues(data);
|
||||||
|
assert.throws(() => atr.calculate(), Error);
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
{ periods: data.length + 1 },
|
||||||
|
{ startIndex: data.length + 1 },
|
||||||
|
{ startIndex: 1, periods: data.length },
|
||||||
|
{ endIndex: data.length + 1 },
|
||||||
|
{ periods: 'foo' },
|
||||||
|
{ startIndex: data.length, endIndex: 0 },
|
||||||
|
{ startIndex: 1, periods: 10 },
|
||||||
|
{ startIndex: 1, endIndex: 2 },
|
||||||
|
].forEach((item) => runTest(item));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
112
test/indicator/bollinger-bands.js
Executable file
112
test/indicator/bollinger-bands.js
Executable file
@ -0,0 +1,112 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import BB from '../../lib/indicator/bollinger-bands.js';
|
||||||
|
|
||||||
|
describe('Boilinger Bands', () => {
|
||||||
|
|
||||||
|
let data = [25.5, 26.75, 27.0, 26.5, 27.25];
|
||||||
|
let data_2 = [25.5, 26.75, 27.0, 26.5, 27.25, 28.1];
|
||||||
|
|
||||||
|
it('should calculate correctly and return result', () => {
|
||||||
|
let runTest = async (periods, values, expectedResult) => {
|
||||||
|
let opts = { periods, lazyEvaluation: true, sliceOffset: true };
|
||||||
|
let bb = new BB(opts);
|
||||||
|
bb.setValues(values);
|
||||||
|
let results = await bb.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results)
|
||||||
|
assert.isTrue(values.length - opts.periods + 1 == results.length);
|
||||||
|
|
||||||
|
expectedResult.forEach((expected, idx) => {
|
||||||
|
let resultItem = results[idx];
|
||||||
|
assert.isObject(resultItem);
|
||||||
|
assert.containsAllKeys(resultItem, ['upper', 'middle', 'lower', 'price']);
|
||||||
|
assert.closeTo(resultItem.upper, expected.u, 0.1);
|
||||||
|
assert.closeTo(resultItem.middle, expected.m, 0.1);
|
||||||
|
assert.closeTo(resultItem.lower, expected.l, 0.1);
|
||||||
|
assert.isTrue(resultItem.price == values[periods - 1 + idx]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let arr = [
|
||||||
|
{
|
||||||
|
p: 5, v: data, e: [
|
||||||
|
{ m: 26.6, u: 27.8, l: 25.4 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
p: 5, v: data_2, e: [
|
||||||
|
{ m: 26.6, u: 27.8, l: 25.4 },
|
||||||
|
{ m: 27.1, u: 28.2, l: 26.0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
let item = arr[i];
|
||||||
|
runTest(item.p, item.v, item.e);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
it('should calculate correctly and return results with offset', () => {
|
||||||
|
let runTest = async (periods, values, expectedResult) => {
|
||||||
|
let opts = { periods, sliceOffset: false, lazyEvaluation: true };
|
||||||
|
let bb = new BB(opts);
|
||||||
|
bb.setValues(values);
|
||||||
|
let results = await bb.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results)
|
||||||
|
assert.isTrue(values.length == results.length);
|
||||||
|
|
||||||
|
expectedResult.forEach((expected, idx) => {
|
||||||
|
let resultItem = results[idx];
|
||||||
|
assert.containsAllKeys(resultItem, ['upper', 'middle', 'lower', 'price']);
|
||||||
|
assert.closeTo(resultItem.upper, expected.u, 0.1);
|
||||||
|
assert.closeTo(resultItem.middle, expected.m, 0.1);
|
||||||
|
assert.closeTo(resultItem.lower, expected.l, 0.1);
|
||||||
|
assert.isTrue(resultItem.price == values[idx]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let arr = [
|
||||||
|
{
|
||||||
|
p: 5, v: data, e: [
|
||||||
|
{ m: 0, u: 0, l: 0 },
|
||||||
|
{ m: 0, u: 0, l: 0 },
|
||||||
|
{ m: 0, u: 0, l: 0 },
|
||||||
|
{ m: 0, u: 0, l: 0 },
|
||||||
|
{ m: 26.6, u: 27.8, l: 25.4 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
let item = arr[i];
|
||||||
|
runTest(item.p, item.v, item.e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw error on invalid options', () => {
|
||||||
|
function runTest(opts) {
|
||||||
|
let bb = new BB(opts);
|
||||||
|
bb.setValues(data);
|
||||||
|
assert.throws(() => bb.calculate(), Error);
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
{ periods: data.length + 1 },
|
||||||
|
{ startIndex: data.length + 1 },
|
||||||
|
{ endIndex: data.length + 1 },
|
||||||
|
{ periods: 'foo' },
|
||||||
|
{ startIndex: data.length, endIndex: 0 },
|
||||||
|
].forEach((option) => runTest(option));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
204
test/indicator/exponential-moving-average.js
Executable file
204
test/indicator/exponential-moving-average.js
Executable file
@ -0,0 +1,204 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import EMA from '../../lib/indicator/exponential-moving-average.js';
|
||||||
|
|
||||||
|
describe('Exponential Moving Average', () => {
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
items: [
|
||||||
|
22.27,
|
||||||
|
22.19,
|
||||||
|
22.08,
|
||||||
|
22.17,
|
||||||
|
22.18,
|
||||||
|
22.13,
|
||||||
|
22.23,
|
||||||
|
22.43,
|
||||||
|
22.24,
|
||||||
|
22.29,
|
||||||
|
22.15,
|
||||||
|
22.39,
|
||||||
|
22.38,
|
||||||
|
22.61,
|
||||||
|
23.36,
|
||||||
|
24.05,
|
||||||
|
23.75,
|
||||||
|
23.83,
|
||||||
|
23.95,
|
||||||
|
23.63,
|
||||||
|
23.82,
|
||||||
|
23.87,
|
||||||
|
23.65,
|
||||||
|
23.19,
|
||||||
|
23.10,
|
||||||
|
23.33,
|
||||||
|
22.68,
|
||||||
|
23.10,
|
||||||
|
22.40,
|
||||||
|
22.17,
|
||||||
|
],
|
||||||
|
periods: 10,
|
||||||
|
results: [
|
||||||
|
22.22,
|
||||||
|
22.21,
|
||||||
|
22.24,
|
||||||
|
22.27,
|
||||||
|
22.33,
|
||||||
|
22.52,
|
||||||
|
22.80,
|
||||||
|
22.97,
|
||||||
|
23.13,
|
||||||
|
23.28,
|
||||||
|
23.34,
|
||||||
|
23.43,
|
||||||
|
23.51,
|
||||||
|
23.53,
|
||||||
|
23.47,
|
||||||
|
23.40,
|
||||||
|
23.39,
|
||||||
|
23.26,
|
||||||
|
23.23,
|
||||||
|
23.08,
|
||||||
|
22.92
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let data_2 = {
|
||||||
|
items: [
|
||||||
|
22.81,
|
||||||
|
23.09,
|
||||||
|
22.91,
|
||||||
|
23.23,
|
||||||
|
22.83,
|
||||||
|
23.05,
|
||||||
|
23.02,
|
||||||
|
23.29,
|
||||||
|
23.41,
|
||||||
|
23.49,
|
||||||
|
24.60,
|
||||||
|
24.63,
|
||||||
|
24.51,
|
||||||
|
23.73,
|
||||||
|
],
|
||||||
|
periods: 9,
|
||||||
|
results: [
|
||||||
|
22.81,
|
||||||
|
22.87,
|
||||||
|
22.87,
|
||||||
|
22.95,
|
||||||
|
22.92,
|
||||||
|
22.95,
|
||||||
|
22.96,
|
||||||
|
23.03,
|
||||||
|
23.10,
|
||||||
|
23.18,
|
||||||
|
23.47,
|
||||||
|
23.70,
|
||||||
|
23.86,
|
||||||
|
23.83,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let data_3 = {
|
||||||
|
items: [
|
||||||
|
22.81,
|
||||||
|
23.09,
|
||||||
|
22.91,
|
||||||
|
23.23,
|
||||||
|
22.83,
|
||||||
|
23.05,
|
||||||
|
23.02,
|
||||||
|
23.29,
|
||||||
|
23.41,
|
||||||
|
23.49,
|
||||||
|
24.60,
|
||||||
|
24.63,
|
||||||
|
24.51,
|
||||||
|
23.73,
|
||||||
|
23.31,
|
||||||
|
23.53,
|
||||||
|
23.06,
|
||||||
|
23.25,
|
||||||
|
23.12,
|
||||||
|
22.80,
|
||||||
|
22.84,
|
||||||
|
],
|
||||||
|
periods: 9,
|
||||||
|
results: [
|
||||||
|
23.07,
|
||||||
|
23.18,
|
||||||
|
23.47,
|
||||||
|
23.70,
|
||||||
|
23.86,
|
||||||
|
23.83,
|
||||||
|
23.73,
|
||||||
|
23.69,
|
||||||
|
23.56,
|
||||||
|
23.50,
|
||||||
|
23.42,
|
||||||
|
23.30,
|
||||||
|
23.21,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should calculate correctly and return results', (done) => {
|
||||||
|
(async () => {
|
||||||
|
let ema = new EMA({ periods: data.periods, lazyEvaluation: true, sliceOffset: true });
|
||||||
|
ema.setValues(data.items);
|
||||||
|
let results = await ema.calculate();
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(data.items.length - (data.periods - 1) == results.length);
|
||||||
|
results.forEach((item, idx) => assert.closeTo(item.ema, data.results[idx], 0.02));
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly when started with first item and return results', (done) => {
|
||||||
|
(async () => {
|
||||||
|
let ema = new EMA({ periods: data_2.periods, startWithFirst: true, lazyEvaluation: true, sliceOffset: true });
|
||||||
|
ema.setValues(data_2.items);
|
||||||
|
let results = await ema.calculate();
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(data_2.items.length == data_2.results.length);
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.closeTo(item.ema, data_2.results[idx], 0.02);
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with range and return results', (done) => {
|
||||||
|
(async () => {
|
||||||
|
let ema = new EMA({ periods: data_3.periods, sliceOffset: true });
|
||||||
|
ema.setValues(data_3.items);
|
||||||
|
let results = await ema.calculate();
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(data_3.items.length - (data_3.periods - 1) == data_3.results.length);
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.closeTo(item.ema, data_3.results[idx], 0.05);
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with periods and return results with offset', (done) => {
|
||||||
|
(async () => {
|
||||||
|
let dataCopy = Object.assign({}, data);
|
||||||
|
let i = 0;
|
||||||
|
while (i < dataCopy.periods - 1) {
|
||||||
|
dataCopy.results.unshift(0);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
let ema = new EMA({ periods: dataCopy.periods, sliceOffset: false, lazyEvaluation: true });
|
||||||
|
ema.setValues(dataCopy.items);
|
||||||
|
let results = await ema.calculate();
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(dataCopy.items.length == results.length);
|
||||||
|
results.forEach((item, idx) => assert.closeTo(item.ema, dataCopy.results[idx], 0.02));
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
84
test/indicator/linearly-weighted-moving-average.js
Executable file
84
test/indicator/linearly-weighted-moving-average.js
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import LWMA from '../../lib/indicator/linearly-weighted-moving-average.js';
|
||||||
|
|
||||||
|
describe('Lineary wighted moving average', () => {
|
||||||
|
|
||||||
|
let data = [10, 15, 16, 18, 20, 18, 17];
|
||||||
|
let periods = 4;
|
||||||
|
|
||||||
|
let expectedResults = [16, 17.85, 18.22, 18.12];
|
||||||
|
|
||||||
|
let runTest = async (options, data) => {
|
||||||
|
let lwma = new LWMA(options);
|
||||||
|
lwma.setValues(data);
|
||||||
|
return await lwma.calculate();
|
||||||
|
};
|
||||||
|
|
||||||
|
let assertResults = (results, expectedResults) => {
|
||||||
|
assert.isArray(results);
|
||||||
|
results.forEach((result, idx) => {
|
||||||
|
assert.isObject(result);
|
||||||
|
assert.containsAllKeys(result, ['price', 'lwma']);
|
||||||
|
assert.isNumber(result.price);
|
||||||
|
assert.isNumber(result.lwma);
|
||||||
|
assert.closeTo(result.lwma, expectedResults[idx], 0.1);
|
||||||
|
assert.isTrue(result.price == data[periods - 1 + idx]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let assertResults_2 = (results, expectedResults) => {
|
||||||
|
assert.isArray(results);
|
||||||
|
results.forEach((result, idx) => {
|
||||||
|
assert.isObject(result);
|
||||||
|
assert.containsAllKeys(result, ['price', 'lwma']);
|
||||||
|
assert.isNumber(result.price);
|
||||||
|
assert.isNumber(result.lwma);
|
||||||
|
assert.closeTo(result.lwma, expectedResults[idx].lwma, 0.1);
|
||||||
|
assert.isTrue(result.price == expectedResults[idx].price);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should calculate correctly and return results', () => {
|
||||||
|
(async () => {
|
||||||
|
let opts = { periods: periods, lazyEvaluation: true, sliceOffset: true };
|
||||||
|
let r = await runTest(opts, data);
|
||||||
|
assertResults(r, expectedResults);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with custom options and return results', () => {
|
||||||
|
(async () => {
|
||||||
|
let clonedData = [...data];
|
||||||
|
clonedData.unshift(5);
|
||||||
|
clonedData.push(20);
|
||||||
|
let opts = { periods: 4, sliceOffset: true, startIndex: 1, endIndex: clonedData.length - 2, lazyEvaluation: true };
|
||||||
|
let r = await runTest(opts, clonedData)
|
||||||
|
assertResults(r, expectedResults);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly and return results with included offset', () => {
|
||||||
|
(async () => {
|
||||||
|
let clonedExpectedResults = [...expectedResults];
|
||||||
|
let opts = { periods: periods, sliceOffset: false, lazyEvaluation: true };
|
||||||
|
|
||||||
|
for (let i = 0; i < opts.periods - 1; i++) {
|
||||||
|
clonedExpectedResults.unshift(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected = clonedExpectedResults.map((val, idx) => {
|
||||||
|
return {
|
||||||
|
lwma: val,
|
||||||
|
price: data[idx],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let r = await runTest(opts, data);
|
||||||
|
assertResults_2(r, expected);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
93
test/indicator/money-flow-index.js
Executable file
93
test/indicator/money-flow-index.js
Executable file
@ -0,0 +1,93 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import MFI from '../../lib/indicator/money-flow-index.js';
|
||||||
|
|
||||||
|
describe('Money Flow Index', () => {
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
//High,, Low, Close, Volume
|
||||||
|
[24.64, 24.62, 24.63, 18730],
|
||||||
|
[24.70, 24.68, 24.69, 12272],
|
||||||
|
[25.00, 24.98, 24.99, 24691],
|
||||||
|
[25.37, 25.35, 25.36, 18358],
|
||||||
|
[25.20, 25.18, 25.19, 22964],
|
||||||
|
[25.18, 25.16, 25.17, 15919],
|
||||||
|
[25.02, 25.00, 25.01, 16067],
|
||||||
|
[24.97, 24.95, 24.96, 16568],
|
||||||
|
[25.09, 25.07, 25.08, 16019],
|
||||||
|
[25.26, 25.24, 25.25, 9774],
|
||||||
|
[25.22, 25.20, 25.21, 22573],
|
||||||
|
[25.38, 25.36, 25.37, 12987],
|
||||||
|
[25.62, 25.60, 25.61, 10907],
|
||||||
|
[25.59, 25.57, 25.58, 5799],
|
||||||
|
[25.47, 25.45, 25.46, 7395],
|
||||||
|
[25.34, 25.32, 25.33, 5818],
|
||||||
|
[25.10, 25.08, 25.09, 7165],
|
||||||
|
[25.04, 25.02, 25.03, 5673],
|
||||||
|
[24.92, 24.90, 24.91, 5625],
|
||||||
|
[24.90, 24.88, 24.89, 5023],
|
||||||
|
];
|
||||||
|
|
||||||
|
let expectedResults = [
|
||||||
|
//moneyFlowRatio, moneyFlowIndex
|
||||||
|
[0.98, 49.47],
|
||||||
|
[0.82, 45.11],
|
||||||
|
[0.57, 36.27],
|
||||||
|
[0.40, 28.41],
|
||||||
|
[0.46, 31.53],
|
||||||
|
[0.51, 33.87],
|
||||||
|
];
|
||||||
|
|
||||||
|
let convertArrayToCollection = (data) => {
|
||||||
|
return data.map(item => {
|
||||||
|
return { high: item[0], low: item[1], close: item[2], volume: item[3] };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should calculate correctly and return result', () => {
|
||||||
|
let expectedResultsCopy = [...expectedResults];
|
||||||
|
|
||||||
|
for (let i = 0, len = data.length - expectedResultsCopy.length; i < len; i++) {
|
||||||
|
expectedResultsCopy.unshift([0, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let opts = { sliceOffset: false };
|
||||||
|
let mfi = new MFI(opts);
|
||||||
|
mfi.setValues(convertArrayToCollection(data));
|
||||||
|
let results = await mfi.calculate();
|
||||||
|
|
||||||
|
assert.isTrue(results.length == expectedResultsCopy.length);
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
let { moneyFlowRatio, moneyFlowIndex } = results[i];
|
||||||
|
assert.closeTo(moneyFlowRatio, expectedResultsCopy[i][0], 0.02);
|
||||||
|
assert.closeTo(moneyFlowIndex, expectedResultsCopy[i][1], 0.1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with options and return results', () => {
|
||||||
|
let dataCopy = [...data];
|
||||||
|
let values = [0.64, 0.62, 0.63, 1000];
|
||||||
|
dataCopy.unshift(values);
|
||||||
|
dataCopy.push(values);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let opts = { sliceOffset: true, startIndex: 1, endIndex: dataCopy.length - 2 };
|
||||||
|
let mfi = new MFI(opts);
|
||||||
|
mfi.setValues(convertArrayToCollection(dataCopy));
|
||||||
|
let results = await mfi.calculate();
|
||||||
|
|
||||||
|
assert.isTrue(results.length == expectedResults.length);
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
let { moneyFlowRatio, moneyFlowIndex } = results[i];
|
||||||
|
assert.closeTo(moneyFlowRatio, expectedResults[i][0], 0.02);
|
||||||
|
assert.closeTo(moneyFlowIndex, expectedResults[i][1], 0.1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
117
test/indicator/moving-average-convergence-divergence.js
Executable file
117
test/indicator/moving-average-convergence-divergence.js
Executable file
@ -0,0 +1,117 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import MACD from '../../lib/indicator/moving-average-convergence-divergence.js';
|
||||||
|
import Fixture from '../fixtures/moving-average-convergence-divergence.js';
|
||||||
|
|
||||||
|
describe('Moving Average Convergence Divergence', () => {
|
||||||
|
|
||||||
|
let data = Fixture.data;
|
||||||
|
|
||||||
|
let results_slow = {
|
||||||
|
periods: 26,
|
||||||
|
items: [
|
||||||
|
40.45,
|
||||||
|
40.49,
|
||||||
|
40.51,
|
||||||
|
40.54,
|
||||||
|
40.76,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let results_fast = {
|
||||||
|
periods: 12,
|
||||||
|
items: [
|
||||||
|
38.90,
|
||||||
|
38.71,
|
||||||
|
38.42,
|
||||||
|
38.11,
|
||||||
|
38.18,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let results_macd = {
|
||||||
|
items: [
|
||||||
|
-1.55,
|
||||||
|
-1.78,
|
||||||
|
-2.09,
|
||||||
|
-2.43,
|
||||||
|
-2.58,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let results_signal = {
|
||||||
|
items: [
|
||||||
|
-1.96,
|
||||||
|
-2.07,
|
||||||
|
-2.14,
|
||||||
|
-2.15,
|
||||||
|
-2.08,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let runTest = async (opts, values, expectedLength) => {
|
||||||
|
let macd = new MACD(opts);
|
||||||
|
macd.setValues(values);
|
||||||
|
let result = await macd.calculate();
|
||||||
|
|
||||||
|
assert.isArray(result);
|
||||||
|
assert.isTrue(result.length == 1);
|
||||||
|
|
||||||
|
result = result[0];
|
||||||
|
assert.isObject(result);
|
||||||
|
|
||||||
|
assert.isArray(result.slow_ema);
|
||||||
|
assert.isArray(result.fast_ema);
|
||||||
|
assert.isArray(result.signal_ema);
|
||||||
|
assert.isArray(result.macd);
|
||||||
|
assert.isArray(result.prices);
|
||||||
|
|
||||||
|
assert.isTrue(result.slow_ema.length == expectedLength);
|
||||||
|
assert.isTrue(result.fast_ema.length == expectedLength);
|
||||||
|
assert.isTrue(result.signal_ema.length == expectedLength);
|
||||||
|
assert.isTrue(result.macd.length == expectedLength);
|
||||||
|
assert.isTrue(result.prices.length == expectedLength);
|
||||||
|
|
||||||
|
results_slow.items.forEach((result_val, idx) => {
|
||||||
|
let val = result.slow_ema[result.slow_ema.length - 1 - idx];
|
||||||
|
assert.closeTo(val, result_val, 0.05);
|
||||||
|
});
|
||||||
|
|
||||||
|
results_fast.items.forEach((result_val, idx) => {
|
||||||
|
let val = result.fast_ema[result.fast_ema.length - 1 - idx];
|
||||||
|
assert.closeTo(val, result_val, 0.05);
|
||||||
|
});
|
||||||
|
|
||||||
|
results_macd.items.forEach((result_val, idx) => {
|
||||||
|
let val = result.macd[result.macd.length - 1 - idx];
|
||||||
|
assert.closeTo(val, result_val, 0.05);
|
||||||
|
});
|
||||||
|
|
||||||
|
results_signal.items.forEach((result_val, idx) => {
|
||||||
|
let val = result.signal_ema[result.signal_ema.length - 1 - idx];
|
||||||
|
assert.closeTo(val, result_val, 0.05);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should calculate and return results', () => {
|
||||||
|
(async () => {
|
||||||
|
let opts = { fastPeriods: results_fast.periods, slowPeriods: results_slow.periods, signalPeriods: 9, lazyEvaluation: true, sliceOffset: true };
|
||||||
|
let expectedLength = data.length - results_slow.periods - opts.signalPeriods + 2;
|
||||||
|
let dataCopy = [...data];
|
||||||
|
await runTest(opts, dataCopy.reverse(), expectedLength);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate and return results with offset', () => {
|
||||||
|
(async () => {
|
||||||
|
let opts = { fastPeriods: results_fast.periods, slowPeriods: results_slow.periods, signalPeriods: 9, sliceOffset: false, lazyEvaluation: true };
|
||||||
|
let expectedLength = data.length;
|
||||||
|
let dataCopy = [...data];
|
||||||
|
await runTest(opts, dataCopy.reverse(), expectedLength);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
129
test/indicator/on-balance-volume.js
Executable file
129
test/indicator/on-balance-volume.js
Executable file
@ -0,0 +1,129 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import OBV from '../../lib/indicator/on-balance-volume.js';
|
||||||
|
|
||||||
|
describe('On Balance Volume', () => {
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
[10, 25200],
|
||||||
|
[10.15, 30000],
|
||||||
|
[10.17, 25600],
|
||||||
|
[10.13, 32000],
|
||||||
|
[10.11, 23000],
|
||||||
|
[10.15, 40000],
|
||||||
|
[10.20, 36000],
|
||||||
|
[10.20, 20500],
|
||||||
|
[10.22, 23000],
|
||||||
|
[10.21, 27500],
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should calculate correctly and return result', () => {
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let obv = new OBV({ lazyEvaluation: true });
|
||||||
|
let collection = data.map((item) => {
|
||||||
|
return { price: item[0], volume: item[1] };
|
||||||
|
});
|
||||||
|
|
||||||
|
obv.setValues(collection);
|
||||||
|
let result = await obv.calculate();
|
||||||
|
|
||||||
|
let exptedResult = [
|
||||||
|
0,
|
||||||
|
30000,
|
||||||
|
55600,
|
||||||
|
23600,
|
||||||
|
600,
|
||||||
|
40600,
|
||||||
|
76600,
|
||||||
|
76600,
|
||||||
|
99600,
|
||||||
|
72100,
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.isArray(result);
|
||||||
|
assert.isTrue(data.length == exptedResult.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < exptedResult.length; i++) {
|
||||||
|
let idx = i;
|
||||||
|
let expectedValue = exptedResult[idx];
|
||||||
|
assert.isObject(result[idx]);
|
||||||
|
assert.containsAllKeys(result[idx], ['price', 'obv']);
|
||||||
|
assert.isTrue(result[idx].price == data[idx][0]);
|
||||||
|
assert.closeTo(result[idx].obv, expectedValue, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly within range and return result', () => {
|
||||||
|
(async () => {
|
||||||
|
let opts = { startIndex: 2, endIndex: data.length - 2, lazyEvaluation: true };
|
||||||
|
let obv = new OBV(opts);
|
||||||
|
|
||||||
|
let collection = data.map((item) => {
|
||||||
|
return { price: item[0], volume: item[1] };
|
||||||
|
});
|
||||||
|
|
||||||
|
obv.setValues(collection);
|
||||||
|
let result = await obv.calculate();
|
||||||
|
|
||||||
|
let exptedResult = [
|
||||||
|
0,
|
||||||
|
-32000,
|
||||||
|
-55000,
|
||||||
|
-15000,
|
||||||
|
21000,
|
||||||
|
21000,
|
||||||
|
44000,
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.isArray(result);
|
||||||
|
assert.isTrue(result.length == exptedResult.length);
|
||||||
|
exptedResult.forEach((expectedValue, idx) => {
|
||||||
|
assert.closeTo(result[idx].obv, expectedValue, 100);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid options', () => {
|
||||||
|
let runTest = async (opts) => {
|
||||||
|
let obv = new OBV(opts);
|
||||||
|
|
||||||
|
let collection = data.map((item) => {
|
||||||
|
return { price: item[0], volume: item[1] };
|
||||||
|
});
|
||||||
|
|
||||||
|
let failed = false;
|
||||||
|
try {
|
||||||
|
obv.setValues(collection);
|
||||||
|
let r = await obv.calculate();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (err.name == 'Error') {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.isTrue(failed);
|
||||||
|
};
|
||||||
|
|
||||||
|
let arr = [
|
||||||
|
{ startIndex: data.length + 1, lazyEvaluation: true },
|
||||||
|
{ endIndex: data.length + 1, lazyEvaluation: true },
|
||||||
|
{ startIndex: data.length, endIndex: 0, lazyEvaluation: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
runTest(arr[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid values', () => {
|
||||||
|
assert.throws(() => (new OBV()).setValues([{ 'price': 1, 'foo': 10 }]), Error);
|
||||||
|
assert.throws(() => (new OBV()).setValues([{ 'volume': 1, 'foo': 10 }]), Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
132
test/indicator/parabolic-stop-and-reverse.js
Executable file
132
test/indicator/parabolic-stop-and-reverse.js
Executable file
@ -0,0 +1,132 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import _ from 'underscore';
|
||||||
|
import ROC from '../../lib/indicator/parabolic-stop-and-reverse.js';
|
||||||
|
|
||||||
|
describe('Parabolic Stop and Reverse', () => {
|
||||||
|
|
||||||
|
let data_uptrend = [
|
||||||
|
//high, low
|
||||||
|
|
||||||
|
// [47.85, 47.48 ],
|
||||||
|
// [47.83, 47.55],
|
||||||
|
// [47.95, 47.32 ],
|
||||||
|
|
||||||
|
[48.11, 47.25],
|
||||||
|
[48.30, 47.77],
|
||||||
|
[48.17, 47.91],
|
||||||
|
[48.60, 47.90],
|
||||||
|
[48.33, 47.74],
|
||||||
|
[48.40, 48.10],
|
||||||
|
[48.55, 48.06],
|
||||||
|
[48.45, 48.07],
|
||||||
|
[48.70, 47.79],
|
||||||
|
|
||||||
|
// [48.72, 48.14 ],
|
||||||
|
// [48.90, 48.39 ],
|
||||||
|
// [48.87, 48.37 ],
|
||||||
|
// [48.82, 48.24 ],
|
||||||
|
// [49.05, 48.64 ],
|
||||||
|
// [49.20, 48.94 ],
|
||||||
|
// [49.35, 48.86 ],
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
let expectedResults_uptrend = [
|
||||||
|
{ sar: 47.25, ep: 48.30, ep_sar: 1.05, af: 0.02, af_ep_sar: 0.02 },
|
||||||
|
{ sar: 47.25, ep: 48.30, ep_sar: 1.05, af: 0.02, af_ep_sar: 0.02 },
|
||||||
|
{ sar: 47.27, ep: 48.60, ep_sar: 1.33, af: 0.04, af_ep_sar: 0.05 },
|
||||||
|
{ sar: 47.32, ep: 48.60, ep_sar: 1.28, af: 0.04, af_ep_sar: 0.05 },
|
||||||
|
{ sar: 47.38, ep: 48.60, ep_sar: 1.22, af: 0.04, af_ep_sar: 0.05 },
|
||||||
|
{ sar: 47.42, ep: 48.60, ep_sar: 1.18, af: 0.04, af_ep_sar: 0.05 },
|
||||||
|
{ sar: 47.47, ep: 48.60, ep_sar: 1.13, af: 0.04, af_ep_sar: 0.05 },
|
||||||
|
{ sar: 47.52, ep: 48.70, ep_sar: 1.18, af: 0.06, af_ep_sar: 0.07 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let data_downtrend = [
|
||||||
|
[46.59, 45.90],
|
||||||
|
[46.55, 45.38],
|
||||||
|
[46.30, 45.25],
|
||||||
|
[45.43, 43.99],
|
||||||
|
[44.55, 44.07],
|
||||||
|
[44.84, 44.00],
|
||||||
|
[44.80, 43.96],
|
||||||
|
[44.38, 43.27],
|
||||||
|
];
|
||||||
|
|
||||||
|
let expectedResults_downtrend = [
|
||||||
|
{ sar: 46.59, ep: 45.38, sar_ep: 1.21, af: 0.02, af_sar_ep: 0.024 },
|
||||||
|
{ sar: 46.56, ep: 45.25, sar_ep: 1.31, af: 0.04, af_sar_ep: 0.054 },
|
||||||
|
{ sar: 46.51, ep: 43.99, sar_ep: 2.52, af: 0.06, af_sar_ep: 0.154 },
|
||||||
|
{ sar: 46.36, ep: 43.99, sar_ep: 2.37, af: 0.06, af_sar_ep: 0.144 },
|
||||||
|
{ sar: 46.22, ep: 43.99, sar_ep: 2.23, af: 0.06, af_sar_ep: 0.136 },
|
||||||
|
{ sar: 46.10, ep: 43.96, sar_ep: 2.14, af: 0.08, af_sar_ep: 0.173 },
|
||||||
|
{ sar: 45.93, ep: 43.27, sar_ep: 2.66, af: 0.10, af_sar_ep: 0.267 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let runTest = (collection, expectedResults, direction, options = {}) => {
|
||||||
|
let roc = new ROC(options);
|
||||||
|
|
||||||
|
roc.setValues(collection);
|
||||||
|
let results = roc.calculate(direction);
|
||||||
|
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(results.length == expectedResults.length);
|
||||||
|
results.forEach((result, idx) => {
|
||||||
|
assert.closeTo(result.sar, expectedResults[idx].sar, 0.03);
|
||||||
|
assert.closeTo(result.ep, expectedResults[idx].ep, 0.02);
|
||||||
|
assert.closeTo(result.af, expectedResults[idx].af, 0.02);
|
||||||
|
|
||||||
|
if (direction == 'uptrend') {
|
||||||
|
assert.closeTo(result.ep_sar, expectedResults[idx].ep_sar, 0.03);
|
||||||
|
assert.closeTo(result.af_ep_sar, expectedResults[idx].af_ep_sar, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (direction == 'downtrend') {
|
||||||
|
assert.closeTo(result.sar_ep, expectedResults[idx].sar_ep, 0.03);
|
||||||
|
assert.closeTo(result.af_sar_ep, expectedResults[idx].af_sar_ep, 0.02);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let arr2Collection = (data) => {
|
||||||
|
return data.map((arr) => {
|
||||||
|
return { high: arr[0], low: arr[1] };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should calculate uptrend correctly and return results', () => {
|
||||||
|
runTest(arr2Collection(data_uptrend), expectedResults_uptrend, 'uptrend');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate uptrend correctly within range and return results', () => {
|
||||||
|
let dataCopy = [...data_uptrend];
|
||||||
|
dataCopy.unshift([48.05, 47.10]);
|
||||||
|
dataCopy.push([48.99, 47.99]);
|
||||||
|
runTest(arr2Collection(dataCopy), expectedResults_uptrend, 'uptrend', { startIndex: 1, endIndex: dataCopy.length - 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate downtrend correctly and return results', () => {
|
||||||
|
runTest(arr2Collection(data_downtrend), expectedResults_downtrend, 'downtrend');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate downtrend correctly within range and return results', () => {
|
||||||
|
let dataCopy = [...data_downtrend];
|
||||||
|
dataCopy.unshift([48.05, 47.10]);
|
||||||
|
dataCopy.push([48.99, 47.99]);
|
||||||
|
runTest(arr2Collection(dataCopy), expectedResults_downtrend, 'downtrend', { startIndex: 1, endIndex: dataCopy.length - 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should just test direction', () => {
|
||||||
|
let roc = new ROC();
|
||||||
|
|
||||||
|
roc.setValues(arr2Collection([...data_downtrend]));
|
||||||
|
let results = roc.calculate('uptrend');
|
||||||
|
|
||||||
|
console.log(results);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
49
test/indicator/rate-of-change.js
Executable file
49
test/indicator/rate-of-change.js
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import ROC from '../../lib/indicator/rate-of-change.js';
|
||||||
|
|
||||||
|
describe('Rate Of Change', () => {
|
||||||
|
|
||||||
|
let data = [2, 3, 5, 8, 4, 2, 6, 7];
|
||||||
|
let expectedResults = [0, 0, 0, 0, 100, -33.3, 20, -12.5];
|
||||||
|
|
||||||
|
let runTest = async (data, expectedResults, options) => {
|
||||||
|
let roc = new ROC(options);
|
||||||
|
roc.setValues(data);
|
||||||
|
let results = await roc.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(results.length == expectedResults.length);
|
||||||
|
results.forEach((result, idx) => {
|
||||||
|
assert.containsAllKeys(result, ['price', 'roc']);
|
||||||
|
assert.isNumber(result.price);
|
||||||
|
assert.isNumber(result.roc);
|
||||||
|
assert.closeTo(result.roc, expectedResults[idx], 0.1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should calculate correctly and return results', () => {
|
||||||
|
(async () => {
|
||||||
|
runTest(data, expectedResults, { periods: 4, sliceOffset: false });
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly in range and return results', () => {
|
||||||
|
(async () => {
|
||||||
|
let dataCopy = [...data];
|
||||||
|
dataCopy.unshift(3);
|
||||||
|
dataCopy.push(4);
|
||||||
|
runTest(dataCopy, expectedResults, { periods: 4, startIndex: 1, endIndex: dataCopy.length - 2, sliceOffset: false });
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with sliceOffset and return results', () => {
|
||||||
|
let e = expectedResults.slice(4);
|
||||||
|
(async () => {
|
||||||
|
runTest(data, e, { periods: 4, sliceOffset: true });
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
126
test/indicator/relative-strength-index.js
Executable file
126
test/indicator/relative-strength-index.js
Executable file
@ -0,0 +1,126 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import RSI from './../../lib/indicator/relative-strength-index.js';
|
||||||
|
|
||||||
|
describe('Relative Strength Index', () => {
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
44.34,
|
||||||
|
44.09,
|
||||||
|
44.15,
|
||||||
|
43.61,
|
||||||
|
44.33,
|
||||||
|
44.83,
|
||||||
|
45.10,
|
||||||
|
45.42,
|
||||||
|
45.84,
|
||||||
|
46.08,
|
||||||
|
45.89,
|
||||||
|
46.03,
|
||||||
|
45.61,
|
||||||
|
46.28,
|
||||||
|
46.28,
|
||||||
|
46.00,
|
||||||
|
46.03,
|
||||||
|
46.41,
|
||||||
|
46.22,
|
||||||
|
45.64,
|
||||||
|
46.21,
|
||||||
|
46.25,
|
||||||
|
45.71,
|
||||||
|
];
|
||||||
|
|
||||||
|
let runTest = async function (data, options, expectedResultLength) {
|
||||||
|
let rsi = new RSI(options);
|
||||||
|
rsi.setValues(data);
|
||||||
|
|
||||||
|
let result = await rsi.calculate();
|
||||||
|
|
||||||
|
assert.isArray(result);
|
||||||
|
assert.isTrue(result.length == expectedResultLength);
|
||||||
|
|
||||||
|
let item_14 = result[13];
|
||||||
|
|
||||||
|
assert.isNull(item_14.avg_gain);
|
||||||
|
assert.isNull(item_14.avg_loss);
|
||||||
|
assert.isNull(item_14.rs);
|
||||||
|
assert.isNull(item_14.rsi);
|
||||||
|
|
||||||
|
let last_item = result[result.length - 1];
|
||||||
|
|
||||||
|
assert.isTrue(last_item.price == 45.71);
|
||||||
|
assert.isTrue(last_item.gain == 0);
|
||||||
|
assert.closeTo(last_item.avg_gain, 0.2, 0.1);
|
||||||
|
assert.closeTo(last_item.loss, 0.5, 0.05);
|
||||||
|
assert.closeTo(last_item.rs, 1.3, 0.1);
|
||||||
|
assert.closeTo(last_item.rsi, 58, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let runTest2 = async function (data, options, expectedResultLength) {
|
||||||
|
let rsi = new RSI(options);
|
||||||
|
rsi.setValues(data);
|
||||||
|
|
||||||
|
let result = await rsi.calculate();
|
||||||
|
|
||||||
|
assert.isArray(result);
|
||||||
|
assert.isTrue(result.length == expectedResultLength);
|
||||||
|
|
||||||
|
let last_item = result[result.length - 1];
|
||||||
|
|
||||||
|
assert.isTrue(last_item.price == 45.71);
|
||||||
|
assert.isTrue(last_item.gain == 0);
|
||||||
|
assert.closeTo(last_item.avg_gain, 0.2, 0.1);
|
||||||
|
assert.closeTo(last_item.loss, 0.5, 0.05);
|
||||||
|
assert.closeTo(last_item.rs, 1.3, 0.1);
|
||||||
|
assert.closeTo(last_item.rsi, 58, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should calculate correctly and return results', async function () {
|
||||||
|
await runTest([...data], { periods: 14, sliceOffset: false, lazyEvaluation: true }, data.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly and return results without offset', async function () {
|
||||||
|
let opts = { periods: 14, sliceOffset: true, lazyEvaluation: true };
|
||||||
|
await runTest2([...data], opts, data.length - opts.periods);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with ranges and return results', async function () {
|
||||||
|
let dataCopy = [...data];
|
||||||
|
dataCopy.unshift(40.11);
|
||||||
|
dataCopy.push(45.99);
|
||||||
|
await runTest(dataCopy, { periods: 14, sliceOffset: false, startIndex: 1, endIndex: dataCopy.length - 2, lazyEvaluation: true }, dataCopy.length - 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// it('should throw error on invalid options', function (done) {
|
||||||
|
// let runTest = async (options) => {
|
||||||
|
// let failed = false;
|
||||||
|
// try {
|
||||||
|
// let rsi = new RSI(options);
|
||||||
|
// rsi.setValues(data);
|
||||||
|
// let results = await rsi.calculate();
|
||||||
|
// } catch (err) {
|
||||||
|
// if (err.name == 'Error') failed = true;
|
||||||
|
// }
|
||||||
|
// return failed;
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// (async () => {
|
||||||
|
// var b = false;
|
||||||
|
// // timeperiod above array length
|
||||||
|
// b = await runTest({ endIndex: data.length - 1, periods: data.length, lazyEvaluation: true });
|
||||||
|
// assert.isTrue(b)
|
||||||
|
//
|
||||||
|
// // focusIndex above array length
|
||||||
|
// b = await runTest({ endIndex: data.length, periods: 14, lazyEvaluation: true });
|
||||||
|
// assert.isTrue(b)
|
||||||
|
// done();
|
||||||
|
// })();
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
128
test/indicator/simple-moving-average.js
Executable file
128
test/indicator/simple-moving-average.js
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import SMA from '../../lib/indicator/simple-moving-average.js';
|
||||||
|
|
||||||
|
describe('Simple Moving Average', () => {
|
||||||
|
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
|
it('should calculate correctly and return result', (done) => {
|
||||||
|
|
||||||
|
let runTest = async (options, values, expectedResult, expectedPrice) => {
|
||||||
|
let sma = new SMA(options);
|
||||||
|
sma.setValues(values);
|
||||||
|
let results = await sma.calculate();
|
||||||
|
|
||||||
|
assert.isTrue(results.length == expectedResult.length);
|
||||||
|
assert.isArray(results)
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.isObject(item);
|
||||||
|
assert.containsAllKeys(item, ['price', 'sma']);
|
||||||
|
assert.isNumber(item.price);
|
||||||
|
assert.isNumber(item.sma);
|
||||||
|
assert.closeTo(expectedResult[idx], item.sma, 0.1);
|
||||||
|
assert.isTrue(item.price == expectedPrice[idx]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let arr = [
|
||||||
|
{ o: { periods: 4, sliceOffset: true, lazyEvaluation: true }, v: data, e: [2.5, 3.5, 4.5, 5.5, 6.5, 7.5], ep: [4, 5, 6, 7, 8, 9] },
|
||||||
|
{ o: { periods: 9, sliceOffset: true }, v: data, e: [5], ep: [9] },
|
||||||
|
{ o: { periods: 4, sliceOffset: false }, v: data, e: [0, 0, 0, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5], ep: [1, 2, 3, 4, 5, 6, 7, 8, 9] },
|
||||||
|
];
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
let item = arr[i];
|
||||||
|
await runTest(item.o, item.v, item.e, item.ep);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
|
||||||
|
// RUN PARALLEL
|
||||||
|
// (async function(){
|
||||||
|
// let promises = await arr.map( async item => {
|
||||||
|
// return await runTest( item.o, item.v, item.e, item.ep );
|
||||||
|
// });
|
||||||
|
// await Promise.all( promises );
|
||||||
|
// done()
|
||||||
|
// })();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with starIndex and endIndex options and return result', (done) => {
|
||||||
|
|
||||||
|
let runTest = async (opts, data, expectedResult) => {
|
||||||
|
let sma = new SMA(opts);
|
||||||
|
sma.setValues(data);
|
||||||
|
let results = await sma.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results)
|
||||||
|
assert.isTrue((opts.endIndex + 1 - opts.startIndex + 1) - opts.periods == results.length);
|
||||||
|
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.isObject(item);
|
||||||
|
assert.containsAllKeys(item, ['price', 'sma']);
|
||||||
|
assert.isNumber(item.price);
|
||||||
|
assert.isNumber(item.sma);
|
||||||
|
assert.closeTo(expectedResult[idx], item.sma, 0.1);
|
||||||
|
assert.isTrue(item.price == data[opts.startIndex + opts.periods - 1 + idx]);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
let opts = { periods: 4, startIndex: 1, endIndex: data.length - 2, lazyEvaluation: true, sliceOffset: true };
|
||||||
|
let expectedResult = [3.5, 4.5, 5.5, 6.5];
|
||||||
|
|
||||||
|
await runTest(opts, data, expectedResult);
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid values', () => {
|
||||||
|
assert.throws(() => SMA(), Error);
|
||||||
|
assert.throws(() => (new SMA()).calculate(), Error);
|
||||||
|
assert.throws(() => (new SMA()).setValues(1), Error);
|
||||||
|
assert.throws(() => (new SMA()).setValues('foo'), Error);
|
||||||
|
|
||||||
|
let sma = new SMA();
|
||||||
|
sma.setValues(data);
|
||||||
|
sma.clear();
|
||||||
|
assert.throws(() => sma.calculate(), Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid periods', () => {
|
||||||
|
function runTest(periods) {
|
||||||
|
let sma = new SMA({ periods, sliceOffset: true });
|
||||||
|
sma.setValues(data);
|
||||||
|
assert.throws(() => sma.calculate(), Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tp = [data.length + 100, -100, 0, 1];
|
||||||
|
tp.forEach((val) => runTest(val));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid startIndex and endIndex ', () => {
|
||||||
|
function runTest(startIndex, endIndex) {
|
||||||
|
let sma = new SMA({ startIndex, endIndex, lazyEvaluation: false, sliceOffset: true });
|
||||||
|
sma.setValues(data);
|
||||||
|
assert.throws(() => sma.calculate(), Error);
|
||||||
|
};
|
||||||
|
|
||||||
|
[[10, 5],
|
||||||
|
[5, 5],
|
||||||
|
[data.length, data.length + 1],
|
||||||
|
[-10, 2],
|
||||||
|
[0, -2],
|
||||||
|
].forEach(item => runTest(item[0], item[1]));
|
||||||
|
|
||||||
|
let sma = new SMA({ endIndex: data.length + 1 });
|
||||||
|
sma.setValues(data);
|
||||||
|
assert.throws(() => sma.calculate(), Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
41
test/indicator/smoothed-moving-average.js
Executable file
41
test/indicator/smoothed-moving-average.js
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import SMMA from '../../lib/indicator/smoothed-moving-average.js';
|
||||||
|
|
||||||
|
describe('Smoothed Moving Average', () => {
|
||||||
|
|
||||||
|
let data = [20, 21, 22, 21, 20, 19, 18, 22, 21, 22, 23, 24];
|
||||||
|
|
||||||
|
let runTest = async (data, expectedResults, options = {}) => {
|
||||||
|
let smma = new SMMA(options);
|
||||||
|
smma.setValues(data);
|
||||||
|
let results = await smma.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(results.length == expectedResults.length);
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.containsAllKeys(item, ['price', 'smma']);
|
||||||
|
assert.closeTo(item.smma, expectedResults[idx], 0.02);
|
||||||
|
assert.isTrue(item.price > 0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should calculate correctly and return results', () => {
|
||||||
|
(async () => {
|
||||||
|
let expectedResults = [21, 20.75, 20.31, 19.73, 20.3, 20.48, 20.86, 21.4, 22.05];
|
||||||
|
await runTest(data, expectedResults, { periods: 4, sliceOffset: true });
|
||||||
|
|
||||||
|
let expectedResults2 = [0, 0, 0].concat(expectedResults);
|
||||||
|
await runTest(data, expectedResults2, { periods: 4, sliceOffset: false });
|
||||||
|
|
||||||
|
let dataCopy = [...data];
|
||||||
|
dataCopy.unshift(19);
|
||||||
|
dataCopy.push(22);
|
||||||
|
await runTest(dataCopy, expectedResults2, { periods: 4, sliceOffset: false, startIndex: 1, endIndex: dataCopy.length - 2 });
|
||||||
|
})();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
106
test/indicator/stochastic-oscillator.js
Executable file
106
test/indicator/stochastic-oscillator.js
Executable file
@ -0,0 +1,106 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import SO from '../../lib/indicator/stochastic-oscillator.js';
|
||||||
|
|
||||||
|
describe('Stochastic Oscilator', () => {
|
||||||
|
|
||||||
|
it('should find the lowest and highest price', () => {
|
||||||
|
let data = [3, 5, 1, 8, 7, 2, 6, 3, 2];
|
||||||
|
|
||||||
|
let runTest = (type, startIndex, endIndex, expectedResult) => {
|
||||||
|
let so = new SO();
|
||||||
|
so.setValues(data);
|
||||||
|
|
||||||
|
let result = so._findPrice(type, startIndex, endIndex);
|
||||||
|
assert.isNumber(result);
|
||||||
|
assert.isTrue(result === expectedResult);
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
['lowest', 0, data.length - 1, 1],
|
||||||
|
['highest', 0, data.length - 1, 8],
|
||||||
|
['lowest', 3, 5, 2],
|
||||||
|
['highest', 3, 5, 8],
|
||||||
|
].forEach((item, idx) => {
|
||||||
|
runTest(item[0], item[1], item[2], item[3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly and return results', () => {
|
||||||
|
let data = [3, 5, 1, 8, 7, 2, 6, 3, 2, 3];
|
||||||
|
|
||||||
|
let expectedResults_1 = [
|
||||||
|
{ k: 85.7, d: 0, price: 7 },
|
||||||
|
{ k: 14.3, d: 0, price: 2 },
|
||||||
|
{ k: 71.4, d: 0, price: 6 },
|
||||||
|
{ k: 16.7, d: 0, price: 3 },
|
||||||
|
{ k: 0, d: 34.1, price: 2 },
|
||||||
|
{ k: 25.0, d: 29.3, price: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let expectedResults_2 = [
|
||||||
|
{ k: 25, d: 0, price: 3 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let expectedResults_3 = [
|
||||||
|
{ k: 0, d: 0, price: 3 },
|
||||||
|
{ k: 0, d: 0, price: 5 },
|
||||||
|
{ k: 0, d: 0, price: 1 },
|
||||||
|
{ k: 0, d: 0, price: 8 },
|
||||||
|
].concat(expectedResults_1);
|
||||||
|
|
||||||
|
let runTest = async (options, data, expectedResults) => {
|
||||||
|
let so = new SO(options);
|
||||||
|
so.setValues(data);
|
||||||
|
let results = await so.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(results.length == expectedResults.length);
|
||||||
|
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.isObject(item);
|
||||||
|
assert.closeTo(item.k, expectedResults[idx].k, 0.1);
|
||||||
|
assert.closeTo(item.d, expectedResults[idx].d, 0.1);
|
||||||
|
assert.isTrue(item.price == expectedResults[idx].price);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tests = [
|
||||||
|
{ o: { periods: 4, sliceOffset: true, lazyEvaluation: true }, d: data, e: expectedResults_1 },
|
||||||
|
{ o: { periods: 4, sliceOffset: true, startIndex: 5, lazyEvaluation: true }, d: data, e: expectedResults_2 },
|
||||||
|
{ o: { periods: 4, sliceOffset: false, lazyEvaluation: true }, d: data, e: expectedResults_3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < tests.length; i++) {
|
||||||
|
let item = tests[i];
|
||||||
|
runTest(item.o, item.d, item.e);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on invalid options', () => {
|
||||||
|
let data = [3, 5, 1, 8, 7, 2, 6, 3, 2];
|
||||||
|
|
||||||
|
function runTest(opts) {
|
||||||
|
opts = Object.assign({}, opts, { lazyEvaluation: false });
|
||||||
|
let so = new SO(opts);
|
||||||
|
so.setValues(data);
|
||||||
|
assert.throws(() => so.calculate(), Error);
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
{ periods: data.length + 1 },
|
||||||
|
{ startIndex: data.length + 1 },
|
||||||
|
{ startIndex: 1, periods: data.length },
|
||||||
|
{ endIndex: data.length + 1 },
|
||||||
|
{ periods: 'foo' },
|
||||||
|
{ startIndex: data.length, endIndex: 0 },
|
||||||
|
{ startIndex: 1, periods: 10 },
|
||||||
|
{ startIndex: 1, endIndex: 2 },
|
||||||
|
].forEach((item) => runTest(item));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
45
test/indicator/triple-exponential-average.1.js
Executable file
45
test/indicator/triple-exponential-average.1.js
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import _ from 'underscore';
|
||||||
|
import TRIX from '../../lib/indicator/triple-exponential-average.js';
|
||||||
|
|
||||||
|
describe('TRIX', function () {
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
102.32,
|
||||||
|
105.54,
|
||||||
|
106.59,
|
||||||
|
107.39,
|
||||||
|
107.45,
|
||||||
|
109.08,
|
||||||
|
109.07,
|
||||||
|
109.10,
|
||||||
|
106.09,
|
||||||
|
106.72,
|
||||||
|
107.90,
|
||||||
|
106.50,
|
||||||
|
108.88,
|
||||||
|
109.82,
|
||||||
|
110.97,
|
||||||
|
110.96,
|
||||||
|
110.24,
|
||||||
|
109.70,
|
||||||
|
109.68,
|
||||||
|
112.16,
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should find the lowest and highest price', () => {
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let trix = new TRIX();
|
||||||
|
trix.setValues(data);
|
||||||
|
let results = await trix.calculate();
|
||||||
|
console.log(results);
|
||||||
|
})();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
33
test/indicator/triple-exponential-average.js
Executable file
33
test/indicator/triple-exponential-average.js
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import _ from 'underscore';
|
||||||
|
import TRIX from '../../lib/indicator/triple-exponential-average.js';
|
||||||
|
|
||||||
|
describe('TRIX', function () {
|
||||||
|
|
||||||
|
let data = [
|
||||||
|
102.32,
|
||||||
|
105.54,
|
||||||
|
106.59,
|
||||||
|
107.39,
|
||||||
|
107.45,
|
||||||
|
109.08,
|
||||||
|
109.07,
|
||||||
|
109.10,
|
||||||
|
106.09,
|
||||||
|
106.72,
|
||||||
|
107.90,
|
||||||
|
106.50,
|
||||||
|
108.88,
|
||||||
|
109.82,
|
||||||
|
110.97,
|
||||||
|
110.96,
|
||||||
|
110.24,
|
||||||
|
109.70,
|
||||||
|
109.68,
|
||||||
|
112.16,
|
||||||
|
];
|
||||||
|
|
||||||
|
});
|
||||||
68
test/indicator/weighted-moving-average.js
Executable file
68
test/indicator/weighted-moving-average.js
Executable file
@ -0,0 +1,68 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import WMA from '../../lib/indicator/weighted-moving-average.js';
|
||||||
|
|
||||||
|
describe('WMA', () => {
|
||||||
|
|
||||||
|
let data = [90.91, 90.83, 90.28, 90.36, 90.90, 90.51];
|
||||||
|
|
||||||
|
let runTest = async (data, expectedResults, options = {}) => {
|
||||||
|
let wma = new WMA(options);
|
||||||
|
wma.setValues(data);
|
||||||
|
let results = await wma.calculate();
|
||||||
|
assert.isArray(results);
|
||||||
|
assert.isTrue(results.length == expectedResults.length);
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.containsAllKeys(item, ['price', 'wma']);
|
||||||
|
assert.closeTo(item.wma, expectedResults[idx], 0.02);
|
||||||
|
assert.isTrue(item.price > 0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let runTest_2 = async (opts, data, expectedResult) => {
|
||||||
|
let sma = new WMA(opts);
|
||||||
|
sma.setValues(data);
|
||||||
|
let results = await sma.calculate();
|
||||||
|
|
||||||
|
assert.isArray(results)
|
||||||
|
assert.isTrue(expectedResult.length == results.length);
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
assert.isObject(item);
|
||||||
|
assert.containsAllKeys(item, ['price', 'wma']);
|
||||||
|
assert.isNumber(item.price);
|
||||||
|
assert.isNumber(item.wma);
|
||||||
|
assert.closeTo(expectedResult[idx].wma, item.wma, 0.1);
|
||||||
|
assert.isTrue(item.price == data[opts.startIndex + opts.periods - 1 + idx]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should calculate correctly and return results', (done) => {
|
||||||
|
(async () => {
|
||||||
|
let expectedResults = [90.62, 90.57];
|
||||||
|
await runTest(data, expectedResults, { periods: 5, sliceOffset: true });
|
||||||
|
|
||||||
|
let expectedResults2 = [0, 0, 0, 0].concat(expectedResults);
|
||||||
|
await runTest(data, expectedResults2, { periods: 5, sliceOffset: false });
|
||||||
|
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correctly with starIndex and endIndex options and return result', (done) => {
|
||||||
|
(async function () {
|
||||||
|
let opts = { periods: 2, startIndex: 1, endIndex: data.length - 2, lazyEvaluation: true, sliceOffset: true };
|
||||||
|
|
||||||
|
let expectedResult = [
|
||||||
|
{ price: 90.28, wma: 90.46 },
|
||||||
|
{ price: 90.36, wma: 90.33 },
|
||||||
|
{ price: 90.9, wma: 90.72 }
|
||||||
|
];
|
||||||
|
await runTest_2(opts, data, expectedResult);
|
||||||
|
done();
|
||||||
|
})();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
67
test/utils/math.js
Executable file
67
test/utils/math.js
Executable file
@ -0,0 +1,67 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
import MathUtil from '../../lib/utils/math.js';
|
||||||
|
const assert = chai.assert;
|
||||||
|
|
||||||
|
describe('Math', () => {
|
||||||
|
|
||||||
|
it('should calculate sum', () => {
|
||||||
|
assert.isTrue(MathUtil.sum([1, 2, 3, 4]) == 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should calculate average', () => {
|
||||||
|
assert.isTrue(MathUtil.average([1, 2, 3, 4]) == 2.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should calculate median', () => {
|
||||||
|
assert.isTrue(MathUtil.median([1, 1, 2, 3, 16, 1, 2]) == 2);
|
||||||
|
assert.isTrue(MathUtil.median([1, 1, 2, 2, 3, 3, 3, 16]) == 2.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate variance', () => {
|
||||||
|
let items_1 = [1, 2, 3, 4, 5];
|
||||||
|
let items_2 = [1, 2, 3, 4, 5, 1];
|
||||||
|
|
||||||
|
//Sample Mode
|
||||||
|
assert.isTrue(MathUtil.variance(items_1) == 2.5);
|
||||||
|
assert.isTrue(MathUtil.variance(items_2).toFixed(2) == 2.67);
|
||||||
|
|
||||||
|
//Population Mode
|
||||||
|
assert.isTrue(MathUtil.variance(items_1, 'population') == 2);
|
||||||
|
assert.isTrue(MathUtil.variance(items_2, 'population').toFixed(2) == 2.22);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate standard deviation', () => {
|
||||||
|
let items_1 = [1, 2, 3, 4, 5];
|
||||||
|
let items_2 = [1, 2, 3, 4, 5, 1];
|
||||||
|
|
||||||
|
//Sample Mode
|
||||||
|
assert.isTrue(MathUtil.standardDeviation(items_1).toFixed(2) == 1.58);
|
||||||
|
assert.isTrue(MathUtil.standardDeviation(items_2).toFixed(2) == 1.63);
|
||||||
|
|
||||||
|
//Population Mode
|
||||||
|
assert.isTrue(MathUtil.standardDeviation(items_1, 'population').toFixed(2) == 1.41);
|
||||||
|
assert.isTrue(MathUtil.standardDeviation(items_2, 'population').toFixed(2) == 1.49);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate the mean deviation', () => {
|
||||||
|
let items = [3, 6, 6, 7, 8, 11, 15, 16];
|
||||||
|
assert.isTrue(MathUtil.meanDeviation(items) == 3.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate range', () => {
|
||||||
|
let items = [8, 11, 5, 9, 7, 6, 3616];
|
||||||
|
assert.isTrue(MathUtil.range(items) == 3611);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate mode', () => {
|
||||||
|
let items = [2, 10, 21, 23, 23, 38, 38, 4];
|
||||||
|
let result = MathUtil.modes(items);
|
||||||
|
assert.isTrue(result.length == 2);
|
||||||
|
assert.isTrue(result.indexOf(23) >= 0 && result.indexOf(38) >= 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
30
test/utils/misc.js
Executable file
30
test/utils/misc.js
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import Misc from '../../lib/utils/misc.js';
|
||||||
|
|
||||||
|
describe('Misc Utils', () => {
|
||||||
|
|
||||||
|
it('should check if object has propety', () => {
|
||||||
|
assert.isTrue(Misc.has({ foo: 'bar' }, 'foo'));
|
||||||
|
assert.isTrue(Misc.has({ 'foo': 'bar' }, 'foo'));
|
||||||
|
assert.isTrue(Misc.has({ foo: null }, 'foo'));
|
||||||
|
assert.isFalse(Misc.has({ foo: 'bar' }, 'bar'));
|
||||||
|
assert.isFalse(Misc.has(123, 'bar'));
|
||||||
|
assert.isFalse(Misc.has('123', 'bar'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extends objects', () => {
|
||||||
|
let obj_1 = { a: 'a', b: 'b' };
|
||||||
|
let obj_2 = { b: 2, c: 'c' };
|
||||||
|
let result = Misc.extends(obj_1, obj_2);
|
||||||
|
|
||||||
|
assert.isObject(result);
|
||||||
|
assert.containsAllKeys(result, ['a', 'b', 'c']);
|
||||||
|
assert.isTrue(result.a === obj_1.a);
|
||||||
|
assert.isTrue(result.b === obj_2.b);
|
||||||
|
assert.isTrue(result.c === obj_1.c);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
41
test/utils/number.js
Executable file
41
test/utils/number.js
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
import NumberUtil from '../../lib/utils/number.js';
|
||||||
|
const assert = chai.assert;
|
||||||
|
|
||||||
|
describe('Number', function () {
|
||||||
|
|
||||||
|
it('should detected nunmeric value', () => {
|
||||||
|
assert.isTrue(NumberUtil.isNumeric(-1));
|
||||||
|
assert.isTrue(NumberUtil.isNumeric(-1.12345));
|
||||||
|
assert.isTrue(NumberUtil.isNumeric(0));
|
||||||
|
assert.isTrue(NumberUtil.isNumeric(1));
|
||||||
|
assert.isTrue(NumberUtil.isNumeric(1.0236));
|
||||||
|
assert.isTrue(NumberUtil.isNumeric('0'));
|
||||||
|
assert.isTrue(NumberUtil.isNumeric('1'));
|
||||||
|
assert.isTrue(NumberUtil.isNumeric('1.123'));
|
||||||
|
assert.isFalse(NumberUtil.isNumeric('lol'));
|
||||||
|
assert.isFalse(NumberUtil.isNumeric('10a'));
|
||||||
|
assert.isFalse(NumberUtil.isNumeric(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should round floats to 2 places', function () {
|
||||||
|
|
||||||
|
var cases = [
|
||||||
|
{ n: 10, e: 10, p: 2 },
|
||||||
|
{ n: 1.7777, e: 1.78, p: 2 },
|
||||||
|
{ n: 1.005, e: 1.01, p: 2 },
|
||||||
|
{ n: 1.005, e: 1, p: 0 },
|
||||||
|
{ n: 1.77777, e: 1.8, p: 1 }
|
||||||
|
]
|
||||||
|
|
||||||
|
cases.forEach(function (testCase) {
|
||||||
|
//use default rounding
|
||||||
|
var r = NumberUtil.roundTo(testCase.n, testCase.p, 'default');
|
||||||
|
assert.equal(r, testCase.e, 'didn\'t get right number');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
138
test/utils/type.js
Executable file
138
test/utils/type.js
Executable file
@ -0,0 +1,138 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import chai from 'chai';
|
||||||
|
const assert = chai.assert;
|
||||||
|
import TypeUtil from '../../lib/utils/type.js';
|
||||||
|
|
||||||
|
describe('Type util', () => {
|
||||||
|
|
||||||
|
it('should check if value is null', () => {
|
||||||
|
assert.isTrue(TypeUtil.isNull(null));
|
||||||
|
|
||||||
|
assert.isFalse(TypeUtil.isNull(0));
|
||||||
|
assert.isFalse(TypeUtil.isNull(undefined));
|
||||||
|
assert.isFalse(TypeUtil.isNull(''));
|
||||||
|
assert.isFalse(TypeUtil.isNull(false));
|
||||||
|
assert.isFalse(TypeUtil.isNull(NaN));
|
||||||
|
assert.isFalse(TypeUtil.isNull([]));
|
||||||
|
assert.isFalse(TypeUtil.isNull({}));
|
||||||
|
assert.isFalse(TypeUtil.isNull(() => { }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if value is number', () => {
|
||||||
|
assert.isTrue(TypeUtil.isNumber(0));
|
||||||
|
assert.isTrue(TypeUtil.isNumber(1));
|
||||||
|
assert.isTrue(TypeUtil.isNumber(-1));
|
||||||
|
assert.isTrue(TypeUtil.isNumber(1.234));
|
||||||
|
assert.isTrue(TypeUtil.isNumber(2e64));
|
||||||
|
|
||||||
|
assert.isFalse(TypeUtil.isNumber(undefined));
|
||||||
|
assert.isFalse(TypeUtil.isNumber(''));
|
||||||
|
assert.isFalse(TypeUtil.isNumber('0'));
|
||||||
|
assert.isFalse(TypeUtil.isNumber('1'));
|
||||||
|
assert.isFalse(TypeUtil.isNumber('-1'));
|
||||||
|
assert.isFalse(TypeUtil.isNumber(true));
|
||||||
|
assert.isFalse(TypeUtil.isNumber(false));
|
||||||
|
assert.isFalse(TypeUtil.isNumber(NaN));
|
||||||
|
assert.isFalse(TypeUtil.isNumber(null));
|
||||||
|
assert.isFalse(TypeUtil.isNumber([]));
|
||||||
|
assert.isFalse(TypeUtil.isNumber({}));
|
||||||
|
assert.isFalse(TypeUtil.isNumber(() => { }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if value is string', () => {
|
||||||
|
assert.isTrue(TypeUtil.isString(''));
|
||||||
|
assert.isTrue(TypeUtil.isString('lol'));
|
||||||
|
assert.isTrue(TypeUtil.isString('0'));
|
||||||
|
assert.isTrue(TypeUtil.isString('false'));
|
||||||
|
assert.isTrue(TypeUtil.isString(' '));
|
||||||
|
assert.isTrue(TypeUtil.isString(String('100')));
|
||||||
|
|
||||||
|
assert.isFalse(TypeUtil.isString(undefined));
|
||||||
|
assert.isFalse(TypeUtil.isString(true));
|
||||||
|
assert.isFalse(TypeUtil.isString(false));
|
||||||
|
assert.isFalse(TypeUtil.isString(NaN));
|
||||||
|
assert.isFalse(TypeUtil.isString(null));
|
||||||
|
assert.isFalse(TypeUtil.isString([]));
|
||||||
|
assert.isFalse(TypeUtil.isString({}));
|
||||||
|
assert.isFalse(TypeUtil.isString(() => { }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if value is object', () => {
|
||||||
|
assert.isTrue(TypeUtil.isObject({}));
|
||||||
|
|
||||||
|
assert.isFalse(TypeUtil.isObject(''));
|
||||||
|
assert.isFalse(TypeUtil.isObject('a'));
|
||||||
|
assert.isFalse(TypeUtil.isObject(123));
|
||||||
|
assert.isFalse(TypeUtil.isObject([]));
|
||||||
|
assert.isFalse(TypeUtil.isObject(undefined));
|
||||||
|
assert.isFalse(TypeUtil.isObject(true));
|
||||||
|
assert.isFalse(TypeUtil.isObject(false));
|
||||||
|
assert.isFalse(TypeUtil.isObject(NaN));
|
||||||
|
assert.isFalse(TypeUtil.isObject(null));
|
||||||
|
assert.isFalse(TypeUtil.isObject(() => { }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if value is boolean', () => {
|
||||||
|
assert.isTrue(TypeUtil.isBoolean(true));
|
||||||
|
assert.isTrue(TypeUtil.isBoolean(false));
|
||||||
|
|
||||||
|
assert.isFalse(TypeUtil.isBoolean(''));
|
||||||
|
assert.isFalse(TypeUtil.isBoolean('a'));
|
||||||
|
assert.isFalse(TypeUtil.isBoolean(123));
|
||||||
|
assert.isFalse(TypeUtil.isBoolean([]));
|
||||||
|
assert.isFalse(TypeUtil.isBoolean(undefined));
|
||||||
|
assert.isFalse(TypeUtil.isBoolean(NaN));
|
||||||
|
assert.isFalse(TypeUtil.isBoolean(null));
|
||||||
|
assert.isFalse(TypeUtil.isBoolean({}));
|
||||||
|
assert.isFalse(TypeUtil.isBoolean(() => { }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if value is undefined', () => {
|
||||||
|
let a;
|
||||||
|
assert.isTrue(TypeUtil.isUndefined(a));
|
||||||
|
assert.isTrue(TypeUtil.isUndefined(undefined));
|
||||||
|
|
||||||
|
assert.isFalse(TypeUtil.isUndefined(true));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined(false));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined(''));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined('a'));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined(123));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined([]));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined(NaN));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined(null));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined({}));
|
||||||
|
assert.isFalse(TypeUtil.isUndefined(() => { }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if value is NaN', () => {
|
||||||
|
assert.isTrue(TypeUtil.isNaN(NaN));
|
||||||
|
|
||||||
|
assert.isFalse(TypeUtil.isNaN(undefined));
|
||||||
|
assert.isFalse(TypeUtil.isNaN(true));
|
||||||
|
assert.isFalse(TypeUtil.isNaN(false));
|
||||||
|
assert.isFalse(TypeUtil.isNaN(''));
|
||||||
|
assert.isFalse(TypeUtil.isNaN('a'));
|
||||||
|
assert.isFalse(TypeUtil.isNaN(123));
|
||||||
|
assert.isFalse(TypeUtil.isNaN([]));
|
||||||
|
assert.isFalse(TypeUtil.isNaN({}));
|
||||||
|
assert.isFalse(TypeUtil.isNaN(null));
|
||||||
|
assert.isFalse(TypeUtil.isNaN(() => { }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if value is function', () => {
|
||||||
|
let a = () => { };
|
||||||
|
assert.isTrue(TypeUtil.isFunction(a));
|
||||||
|
|
||||||
|
assert.isFalse(TypeUtil.isFunction(undefined));
|
||||||
|
assert.isFalse(TypeUtil.isFunction(true));
|
||||||
|
assert.isFalse(TypeUtil.isFunction(false));
|
||||||
|
assert.isFalse(TypeUtil.isFunction(''));
|
||||||
|
assert.isFalse(TypeUtil.isFunction('a'));
|
||||||
|
assert.isFalse(TypeUtil.isFunction(123));
|
||||||
|
assert.isFalse(TypeUtil.isFunction([]));
|
||||||
|
assert.isFalse(TypeUtil.isFunction({}));
|
||||||
|
assert.isFalse(TypeUtil.isFunction(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
18
tests.js
Executable file
18
tests.js
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
import './test/utils/math.js';
|
||||||
|
import './test/utils/number.js';
|
||||||
|
import './test/utils/type.js';
|
||||||
|
import './test/utils/misc.js';
|
||||||
|
import './test/indicator/simple-moving-average.js';
|
||||||
|
import './test/indicator/exponential-moving-average.js';
|
||||||
|
import './test/indicator/linearly-weighted-moving-average.js';
|
||||||
|
import './test/indicator/relative-strength-index.js';
|
||||||
|
import './test/indicator/bollinger-bands.js';
|
||||||
|
import './test/indicator/average-true-range.js';
|
||||||
|
import './test/indicator/rate-of-change.js';
|
||||||
|
import './test/indicator/on-balance-volume.js';
|
||||||
|
import './test/indicator/stochastic-oscillator.js';
|
||||||
|
import './test/indicator/moving-average-convergence-divergence.js';
|
||||||
|
import './test/indicator/money-flow-index.js';
|
||||||
|
import './test/indicator/weighted-moving-average.js';
|
||||||
|
import './test/indicator/accumulation-distribution-line.js';
|
||||||
|
import './test/indicator/average-directional-index.js';
|
||||||
Loading…
Reference in New Issue
Block a user