ohlcv-indicators 2.1.5 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +95 -70
- package/package.json +1 -1
- package/src/moving-averages/bollingerBands.js +26 -7
- package/src/moving-averages/donchianChannel.js +39 -21
- package/src/moving-averages/macd.js +65 -61
- package/src/moving-averages/movingAverages.js +160 -0
- package/src/moving-averages/relativeVolume.js +15 -2
- package/src/oscillators/rsi.js +39 -16
- package/src/oscillators/volumeOscillator.js +13 -5
- package/src/studies/candleStudies.js +32 -31
- package/src/utilities/classification.js +42 -30
- package/src/utilities/{precision.js → numberUtilities.js} +11 -0
- package/src/utilities/parsing-utilities.js +3 -6
- package/src/moving-averages/ema.js +0 -32
- package/src/moving-averages/sma.js +0 -33
package/index.js
CHANGED
|
@@ -3,7 +3,10 @@ import { correlation } from './src/studies/correlation.js'
|
|
|
3
3
|
import { setIndicatorsFromInputParams } from './src/utilities/setIndicatorsFromInputParams.js'
|
|
4
4
|
import { validateDate } from './src/utilities/validators.js'
|
|
5
5
|
import { isAlreadyComputed } from './src/utilities/validators.js'
|
|
6
|
-
import { divideByMultiplier } from './src/utilities/
|
|
6
|
+
import { divideByMultiplier } from './src/utilities/numberUtilities.js'
|
|
7
|
+
import { getMovingAveragesParams } from './src/moving-averages/movingAverages.js'
|
|
8
|
+
|
|
9
|
+
const validMagnitudeValues = [0.01, 0.02, 0.025, 0.05, 0.1, 0.20, 0.25, 0.5, 1]
|
|
7
10
|
|
|
8
11
|
export default class OHLCV_INDICATORS {
|
|
9
12
|
constructor({input, ticker = null, precision = true}) {
|
|
@@ -206,7 +209,7 @@ export default class OHLCV_INDICATORS {
|
|
|
206
209
|
{
|
|
207
210
|
const key = colKeys[x]
|
|
208
211
|
|
|
209
|
-
if(this.priceBased.find(v => key
|
|
212
|
+
if(this.priceBased.find(v => key === v))
|
|
210
213
|
{
|
|
211
214
|
this.priceBased.push(key)
|
|
212
215
|
}
|
|
@@ -215,11 +218,18 @@ export default class OHLCV_INDICATORS {
|
|
|
215
218
|
return this;
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
relativeVolume(size) {
|
|
221
|
+
relativeVolume(size, options = {}) {
|
|
219
222
|
|
|
220
223
|
isAlreadyComputed(this)
|
|
221
224
|
|
|
222
|
-
|
|
225
|
+
const {scale = null} = options
|
|
226
|
+
|
|
227
|
+
if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
|
|
228
|
+
|
|
229
|
+
throw new Error(`"scale" value in relativeVolume must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.inputParams.push({key: 'relativeVolume', params: [size, {scale}]})
|
|
223
233
|
|
|
224
234
|
return this
|
|
225
235
|
}
|
|
@@ -228,19 +238,9 @@ export default class OHLCV_INDICATORS {
|
|
|
228
238
|
|
|
229
239
|
isAlreadyComputed(this)
|
|
230
240
|
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
throw new Error('"options" must be an object in ema. eg: {target, height, range}');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const {target = 'close'} = options
|
|
237
|
-
|
|
238
|
-
if (typeof size !== 'number' || size <= 0) {
|
|
239
|
-
throw new Error('"size" must be a positive number in ema.');
|
|
240
|
-
}
|
|
241
|
+
const optionArgs = getMovingAveragesParams('ema', size, options, validMagnitudeValues)
|
|
241
242
|
|
|
242
|
-
this.inputParams.push({key: '
|
|
243
|
-
this.priceBased.push(`ema_${size}`)
|
|
243
|
+
this.inputParams.push({key: 'movingAverages', params: ['ema', size, optionArgs]})
|
|
244
244
|
|
|
245
245
|
return this
|
|
246
246
|
}
|
|
@@ -248,23 +248,14 @@ export default class OHLCV_INDICATORS {
|
|
|
248
248
|
|
|
249
249
|
isAlreadyComputed(this)
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
{
|
|
253
|
-
throw new Error('"options" must be an object in sma. eg: {target, height, range}');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const {target = 'close'} = options
|
|
257
|
-
|
|
258
|
-
if (typeof size !== 'number' || size <= 0) {
|
|
259
|
-
throw new Error('"size" must be a positive number in sma.');
|
|
260
|
-
}
|
|
261
|
-
|
|
251
|
+
const optionArgs = getMovingAveragesParams('sma', size, options, validMagnitudeValues)
|
|
262
252
|
|
|
263
|
-
this.inputParams.push({key: '
|
|
264
|
-
this.priceBased.push(`sma_${size}`)
|
|
253
|
+
this.inputParams.push({key: 'movingAverages', params: ['sma', size, optionArgs]})
|
|
265
254
|
|
|
266
|
-
return this
|
|
255
|
+
return this
|
|
267
256
|
}
|
|
257
|
+
|
|
258
|
+
|
|
268
259
|
macd(fast = 12, slow = 26, signal = 9, options = {}) {
|
|
269
260
|
|
|
270
261
|
isAlreadyComputed(this)
|
|
@@ -300,7 +291,7 @@ export default class OHLCV_INDICATORS {
|
|
|
300
291
|
throw new Error('"options" must be an object in bollingerBands. eg: {target, height, range}');
|
|
301
292
|
}
|
|
302
293
|
|
|
303
|
-
const {target = 'close', height = false, range = [], zScore = []} = options
|
|
294
|
+
const {target = 'close', height = false, range = [], zScore = [], scale = null} = options
|
|
304
295
|
|
|
305
296
|
// Validate size and times
|
|
306
297
|
if (typeof size !== 'number' || size <= 0) {
|
|
@@ -317,15 +308,23 @@ export default class OHLCV_INDICATORS {
|
|
|
317
308
|
}
|
|
318
309
|
if (typeof height !== 'boolean') {
|
|
319
310
|
throw new Error('"height" must be a boolean in bollingerBands.');
|
|
320
|
-
}
|
|
311
|
+
}
|
|
312
|
+
else
|
|
313
|
+
{
|
|
314
|
+
if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
|
|
315
|
+
|
|
316
|
+
throw new Error(`"scale" value in bollingerBands must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
321
|
|
|
322
|
-
this.inputParams.push({key: 'bollingerBands', params: [size, stdDev, {target, height, range, zScore}]});
|
|
323
|
-
this.priceBased.push('bollinger_bands_middle', 'bollinger_bands_upper', 'bollinger_bands_lower');
|
|
322
|
+
this.inputParams.push({key: 'bollingerBands', params: [size, stdDev, {target, height, scale, range, zScore}]});
|
|
324
323
|
|
|
325
324
|
return this;
|
|
326
325
|
}
|
|
327
326
|
|
|
328
|
-
rsi(size)
|
|
327
|
+
rsi(size, options = {})
|
|
329
328
|
{
|
|
330
329
|
isAlreadyComputed(this)
|
|
331
330
|
|
|
@@ -334,71 +333,97 @@ export default class OHLCV_INDICATORS {
|
|
|
334
333
|
throw new Error('"size" must be a positive number in rsi.');
|
|
335
334
|
}
|
|
336
335
|
|
|
337
|
-
|
|
336
|
+
const {scale = null, target = 'close'} = options
|
|
337
|
+
|
|
338
|
+
if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
|
|
339
|
+
|
|
340
|
+
throw new Error(`"scale" value in rsi must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
this.inputParams.push({key: 'rsi', params: [size, {scale, target}]})
|
|
338
344
|
|
|
339
345
|
return this
|
|
340
346
|
}
|
|
341
|
-
donchianChannels(size = 20, offset = 0, options = {})
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
347
|
+
donchianChannels(size = 20, offset = 0, options = {}) {
|
|
348
|
+
isAlreadyComputed(this);
|
|
349
|
+
|
|
345
350
|
if (typeof size !== 'number' || size <= 0) {
|
|
346
|
-
|
|
351
|
+
throw new Error('"size" must be a positive number greater than 0 in donchianChannels.');
|
|
347
352
|
}
|
|
348
|
-
|
|
353
|
+
|
|
349
354
|
if (typeof offset !== 'number' || offset < 0) {
|
|
350
|
-
|
|
355
|
+
throw new Error('"offset" must be a number greater than or equal to 0 in donchianChannels.');
|
|
351
356
|
}
|
|
352
|
-
|
|
353
|
-
const {height = false, range = []} = options
|
|
354
|
-
|
|
357
|
+
|
|
358
|
+
const { height = false, range = [], scale = null } = options;
|
|
359
|
+
|
|
355
360
|
if (!Array.isArray(range)) {
|
|
356
|
-
|
|
361
|
+
throw new Error('If set, "range" must be an array of column names in donchianChannels.');
|
|
357
362
|
}
|
|
358
|
-
|
|
363
|
+
|
|
359
364
|
if (typeof height !== 'boolean') {
|
|
360
|
-
|
|
361
|
-
}
|
|
365
|
+
throw new Error('"height" must be a boolean in donchianChannels.');
|
|
366
|
+
}
|
|
367
|
+
else
|
|
368
|
+
{
|
|
362
369
|
|
|
363
|
-
|
|
364
|
-
this.priceBased.push('donchian_channel_upper', 'donchian_channel_lower', 'donchian_channel_basis')
|
|
370
|
+
if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
|
|
365
371
|
|
|
366
|
-
|
|
372
|
+
throw new Error(`"scale" value in donchianChannels must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.inputParams.push({ key: 'donchianChannels', params: [size, offset, { height, range, scale }] });
|
|
377
|
+
|
|
378
|
+
return this;
|
|
367
379
|
}
|
|
368
|
-
|
|
369
|
-
{
|
|
370
|
-
|
|
371
|
-
isAlreadyComputed(this)
|
|
380
|
+
|
|
372
381
|
|
|
382
|
+
candleStudies(size = 20, stdDev = 2, options = {}) {
|
|
383
|
+
isAlreadyComputed(this);
|
|
384
|
+
|
|
373
385
|
if (typeof size !== 'number' || size <= 0) {
|
|
374
|
-
|
|
386
|
+
throw new Error('"size" must be a positive number greater than 0 in candleStudies.');
|
|
375
387
|
}
|
|
388
|
+
|
|
389
|
+
if (typeof stdDev !== 'number' || stdDev <= 0) {
|
|
390
|
+
throw new Error('"stdDev" must be a positive number greater than 0 in candleStudies.');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const { lag = 0, scale = 0.05 } = options;
|
|
376
394
|
|
|
377
|
-
const {stdDev = 2, lag = 0} = options
|
|
378
395
|
|
|
379
|
-
if
|
|
380
|
-
{
|
|
381
|
-
throw new Error('"stdDev" must be a positive number greater than 0 in candleStudies.');
|
|
382
|
-
}
|
|
396
|
+
if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
|
|
383
397
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
398
|
+
throw new Error(`"scale" value in candleStudies must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this.inputParams.push({ key: 'candleStudies', params: [size, stdDev, {lag, scale}] });
|
|
402
|
+
return this;
|
|
403
|
+
}
|
|
404
|
+
|
|
387
405
|
|
|
388
|
-
volumeOscillator(fastSize = 5, slowSize = 10)
|
|
406
|
+
volumeOscillator(fastSize = 5, slowSize = 10, options = {})
|
|
389
407
|
{
|
|
390
408
|
|
|
391
409
|
isAlreadyComputed(this)
|
|
392
410
|
|
|
393
|
-
if (typeof fastSize !== 'number' || fastSize <=
|
|
394
|
-
throw new Error('fastSize" must be a positive number in volumeOscillator.');
|
|
411
|
+
if (typeof fastSize !== 'number' || fastSize <= 1) {
|
|
412
|
+
throw new Error('fastSize" must be a positive number greater than 1 in volumeOscillator.');
|
|
395
413
|
}
|
|
396
414
|
|
|
397
415
|
if (typeof slowSize !== 'number' || slowSize <= fastSize) {
|
|
398
416
|
throw new Error('"slowSize" must be a positive number greater than the "fastSize" in volumeOscillator.');
|
|
399
417
|
}
|
|
400
418
|
|
|
401
|
-
|
|
419
|
+
const {scale = null} = options
|
|
420
|
+
|
|
421
|
+
if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
|
|
422
|
+
|
|
423
|
+
throw new Error(`"scale" value in volumeOscillator must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this.inputParams.push({key: 'volumeOscillator', params: [fastSize, slowSize, {scale}]})
|
|
402
427
|
return this
|
|
403
428
|
}
|
|
404
429
|
dateTime()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ohlcv-indicators",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
4
4
|
"description": "A JavaScript library featuring optimized trading indicators for OHLCV arrays [{open, high, low, close, value}]. This library is optimized for backtesting and trading bot development.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { FasterBollingerBands } from 'trading-signals'
|
|
2
|
+
import { calcMagnitude } from '../utilities/numberUtilities.js';
|
|
2
3
|
|
|
3
4
|
const defaultTarget = 'close'
|
|
4
5
|
|
|
5
|
-
export const bollingerBands = (main, index, size, stdDev,
|
|
6
|
+
export const bollingerBands = (main, index, size, stdDev, { height, range = [], zScore = [], target, scale }) => {
|
|
6
7
|
// Destructure options ensure range and zScore default to empty arrays if not provided.
|
|
7
8
|
|
|
8
9
|
const { verticalOhlcv, instances, lastIndexReplace } = main;
|
|
9
|
-
const { height, range = [], zScore = [], target } = options
|
|
10
10
|
const suffix = target === defaultTarget ? '' : `_${target}`
|
|
11
11
|
const indicatorKey = `${size}_${stdDev}${suffix}`
|
|
12
12
|
let prefix // Will be computed during initialization
|
|
@@ -47,10 +47,12 @@ export const bollingerBands = (main, index, size, stdDev, options) => {
|
|
|
47
47
|
...(height && { [`${prefix}_height`]: [...nullArray] })
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
main.priceBased.push(`${prefix}_upper`, `${prefix}_middle`, `${prefix}_lower`)
|
|
51
|
+
|
|
50
52
|
// Set up additional arrays for each range property.
|
|
51
53
|
for (const rangeKey of range) {
|
|
52
54
|
if (!(rangeKey in verticalOhlcv) || !priceBased.includes(rangeKey)) {
|
|
53
|
-
throw new Error(`Invalid range item value "${rangeKey}" property for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(
|
|
55
|
+
throw new Error(`Invalid range item value "${rangeKey}" property for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`)
|
|
54
56
|
}
|
|
55
57
|
ohlcvSetup[`${prefix}_range_${rangeKey}`] = [...nullArray]
|
|
56
58
|
}
|
|
@@ -58,7 +60,7 @@ export const bollingerBands = (main, index, size, stdDev, options) => {
|
|
|
58
60
|
// Set up additional arrays for each zScore property.
|
|
59
61
|
for (const zScoreKey of zScore) {
|
|
60
62
|
if (!(zScoreKey in verticalOhlcv) || !priceBased.includes(zScoreKey)) {
|
|
61
|
-
throw new Error(`Invalid zScore item value "${zScoreKey}" for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(
|
|
63
|
+
throw new Error(`Invalid zScore item value "${zScoreKey}" for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`)
|
|
62
64
|
}
|
|
63
65
|
ohlcvSetup[`${prefix}_zscore_${zScoreKey}`] = [...nullArray]
|
|
64
66
|
}
|
|
@@ -97,23 +99,40 @@ export const bollingerBands = (main, index, size, stdDev, options) => {
|
|
|
97
99
|
|
|
98
100
|
// Process height if a height instance exists.
|
|
99
101
|
if (height) {
|
|
100
|
-
|
|
102
|
+
let heightValue = ((upper - lower) / lower)
|
|
103
|
+
|
|
104
|
+
if(scale)
|
|
105
|
+
{
|
|
106
|
+
heightValue = calcMagnitude(heightValue, scale)
|
|
107
|
+
}
|
|
108
|
+
|
|
101
109
|
main.pushToMain({index, key: `${subPrefix}_height`, value: heightValue})
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
// Process each range property.
|
|
105
113
|
for (const rangeKey of range) {
|
|
106
|
-
|
|
114
|
+
let rangeValue = (verticalOhlcv[rangeKey][index] - lower) / (upper - lower)
|
|
115
|
+
|
|
116
|
+
if(scale)
|
|
117
|
+
{
|
|
118
|
+
rangeValue = calcMagnitude(rangeValue, scale)
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
main.pushToMain({index, key: `${subPrefix}_range_${rangeKey}`, value: rangeValue})
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
// Process each zScore property.
|
|
111
125
|
for (const zScoreKey of zScore) {
|
|
112
126
|
const denominator = upper - lower
|
|
113
|
-
|
|
127
|
+
let zScoreValue = denominator !== 0
|
|
114
128
|
? (2 * stdDev * (verticalOhlcv[zScoreKey][index] - middle)) / denominator
|
|
115
129
|
: 0
|
|
116
130
|
|
|
131
|
+
if(denominator !== 0 && scale)
|
|
132
|
+
{
|
|
133
|
+
zScoreValue = calcMagnitude(zScoreValue, scale)
|
|
134
|
+
}
|
|
135
|
+
|
|
117
136
|
main.pushToMain({index, key: `${subPrefix}_zscore_${zScoreKey}`, value: zScoreValue})
|
|
118
137
|
}
|
|
119
138
|
|
|
@@ -1,41 +1,47 @@
|
|
|
1
|
+
import { calcMagnitude } from "../utilities/numberUtilities.js"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const donchianChannels = (main, index, size, offset, { height, range, scale}) => {
|
|
1
5
|
|
|
2
|
-
export const donchianChannels = (main, index, size, offset, options) => {
|
|
3
|
-
const { height, range } = options
|
|
4
6
|
const indicatorKey = `${size}_${offset}`
|
|
7
|
+
const {verticalOhlcv, instances, len} = main
|
|
5
8
|
|
|
6
9
|
// Initialization: create output arrays and indicator instance on the first call.
|
|
7
10
|
if (index === 0) {
|
|
8
|
-
const
|
|
11
|
+
const {inputParams, nullArray, priceBased} = main
|
|
12
|
+
const numberOfIndicators = inputParams.filter(o => o.key === 'donchianChannels').length
|
|
9
13
|
const prefix = numberOfIndicators > 1 ? `donchian_channel_${indicatorKey}` : 'donchian_channel'
|
|
10
14
|
|
|
11
|
-
Object.assign(
|
|
12
|
-
[`${prefix}_upper`]: [...
|
|
13
|
-
[`${prefix}_basis`]: [...
|
|
14
|
-
[`${prefix}_lower`]: [...
|
|
15
|
-
...(height && { [`${prefix}_height`]: [...
|
|
15
|
+
Object.assign(verticalOhlcv, {
|
|
16
|
+
[`${prefix}_upper`]: [...nullArray],
|
|
17
|
+
[`${prefix}_basis`]: [...nullArray],
|
|
18
|
+
[`${prefix}_lower`]: [...nullArray],
|
|
19
|
+
...(height && { [`${prefix}_height`]: [...nullArray] })
|
|
16
20
|
})
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
priceBased.push(`${prefix}_upper`, `${prefix}_basis`, `${prefix}_lower`);
|
|
23
|
+
|
|
24
|
+
if (!instances.hasOwnProperty('donchian_channel')) {
|
|
25
|
+
instances.donchian_channel = { numberOfIndicators, settings: {} }
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
// Set up additional arrays for each range property.
|
|
23
29
|
for (const rangeKey of range) {
|
|
24
|
-
if (!(rangeKey in
|
|
25
|
-
throw new Error(`Invalid range item value "${rangeKey}" property for donchianChannels. Only price based key names are accepted:\n${JSON.stringify(
|
|
30
|
+
if (!(rangeKey in verticalOhlcv) || !priceBased.includes(rangeKey)) {
|
|
31
|
+
throw new Error(`Invalid range item value "${rangeKey}" property for donchianChannels. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`)
|
|
26
32
|
}
|
|
27
|
-
|
|
33
|
+
verticalOhlcv[`${prefix}_range_${rangeKey}`] = [...nullArray]
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
instances.donchian_channel.settings[indicatorKey] = {
|
|
31
37
|
maxDeque: [], // will hold indices for highs in descending order
|
|
32
38
|
minDeque: [] // will hold indices for lows in ascending order
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
const numberOfIndicators =
|
|
42
|
+
const numberOfIndicators = instances.donchian_channel.numberOfIndicators
|
|
37
43
|
const subPrefix = numberOfIndicators > 1 ? `donchian_channel_${indicatorKey}` : 'donchian_channel'
|
|
38
|
-
const state =
|
|
44
|
+
const state = instances.donchian_channel.settings[indicatorKey]
|
|
39
45
|
const { maxDeque, minDeque } = state
|
|
40
46
|
|
|
41
47
|
// Compute the “current bar index” for the window.
|
|
@@ -45,10 +51,10 @@ export const donchianChannels = (main, index, size, offset, options) => {
|
|
|
45
51
|
const startIdx = endIdx - size // window: [startIdx, endIdx)
|
|
46
52
|
|
|
47
53
|
// If the window is not fully available, exit early.
|
|
48
|
-
if (startIdx < 0 || endIdx >
|
|
54
|
+
if (startIdx < 0 || endIdx > len) return true
|
|
49
55
|
|
|
50
|
-
const highs =
|
|
51
|
-
const lows =
|
|
56
|
+
const highs = verticalOhlcv.high
|
|
57
|
+
const lows = verticalOhlcv.low
|
|
52
58
|
|
|
53
59
|
// **Update the maximum deque:**
|
|
54
60
|
// Remove indices that are out of the current window.
|
|
@@ -86,13 +92,25 @@ export const donchianChannels = (main, index, size, offset, options) => {
|
|
|
86
92
|
main.pushToMain({index, key: `${subPrefix}_lower`, value: lower})
|
|
87
93
|
|
|
88
94
|
if (height) {
|
|
89
|
-
|
|
95
|
+
let heightValue = ((upper - lower) / lower)
|
|
96
|
+
|
|
97
|
+
if(scale)
|
|
98
|
+
{
|
|
99
|
+
heightValue = calcMagnitude(heightValue, scale)
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
main.pushToMain({index, key: `${subPrefix}_height`, value: heightValue})
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
// Process each range property.
|
|
94
106
|
for (const rangeKey of range) {
|
|
95
|
-
|
|
107
|
+
let rangeValue = (verticalOhlcv[rangeKey][index] - lower) / (upper - lower)
|
|
108
|
+
|
|
109
|
+
if(scale)
|
|
110
|
+
{
|
|
111
|
+
rangeValue = calcMagnitude(rangeValue, scale)
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
main.pushToMain({index, key: `${subPrefix}_range_${rangeKey}`, value: rangeValue})
|
|
97
115
|
}
|
|
98
116
|
|
|
@@ -1,73 +1,77 @@
|
|
|
1
|
-
import {FasterEMA, FasterMACD} from 'trading-signals';
|
|
1
|
+
import { FasterEMA, FasterMACD } from 'trading-signals';
|
|
2
2
|
|
|
3
|
-
const defaultTarget = 'close'
|
|
3
|
+
const defaultTarget = 'close';
|
|
4
4
|
|
|
5
5
|
export const macd = (main, index, fast, slow, signal, options) => {
|
|
6
|
+
const { target } = options;
|
|
7
|
+
const { verticalOhlcv, instances } = main;
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if(!main.verticalOhlcv.hasOwnProperty(target))
|
|
14
|
-
{
|
|
15
|
-
throw new Error(`Target property ${target} not found in verticalOhlcv for macd.`)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const numberOfIndicators = main.inputParams.filter(o => o.key === 'macd').length;
|
|
19
|
-
const prefix =
|
|
20
|
-
numberOfIndicators > 1
|
|
21
|
-
? `macd_${indicatorKey}`
|
|
22
|
-
: `macd${suffix}`;
|
|
23
|
-
|
|
24
|
-
main.crossPairsList.push({fast: `${prefix}_diff`, slow: `${prefix}_dea`, isDefault: true});
|
|
25
|
-
|
|
26
|
-
if(!main.instances.hasOwnProperty('macd'))
|
|
27
|
-
{
|
|
28
|
-
main.instances.macd = {
|
|
29
|
-
numberOfIndicators,
|
|
30
|
-
settings: {}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
main.instances.macd.settings[indicatorKey] = new FasterMACD(
|
|
35
|
-
new FasterEMA(fast),
|
|
36
|
-
new FasterEMA(slow),
|
|
37
|
-
new FasterEMA(signal)
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
Object.assign(main.verticalOhlcv, {
|
|
41
|
-
[`${prefix}_diff`]: [...main.nullArray],
|
|
42
|
-
[`${prefix}_dea`]: [...main.nullArray],
|
|
43
|
-
[`${prefix}_histogram`]: [...main.nullArray],
|
|
44
|
-
})
|
|
45
|
-
}
|
|
9
|
+
// Instance key (for internal storage) includes target if not default.
|
|
10
|
+
const instanceKey = `${fast}_${slow}_${signal}${target === defaultTarget ? '' : `_${target}`}`;
|
|
11
|
+
|
|
12
|
+
// On the first index, do the initialization.
|
|
13
|
+
if (index === 0) {
|
|
14
|
+
const { inputParams, crossPairsList, nullArray } = main;
|
|
46
15
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? `macd_${indicatorKey}`
|
|
50
|
-
: `macd${suffix}`;
|
|
51
|
-
|
|
52
|
-
const macdInstance = main.instances.macd.settings[indicatorKey];
|
|
53
|
-
const value = main.verticalOhlcv[target][index]
|
|
54
|
-
macdInstance.update(value, main.lastIndexReplace);
|
|
55
|
-
|
|
56
|
-
let macdResult;
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
macdResult = macdInstance.getResult();
|
|
60
|
-
} catch (err) {
|
|
61
|
-
//do nothing
|
|
16
|
+
if (!verticalOhlcv.hasOwnProperty(target)) {
|
|
17
|
+
throw new Error(`Target property ${target} not found in verticalOhlcv for macd.`);
|
|
62
18
|
}
|
|
63
19
|
|
|
20
|
+
const numberOfIndicators = inputParams.filter(o => o.key === 'macd').length;
|
|
21
|
+
// For display keys: if there's more than one indicator, include the parameters in the prefix.
|
|
22
|
+
const displayPrefix = numberOfIndicators > 1 ? `macd_${fast}_${slow}_${signal}` : 'macd';
|
|
23
|
+
|
|
24
|
+
// Build the keys: if target is not the default, append it at the end.
|
|
25
|
+
const diffKey = target === defaultTarget ? `${displayPrefix}_diff` : `${displayPrefix}_diff_${target}`;
|
|
26
|
+
const deaKey = target === defaultTarget ? `${displayPrefix}_dea` : `${displayPrefix}_dea_${target}`;
|
|
27
|
+
const histogramKey = target === defaultTarget ? `${displayPrefix}_histogram` : `${displayPrefix}_histogram_${target}`;
|
|
64
28
|
|
|
65
|
-
|
|
29
|
+
crossPairsList.push({ fast: diffKey, slow: deaKey, isDefault: true });
|
|
66
30
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
31
|
+
if (!instances.hasOwnProperty('macd')) {
|
|
32
|
+
instances.macd = {
|
|
33
|
+
numberOfIndicators,
|
|
34
|
+
settings: {}
|
|
35
|
+
};
|
|
70
36
|
}
|
|
71
37
|
|
|
72
|
-
|
|
38
|
+
instances.macd.settings[instanceKey] = new FasterMACD(
|
|
39
|
+
new FasterEMA(fast),
|
|
40
|
+
new FasterEMA(slow),
|
|
41
|
+
new FasterEMA(signal)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
Object.assign(verticalOhlcv, {
|
|
45
|
+
[diffKey]: [...nullArray],
|
|
46
|
+
[deaKey]: [...nullArray],
|
|
47
|
+
[histogramKey]: [...nullArray],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const {numberOfIndicators, settings} = instances.macd
|
|
52
|
+
|
|
53
|
+
// Determine display prefix for updating values.
|
|
54
|
+
const displayPrefix = numberOfIndicators > 1 ? `macd_${fast}_${slow}_${signal}` : 'macd';
|
|
55
|
+
const diffKey = target === defaultTarget ? `${displayPrefix}_diff` : `${displayPrefix}_diff_${target}`;
|
|
56
|
+
const deaKey = target === defaultTarget ? `${displayPrefix}_dea` : `${displayPrefix}_dea_${target}`;
|
|
57
|
+
const histogramKey = target === defaultTarget ? `${displayPrefix}_histogram` : `${displayPrefix}_histogram_${target}`;
|
|
58
|
+
|
|
59
|
+
const macdInstance = settings[instanceKey];
|
|
60
|
+
const value = verticalOhlcv[target][index];
|
|
61
|
+
macdInstance.update(value, main.lastIndexReplace);
|
|
62
|
+
|
|
63
|
+
let macdResult;
|
|
64
|
+
try {
|
|
65
|
+
macdResult = macdInstance.getResult();
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Do nothing if no result is available.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (macdResult) {
|
|
71
|
+
main.pushToMain({ index, key: diffKey, value: macdResult.macd });
|
|
72
|
+
main.pushToMain({ index, key: deaKey, value: macdResult.signal });
|
|
73
|
+
main.pushToMain({ index, key: histogramKey, value: macdResult.histogram });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return true;
|
|
73
77
|
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
|
|
2
|
+
import {FasterEMA, FasterSMA, FasterBollingerBands} from 'trading-signals';
|
|
3
|
+
import { classifyBoll } from '../utilities/classification.js'
|
|
4
|
+
|
|
5
|
+
const indicatorClasses = {ema: FasterEMA, sma: FasterSMA}
|
|
6
|
+
|
|
7
|
+
export const movingAverages = (main, index, indicatorName, size, {target, diff}) => {
|
|
8
|
+
|
|
9
|
+
const {verticalOhlcv, instances, priceBased, lastIndexReplace} = main
|
|
10
|
+
let suffix = (typeof target === 'string' && verticalOhlcv.hasOwnProperty(target) && target !== 'close') ? `_${target}` : ''
|
|
11
|
+
const keyName = `${indicatorName}_${size}${suffix}`
|
|
12
|
+
const diffKeyName = `${indicatorName}_${size}_diff${suffix}`
|
|
13
|
+
|
|
14
|
+
if(index === 0)
|
|
15
|
+
{
|
|
16
|
+
const {nullArray} = main
|
|
17
|
+
|
|
18
|
+
if(!verticalOhlcv.hasOwnProperty(target))
|
|
19
|
+
{
|
|
20
|
+
throw new Error(`Target property ${target} not found in verticalOhlcv for ${indicatorName}.`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
instances[keyName] = {
|
|
24
|
+
maInstance: new indicatorClasses[indicatorName](size)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
verticalOhlcv[keyName] = [...nullArray]
|
|
28
|
+
|
|
29
|
+
if(diff !== null)
|
|
30
|
+
{
|
|
31
|
+
instances[keyName].diffInstance = new FasterBollingerBands(diff.size, diff.stdDev)
|
|
32
|
+
|
|
33
|
+
const lagDiffArr = []
|
|
34
|
+
|
|
35
|
+
for(const targetKey of diff.targets)
|
|
36
|
+
{
|
|
37
|
+
verticalOhlcv[`${diffKeyName}_${targetKey}`] = [...nullArray]
|
|
38
|
+
|
|
39
|
+
if(diff.lag > 0)
|
|
40
|
+
{
|
|
41
|
+
lagDiffArr.push(`${diffKeyName}_${targetKey}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if(diff.lag > 0)
|
|
46
|
+
{
|
|
47
|
+
console.log(lagDiffArr, diff.lag)
|
|
48
|
+
main.lag(lagDiffArr, diff.lag)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
priceBased.push(keyName)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const value = verticalOhlcv[target][index]
|
|
56
|
+
const {maInstance, diffInstance} = instances[keyName]
|
|
57
|
+
maInstance.update(value, lastIndexReplace)
|
|
58
|
+
|
|
59
|
+
let currMa
|
|
60
|
+
|
|
61
|
+
try{
|
|
62
|
+
currMa = maInstance.getResult()
|
|
63
|
+
} catch(err)
|
|
64
|
+
{
|
|
65
|
+
//do nothing
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if(currMa)
|
|
69
|
+
{
|
|
70
|
+
main.pushToMain({index, key: keyName, value: currMa})
|
|
71
|
+
|
|
72
|
+
if(diff === null)
|
|
73
|
+
{
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for(const targetKey of diff.targets)
|
|
78
|
+
{
|
|
79
|
+
|
|
80
|
+
const diffValue = (target === targetKey) ? value - currMa : verticalOhlcv[targetKey][index]
|
|
81
|
+
|
|
82
|
+
diffInstance.update(Math.abs(diffValue), lastIndexReplace)
|
|
83
|
+
|
|
84
|
+
let diffBoll
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
diffBoll = diffInstance.getResult()
|
|
88
|
+
} catch(err)
|
|
89
|
+
{
|
|
90
|
+
diffBoll = null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main.pushToMain({index, key: `${diffKeyName}_${targetKey}`, value: classifyBoll(diffValue, diffBoll, diff.scale)})
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
export const getMovingAveragesParams = (indicatorName, size, options, validMagnitudeValues) => {
|
|
106
|
+
if (!options || typeof options !== 'object') {
|
|
107
|
+
throw new Error(`"options" must be an object in ${indicatorName}. eg: {target, height, range}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let { target = 'close', diff = null } = options;
|
|
111
|
+
|
|
112
|
+
if(typeof target !== 'string')
|
|
113
|
+
{
|
|
114
|
+
throw new Error(`"target" must be a valid keyName ${indicatorName}.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof size !== 'number' || size <= 0) {
|
|
118
|
+
throw new Error(`"size" must be a positive number in ${indicatorName}.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//diff can be null, true, false or an object with optional {stdDev, scale, targets, and size}
|
|
122
|
+
const optionArgs = { target, diff: null };
|
|
123
|
+
|
|
124
|
+
if (diff !== null && (typeof diff === 'object' || (typeof diff === 'boolean' && diff === true))) {
|
|
125
|
+
|
|
126
|
+
diff = (diff === true) ? ({}) : diff
|
|
127
|
+
|
|
128
|
+
const {
|
|
129
|
+
stdDev = 2,
|
|
130
|
+
scale = 0.05,
|
|
131
|
+
targets = ['close'],
|
|
132
|
+
size: diffSize = size,
|
|
133
|
+
lag = 0
|
|
134
|
+
} = diff;
|
|
135
|
+
|
|
136
|
+
if (typeof stdDev !== 'number' || stdDev <= 0) {
|
|
137
|
+
throw new Error(`"diff.stdDev" must be a number greater than 0 in ${indicatorName}.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof scale !== 'number' || !validMagnitudeValues.includes(scale)) {
|
|
141
|
+
throw new Error(`"diff.scale" must be a number between 0.01 and 1 in ${indicatorName}.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof diffSize !== 'number' || !Number.isInteger(diffSize) || diffSize <= 0) {
|
|
145
|
+
throw new Error(`"diff.size" must be an integer greater than or equal to size in ${indicatorName}.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!Array.isArray(targets)) {
|
|
149
|
+
throw new Error(`"diff.targest" must be an array of keyNames with at least 1 item in ${indicatorName}.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof lag !== 'number' || !Number.isInteger(lag) || lag < 0) {
|
|
153
|
+
throw new Error(`"diff.lag" must be an integer greater than 0 in ${indicatorName}.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
optionArgs.diff = { stdDev, scale, size: diffSize, targets, lag };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return optionArgs;
|
|
160
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {FasterSMA} from 'trading-signals';
|
|
2
|
+
import { calcMagnitude } from '../utilities/numberUtilities.js';
|
|
2
3
|
|
|
3
|
-
export const relativeVolume = (main, index, size = 10) => {
|
|
4
|
+
export const relativeVolume = (main, index, size = 10, {scale}) => {
|
|
4
5
|
|
|
5
6
|
const key = `relative_volume_${size}`
|
|
6
7
|
|
|
@@ -32,10 +33,22 @@ export const relativeVolume = (main, index, size = 10) => {
|
|
|
32
33
|
|
|
33
34
|
const {prevRelativeVolumeSma} = main.instances[key]
|
|
34
35
|
|
|
36
|
+
let currRelativeVolume = null
|
|
37
|
+
|
|
38
|
+
if(smaValue !== null && prevRelativeVolumeSma !== null)
|
|
39
|
+
{
|
|
40
|
+
currRelativeVolume = value / prevRelativeVolumeSma
|
|
41
|
+
|
|
42
|
+
if(scale)
|
|
43
|
+
{
|
|
44
|
+
currRelativeVolume = calcMagnitude(currRelativeVolume, scale)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
main.pushToMain({
|
|
36
49
|
index,
|
|
37
50
|
key,
|
|
38
|
-
value:
|
|
51
|
+
value: currRelativeVolume
|
|
39
52
|
})
|
|
40
53
|
|
|
41
54
|
main.instances[key].prevRelativeVolumeSma = smaValue
|
package/src/oscillators/rsi.js
CHANGED
|
@@ -1,47 +1,65 @@
|
|
|
1
1
|
import {FasterRSI} from 'trading-signals';
|
|
2
2
|
import { FasterSMA } from 'trading-signals';
|
|
3
|
+
import { calcMagnitude } from '../utilities/numberUtilities.js';
|
|
3
4
|
|
|
5
|
+
const defaultTarget = 'close'
|
|
4
6
|
|
|
5
|
-
export const rsi = (main, index, size) => {
|
|
7
|
+
export const rsi = (main, index, size, {scale, target}) => {
|
|
6
8
|
|
|
7
|
-
const
|
|
9
|
+
const {verticalOhlcv, instances, lastIndexReplace} = main
|
|
10
|
+
const suffix = (target === defaultTarget) ? '' : `_${target}`
|
|
11
|
+
const rsiKey = `rsi_${size}${suffix}`
|
|
12
|
+
const rsiSmaKey = `rsi_sma_${size}${suffix}`
|
|
8
13
|
|
|
9
14
|
if(index === 0)
|
|
10
15
|
{
|
|
11
|
-
|
|
16
|
+
const {crossPairsList, nullArray} = main
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
if(!verticalOhlcv.hasOwnProperty(target))
|
|
19
|
+
{
|
|
20
|
+
throw new Error(`Target property ${target} not found in verticalOhlcv for rsi.`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
crossPairsList.push({fast: rsiKey, slow: rsiSmaKey, isDefault: true})
|
|
24
|
+
|
|
25
|
+
Object.assign(instances, {
|
|
26
|
+
[rsiKey]: new FasterRSI(size),
|
|
27
|
+
[rsiSmaKey]: new FasterSMA(size)
|
|
16
28
|
})
|
|
17
29
|
|
|
18
|
-
Object.assign(
|
|
19
|
-
[
|
|
20
|
-
[
|
|
30
|
+
Object.assign(verticalOhlcv, {
|
|
31
|
+
[rsiKey]: [...nullArray],
|
|
32
|
+
[rsiSmaKey]: [...nullArray]
|
|
21
33
|
})
|
|
22
34
|
}
|
|
23
|
-
|
|
35
|
+
|
|
36
|
+
const value = verticalOhlcv[target][index]
|
|
24
37
|
let currentRsi
|
|
25
38
|
let smoothedRsi
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
instances[rsiKey].update(value, lastIndexReplace)
|
|
28
41
|
|
|
29
42
|
try
|
|
30
43
|
{
|
|
31
|
-
currentRsi =
|
|
44
|
+
currentRsi = instances[rsiKey].getResult()
|
|
32
45
|
} catch(err) {
|
|
33
46
|
currentRsi = null
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
if(currentRsi)
|
|
37
50
|
{
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
if(scale)
|
|
52
|
+
{
|
|
53
|
+
currentRsi = calcMagnitude(currentRsi, scale)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main.pushToMain({index, key: rsiKey, value: currentRsi})
|
|
57
|
+
instances[rsiSmaKey].update(currentRsi, lastIndexReplace)
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
try
|
|
43
61
|
{
|
|
44
|
-
smoothedRsi =
|
|
62
|
+
smoothedRsi = instances[rsiSmaKey].getResult()
|
|
45
63
|
} catch(err)
|
|
46
64
|
{
|
|
47
65
|
smoothedRsi = null
|
|
@@ -49,7 +67,12 @@ export const rsi = (main, index, size) => {
|
|
|
49
67
|
|
|
50
68
|
if(smoothedRsi)
|
|
51
69
|
{
|
|
52
|
-
|
|
70
|
+
if(scale)
|
|
71
|
+
{
|
|
72
|
+
smoothedRsi = calcMagnitude(smoothedRsi, scale)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main.pushToMain({index, key: rsiSmaKey, value: smoothedRsi})
|
|
53
76
|
}
|
|
54
77
|
|
|
55
78
|
return true
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FasterEMA } from 'trading-signals';
|
|
2
|
+
import { calcMagnitude } from '../utilities/numberUtilities.js';
|
|
2
3
|
|
|
3
|
-
export const volumeOscillator = (main, index, fast, slow) => {
|
|
4
|
+
export const volumeOscillator = (main, index, fast, slow, {scale}) => {
|
|
4
5
|
|
|
5
6
|
const value = main.verticalOhlcv.volume[index]
|
|
6
7
|
const key = 'volume_oscillator'
|
|
@@ -38,12 +39,19 @@ export const volumeOscillator = (main, index, fast, slow) => {
|
|
|
38
39
|
slowValue = null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
let volumeOscValue = null
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if(typeof fastValue === 'number' && typeof slowValue === 'number' && slowValue !== 0)
|
|
45
|
+
{
|
|
46
|
+
volumeOscValue = 100 * (fastValue - slowValue) / slowValue
|
|
47
|
+
|
|
48
|
+
if(scale)
|
|
49
|
+
{
|
|
50
|
+
volumeOscValue = calcMagnitude(volumeOscValue, scale)
|
|
51
|
+
}
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
main.pushToMain({index, key, value: volumeOscValue})
|
|
55
|
+
|
|
48
56
|
return true;
|
|
49
57
|
};
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { FasterSMA, FasterBollingerBands } from 'trading-signals'
|
|
2
2
|
import { classifyBoll } from '../utilities/classification.js'
|
|
3
3
|
|
|
4
|
-
const diff = (a, b) =>
|
|
5
|
-
const value = a - b
|
|
6
|
-
return {value: Math.abs(value), positive: value >= 0}
|
|
7
|
-
}
|
|
4
|
+
const diff = (a, b) => a - b
|
|
8
5
|
|
|
9
|
-
export const candleStudies = (main, index, size, stdDev, lag) => {
|
|
6
|
+
export const candleStudies = (main, index, size, stdDev, {lag, scale}) => {
|
|
10
7
|
|
|
11
8
|
const { verticalOhlcv, instances, lastIndexReplace } = main
|
|
12
9
|
|
|
@@ -14,16 +11,12 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
|
|
|
14
11
|
{
|
|
15
12
|
const {nullArray} = main
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
candle_high: [...nullArray],
|
|
24
|
-
candle_low: [...nullArray],
|
|
25
|
-
candle_open: [...nullArray],
|
|
26
|
-
})
|
|
14
|
+
const candleParts = ['candle_body', 'candle_top', 'candle_bottom']
|
|
15
|
+
const candleGaps = ['candle_gap']
|
|
16
|
+
const candleChanges = ['candle_change_close', 'candle_change_high', 'candle_change_low', 'candle_change_open']
|
|
17
|
+
const keyNames = [...candleParts, ...candleGaps, ...candleChanges]
|
|
18
|
+
|
|
19
|
+
Object.assign(verticalOhlcv, Object.fromEntries(keyNames.map(k => [k, nullArray])))
|
|
27
20
|
|
|
28
21
|
Object.assign(instances, {
|
|
29
22
|
candleStudies: {
|
|
@@ -37,7 +30,6 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
|
|
|
37
30
|
|
|
38
31
|
if(lag > 0)
|
|
39
32
|
{
|
|
40
|
-
const keyNames = ['candle_body', 'candle_gap', 'candle_close', 'candle_high', 'candle_low', 'candle_open', 'candle_top', 'candle_bottom']
|
|
41
33
|
main.lag(keyNames, lag)
|
|
42
34
|
}
|
|
43
35
|
|
|
@@ -57,11 +49,15 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
|
|
|
57
49
|
const currClose = verticalOhlcv.close[index]
|
|
58
50
|
|
|
59
51
|
|
|
60
|
-
//
|
|
52
|
+
//parts
|
|
61
53
|
const bodySize = diff(currClose, currOpen)
|
|
62
54
|
const topSize = diff(currHigh, Math.max(currOpen, currClose))
|
|
63
55
|
const bottomSize = diff(Math.min(currOpen, currClose), currLow)
|
|
64
|
-
|
|
56
|
+
|
|
57
|
+
//gaps
|
|
58
|
+
const gapOpenClose = diff(prevClose, currOpen)
|
|
59
|
+
|
|
60
|
+
//changes
|
|
65
61
|
const closeChange = diff(currClose, prevClose)
|
|
66
62
|
const highChange = diff(currHigh, prevHigh)
|
|
67
63
|
const lowChange = diff(currLow, prevLow)
|
|
@@ -82,11 +78,11 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
|
|
|
82
78
|
} = instances.candleStudies
|
|
83
79
|
|
|
84
80
|
// Update instances with current absolute values
|
|
85
|
-
bodyInstance.update(bodySize
|
|
86
|
-
closeInstance.update(closeChange
|
|
87
|
-
highInstance.update(highChange
|
|
88
|
-
lowInstance.update(lowChange
|
|
89
|
-
openInstance.update(openChange
|
|
81
|
+
bodyInstance.update(Math.abs(bodySize), lastIndexReplace)
|
|
82
|
+
closeInstance.update(Math.abs(closeChange), lastIndexReplace)
|
|
83
|
+
highInstance.update(Math.abs(highChange), lastIndexReplace)
|
|
84
|
+
lowInstance.update(Math.abs(lowChange), lastIndexReplace)
|
|
85
|
+
openInstance.update(Math.abs(openChange), lastIndexReplace)
|
|
90
86
|
|
|
91
87
|
try {
|
|
92
88
|
bodyBoll = bodyInstance.getResult()
|
|
@@ -99,15 +95,20 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
|
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
|
|
102
|
-
main.pushToMain({index, key: 'candle_close', value: classifyBoll(closeChange, closeBoll)})
|
|
103
|
-
main.pushToMain({index, key: 'candle_high', value: classifyBoll(highChange, highBoll)})
|
|
104
|
-
main.pushToMain({index, key: 'candle_low', value: classifyBoll(lowChange, lowBoll)})
|
|
105
|
-
main.pushToMain({index, key: 'candle_open', value: classifyBoll(openChange, openBoll)})
|
|
106
98
|
|
|
107
|
-
|
|
108
|
-
main.pushToMain({index, key: '
|
|
109
|
-
main.pushToMain({index, key: 'candle_top', value: classifyBoll(topSize, bodyBoll)})
|
|
110
|
-
main.pushToMain({index, key: 'candle_bottom', value: classifyBoll(bottomSize, bodyBoll)})
|
|
99
|
+
//parts
|
|
100
|
+
main.pushToMain({index, key: 'candle_body', value: classifyBoll(bodySize, bodyBoll, scale)})
|
|
101
|
+
main.pushToMain({index, key: 'candle_top', value: classifyBoll(topSize, bodyBoll, scale)})
|
|
102
|
+
main.pushToMain({index, key: 'candle_bottom', value: classifyBoll(bottomSize, bodyBoll, scale)})
|
|
103
|
+
|
|
104
|
+
//gaps
|
|
105
|
+
main.pushToMain({index, key: 'candle_gap', value: classifyBoll(gapOpenClose, bodyBoll, scale)})
|
|
106
|
+
|
|
107
|
+
//changes
|
|
108
|
+
main.pushToMain({index, key: 'candle_change_close', value: classifyBoll(closeChange, closeBoll, scale)})
|
|
109
|
+
main.pushToMain({index, key: 'candle_change_high', value: classifyBoll(highChange, highBoll, scale)})
|
|
110
|
+
main.pushToMain({index, key: 'candle_change_low', value: classifyBoll(lowChange, lowBoll, scale)})
|
|
111
|
+
main.pushToMain({index, key: 'candle_change_open', value: classifyBoll(openChange, openBoll, scale)})
|
|
111
112
|
|
|
112
113
|
return true
|
|
113
114
|
}
|
|
@@ -1,36 +1,48 @@
|
|
|
1
|
+
import { calcMagnitude } from "./numberUtilities.js"
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Classifies a given value relative to
|
|
4
|
+
* Classifies a given value relative to provided Bollinger band thresholds.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* The function evaluates the input value against the lower, middle, and upper thresholds
|
|
7
|
+
* of the Bollinger bands. If the value is below the lower threshold, it returns 0.
|
|
8
|
+
* If the value exceeds the upper threshold, it returns 1 (or -1 if the value is negative).
|
|
9
|
+
* For values within the bands, it calculates a normalized magnitude between 0 and 1 (or 0 and -1 for negatives)
|
|
10
|
+
* based on the position within the range, rounded to one decimal place.
|
|
11
|
+
* Additionally, if the value is nearly equal to the middle threshold (within a small tolerance),
|
|
12
|
+
* the function returns 0.5 or -0.5 depending on the sign of the value.
|
|
13
|
+
*
|
|
14
|
+
* @param {number} value - The value to classify.
|
|
15
|
+
* @param {Object} bollingerBands - An object containing Bollinger band thresholds.
|
|
8
16
|
* @param {number} bollingerBands.upper - The upper band threshold.
|
|
9
17
|
* @param {number} bollingerBands.middle - The middle band value.
|
|
10
18
|
* @param {number} bollingerBands.lower - The lower band threshold.
|
|
11
|
-
* @returns {number|null} The classified value, or null if inputs are invalid.
|
|
19
|
+
* @returns {number|null} The classified value, or null if inputs are invalid or if upper equals lower.
|
|
12
20
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export const classifyBoll = (value, bollingerBands, scale = 0.05) => {
|
|
24
|
+
// Validate that value and bollingerBands are not null/undefined
|
|
25
|
+
if (value == null || bollingerBands == null) return null
|
|
26
|
+
|
|
27
|
+
const positive = value >= 0
|
|
28
|
+
|
|
29
|
+
const { upper, middle, lower } = bollingerBands
|
|
30
|
+
|
|
31
|
+
// Handle values outside the Bollinger bands
|
|
32
|
+
if (value < lower) return 0
|
|
33
|
+
if (value > upper) return positive ? 1 : -1
|
|
34
|
+
|
|
35
|
+
// Use a tolerance for floating point comparisons
|
|
36
|
+
const epsilon = Number.EPSILON * Math.max(Math.abs(value), Math.abs(middle))
|
|
37
|
+
if (Math.abs(value - middle) < epsilon) return positive ? 0.5 : -0.5
|
|
38
|
+
|
|
39
|
+
// Prevent division by zero if upper and lower are equal
|
|
40
|
+
if (upper === lower) return null
|
|
41
|
+
|
|
42
|
+
const rangeValue = (value - lower) / (upper - lower)
|
|
43
|
+
let magnitude = calcMagnitude(rangeValue, scale)
|
|
44
|
+
|
|
45
|
+
magnitude = Math.min(Math.max(magnitude, 0), 1)
|
|
46
|
+
|
|
47
|
+
return positive ? magnitude : -magnitude
|
|
48
|
+
}
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
export const calcMagnitude = (number, magnitudeLevel = 0.1) => {
|
|
3
|
+
|
|
4
|
+
const factor = 1 / magnitudeLevel
|
|
5
|
+
|
|
6
|
+
return Math.round(number * factor) / factor
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Helper to clean non-numeric characters (except "-" at the start and decimal point)
|
|
10
|
+
export const cleanNumStr = str => str.replace(/(?!^-)[^0-9.]/g, '');
|
|
11
|
+
|
|
1
12
|
export const divideByMultiplier = ({row, precisionMultiplier, priceBased}) => {
|
|
2
13
|
const output = {}
|
|
3
14
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { validateInputParams } from "./validators.js";
|
|
2
2
|
import { rsi } from "../oscillators/rsi.js";
|
|
3
|
-
import {
|
|
4
|
-
import { ema } from "../moving-averages/ema.js";
|
|
3
|
+
import { movingAverages } from "../moving-averages/movingAverages.js";
|
|
5
4
|
import { macd } from "../moving-averages/macd.js";
|
|
6
5
|
import { relativeVolume } from "../moving-averages/relativeVolume.js";
|
|
7
6
|
import { donchianChannels } from "../moving-averages/donchianChannel.js";
|
|
@@ -13,16 +12,14 @@ import { crossPairs } from "../studies/findCrosses.js";
|
|
|
13
12
|
import { dateTime } from "../studies/dateTime.js";
|
|
14
13
|
import { priceVariations } from "../studies/priceVariations.js";
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
const cleanNumStr = str => str.replace(/(?!^-)[^0-9.]/g, '');
|
|
15
|
+
import { cleanNumStr } from "./numberUtilities.js";
|
|
18
16
|
|
|
19
17
|
// Map indicator keys to their respective functions
|
|
20
18
|
const indicatorFunctions = {
|
|
21
19
|
dateTime,
|
|
22
20
|
priceVariations,
|
|
23
21
|
rsi,
|
|
24
|
-
|
|
25
|
-
ema,
|
|
22
|
+
movingAverages,
|
|
26
23
|
macd,
|
|
27
24
|
relativeVolume,
|
|
28
25
|
donchianChannels,
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import {FasterEMA} from 'trading-signals'
|
|
3
|
-
|
|
4
|
-
export const ema = (main, index, size, target) => {
|
|
5
|
-
|
|
6
|
-
const value = main.verticalOhlcv.close[index]
|
|
7
|
-
|
|
8
|
-
let prefix = (typeof target === 'string' && main.verticalOhlcv.hasOwnProperty(target) && target !== 'close') ? `${target}_` : ''
|
|
9
|
-
const keyName = `${prefix}ema_${size}`
|
|
10
|
-
|
|
11
|
-
if(index === 0)
|
|
12
|
-
{
|
|
13
|
-
main.instances[keyName] = new FasterEMA(size)
|
|
14
|
-
main.verticalOhlcv[keyName] = [...main.nullArray]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let currEma
|
|
18
|
-
main.instances[keyName].update(value, main.lastIndexReplace)
|
|
19
|
-
|
|
20
|
-
try{
|
|
21
|
-
currEma = main.instances[keyName].getResult()
|
|
22
|
-
} catch(err)
|
|
23
|
-
{
|
|
24
|
-
currEma = null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if(currEma)
|
|
28
|
-
{
|
|
29
|
-
main.pushToMain({index, key: keyName, value: currEma})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import {FasterSMA} from 'trading-signals';
|
|
3
|
-
|
|
4
|
-
export const sma = (main, index, size, target) => {
|
|
5
|
-
|
|
6
|
-
const value = main.verticalOhlcv.close[index]
|
|
7
|
-
|
|
8
|
-
let prefix = (typeof target === 'string' && main.verticalOhlcv.hasOwnProperty(target) && target !== 'close') ? `${target}_` : ''
|
|
9
|
-
const keyName = `${prefix}sma_${size}`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if(index === 0)
|
|
13
|
-
{
|
|
14
|
-
main.instances[keyName] = new FasterSMA(size)
|
|
15
|
-
main.verticalOhlcv[keyName] = [...main.nullArray]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let currSma
|
|
19
|
-
main.instances[keyName].update(value, main.lastIndexReplace)
|
|
20
|
-
|
|
21
|
-
try{
|
|
22
|
-
currSma = main.instances[keyName].getResult()
|
|
23
|
-
} catch(err)
|
|
24
|
-
{
|
|
25
|
-
//do nothing
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if(currSma)
|
|
29
|
-
{
|
|
30
|
-
main.pushToMain({index, key: keyName, value: currSma})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
}
|