ohlcv-indicators 3.5.79 → 3.5.80
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/AGENTS.md +23 -15
- package/README.md +104 -63
- package/dist/ohlcv-indicators.min.js +1 -1
- package/dist/ohlcv-indicators.min.js.LICENSE.txt +0 -1
- package/index.js +41 -51
- package/package.json +2 -5
- package/src/core-functions/mainLoop.js +26 -46
- package/src/core-functions/pushToMain.js +7 -24
- package/src/core-indicators/README.md +8 -9
- package/src/core-indicators/indicator.js +0 -1
- package/src/moving-averages/bollingerBands.js +29 -6
- package/src/moving-averages/donchianChannel.js +34 -9
- package/src/moving-averages/heikenAshi.js +10 -19
- package/src/moving-averages/movingAverages.js +15 -5
- package/src/moving-averages/relativeVolume.js +9 -5
- package/src/oscillators/mfi.js +56 -0
- package/src/oscillators/volumeOscillator.js +5 -11
- package/src/studies/candleFeatures.js +28 -8
- package/src/studies/dateTime.js +41 -38
- package/src/studies/findCrosses.js +2 -9
- package/src/studies/lag.js +11 -12
- package/src/utilities/config.js +4 -37
- package/src/utilities/numberUtilities.js +3 -3
- package/src/utilities/precisionMultiplier.js +11 -6
- package/src/utilities/verticalToHorizontal.js +6 -1
- package/src/volatility/atr.js +1 -1
- package/test/test.js +852 -155
- package/webpack.config.js +0 -11
- package/src/studies/mapCols.js +0 -45
package/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { verticalToHorizontal } from './src/utilities/verticalToHorizontal.js'
|
|
|
13
13
|
import { assignTypes } from './src/utilities/assignTypes.js'
|
|
14
14
|
import { dateOutputFormaters } from './src/utilities/dateUtilities.js'
|
|
15
15
|
import { calcPrecisionMultiplier } from './src/utilities/precisionMultiplier.js'
|
|
16
|
-
import { CONFIG_VERSION, copyConfig, freezeConfig
|
|
16
|
+
import { CONFIG_VERSION, copyConfig, freezeConfig } from './src/utilities/config.js'
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Class OHLCV_INDICATORS
|
|
@@ -116,18 +116,13 @@ export default class OHLCV_INDICATORS {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
static registerMapCallback(name, callback) {
|
|
120
|
-
registerMapCallback(name, callback)
|
|
121
|
-
return this
|
|
122
|
-
}
|
|
123
|
-
|
|
124
119
|
exportConfig() {
|
|
125
|
-
return { ...this.config, inputParams:
|
|
120
|
+
return { ...this.config, inputParams: copyConfig(this.inputParams) }
|
|
126
121
|
}
|
|
127
122
|
|
|
128
123
|
_registerIndicator(job) {
|
|
129
124
|
isAlreadyComputed(this)
|
|
130
|
-
this.inputParams = Object.freeze([...this.inputParams, freezeConfig(copyConfig(job
|
|
125
|
+
this.inputParams = Object.freeze([...this.inputParams, freezeConfig(copyConfig(job))])
|
|
131
126
|
return this
|
|
132
127
|
}
|
|
133
128
|
|
|
@@ -175,11 +170,8 @@ export default class OHLCV_INDICATORS {
|
|
|
175
170
|
|
|
176
171
|
if (this.isComputing) throw new Error('Computation is already in progress.')
|
|
177
172
|
|
|
178
|
-
this.executionParams = copyConfig(this.inputParams
|
|
173
|
+
this.executionParams = copyConfig(this.inputParams)
|
|
179
174
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
180
|
-
for (const job of this.executionParams) {
|
|
181
|
-
if (job.key === 'mapCols') job.params[1] = resolveMapCallback(job.params[1])
|
|
182
|
-
}
|
|
183
175
|
|
|
184
176
|
// Mark as “in progress”
|
|
185
177
|
this.isComputed = false;
|
|
@@ -255,6 +247,13 @@ export default class OHLCV_INDICATORS {
|
|
|
255
247
|
|
|
256
248
|
isAlreadyComputed(this)
|
|
257
249
|
validateArray(colKeys, 'colKeys', methodName)
|
|
250
|
+
|
|
251
|
+
const invalidItem = colKeys.find(v => typeof v !== 'string' || v === '')
|
|
252
|
+
|
|
253
|
+
if(invalidItem !== undefined) {
|
|
254
|
+
throw new Error(`Invalid item "${invalidItem}" (column name) in "colKeys" array param of "${methodName}()".`)
|
|
255
|
+
}
|
|
256
|
+
|
|
258
257
|
validateNumber(lookback, {min:1, max: this.len, allowDecimals: false}, 'lookback', methodName)
|
|
259
258
|
|
|
260
259
|
this._registerIndicator({key: methodName, params: [colKeys, lookback]})
|
|
@@ -275,11 +274,12 @@ export default class OHLCV_INDICATORS {
|
|
|
275
274
|
validateNumber(size, {min:1, max: this.len, allowDecimals: false}, 'size', methodName)
|
|
276
275
|
validateObject(options, 'options', methodName)
|
|
277
276
|
|
|
278
|
-
const { lag = 0} = options;
|
|
277
|
+
const { lag = 0, retLogs = false } = options;
|
|
279
278
|
|
|
280
279
|
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
280
|
+
validateBoolean(retLogs, 'options.retLogs', methodName)
|
|
281
281
|
|
|
282
|
-
this._registerIndicator({key: methodName, order: 0, params: [size, {lag}]})
|
|
282
|
+
this._registerIndicator({key: methodName, order: 0, params: [size, {lag, retLogs}]})
|
|
283
283
|
|
|
284
284
|
return this
|
|
285
285
|
}
|
|
@@ -354,12 +354,13 @@ export default class OHLCV_INDICATORS {
|
|
|
354
354
|
validateNumber(size, {min: 1, max: this.len, allowDecimals: false}, 'size', methodName)
|
|
355
355
|
validateObject(options, 'options', methodName)
|
|
356
356
|
|
|
357
|
-
const {target = 'close', lag = 0} = options
|
|
357
|
+
const {target = 'close', lag = 0, retLogs = false} = options
|
|
358
358
|
|
|
359
359
|
validateString(target, 'options.target', methodName)
|
|
360
360
|
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
361
|
+
validateBoolean(retLogs, 'options.retLogs', methodName)
|
|
361
362
|
|
|
362
|
-
this._registerIndicator({key: methodName, params: [methodName, size, {target, lag}]})
|
|
363
|
+
this._registerIndicator({key: methodName, params: [methodName, size, {target, lag, retLogs}]})
|
|
363
364
|
|
|
364
365
|
return this
|
|
365
366
|
}
|
|
@@ -372,12 +373,13 @@ export default class OHLCV_INDICATORS {
|
|
|
372
373
|
validateNumber(size, {min: 1, max: this.len, allowDecimals: false}, 'size', methodName)
|
|
373
374
|
validateObject(options, 'options', methodName)
|
|
374
375
|
|
|
375
|
-
const {target = 'close', lag = 0} = options
|
|
376
|
+
const {target = 'close', lag = 0, retLogs = false} = options
|
|
376
377
|
|
|
377
378
|
validateString(target, 'options.target', methodName)
|
|
378
379
|
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
380
|
+
validateBoolean(retLogs, 'options.retLogs', methodName)
|
|
379
381
|
|
|
380
|
-
this._registerIndicator({key: methodName, params: [methodName, size, {target, lag}]})
|
|
382
|
+
this._registerIndicator({key: methodName, params: [methodName, size, {target, lag, retLogs}]})
|
|
381
383
|
|
|
382
384
|
return this
|
|
383
385
|
}
|
|
@@ -464,11 +466,12 @@ export default class OHLCV_INDICATORS {
|
|
|
464
466
|
validateNumber(stdDev, {min: 0.01, max: 50, allowDecimals: true}, 'stdDev', methodName)
|
|
465
467
|
validateObject(options, 'options', methodName)
|
|
466
468
|
|
|
467
|
-
const { lag = 0} = options
|
|
469
|
+
const { lag = 0, retLogs = false } = options
|
|
468
470
|
|
|
469
471
|
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
472
|
+
validateBoolean(retLogs, 'options.retLogs', methodName)
|
|
470
473
|
|
|
471
|
-
this._registerIndicator({key: methodName, params: [size, stdDev, {lag}]});
|
|
474
|
+
this._registerIndicator({key: methodName, params: [size, stdDev, {lag, retLogs}]});
|
|
472
475
|
|
|
473
476
|
return this;
|
|
474
477
|
}
|
|
@@ -492,6 +495,21 @@ export default class OHLCV_INDICATORS {
|
|
|
492
495
|
|
|
493
496
|
return this
|
|
494
497
|
}
|
|
498
|
+
mfi(size = 14, options = {}) {
|
|
499
|
+
const methodName = 'mfi'
|
|
500
|
+
|
|
501
|
+
isAlreadyComputed(this)
|
|
502
|
+
validateNumber(size, {min: 1, max: this.len, allowDecimals: false}, 'size', methodName)
|
|
503
|
+
validateObject(options, 'options', methodName)
|
|
504
|
+
|
|
505
|
+
const {lag = 0, retLogs = false} = options
|
|
506
|
+
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
507
|
+
validateBoolean(retLogs, 'options.retLogs', methodName)
|
|
508
|
+
|
|
509
|
+
this._registerIndicator({key: methodName, params: [size, {lag, retLogs}]})
|
|
510
|
+
return this
|
|
511
|
+
}
|
|
512
|
+
|
|
495
513
|
donchianChannels(size = 20, offset = 0, options = {}) {
|
|
496
514
|
|
|
497
515
|
const methodName = 'donchianChannels'
|
|
@@ -502,11 +520,12 @@ export default class OHLCV_INDICATORS {
|
|
|
502
520
|
validateNumber(offset, {min: 0, max: this.len, allowDecimals: false}, 'offset', methodName)
|
|
503
521
|
|
|
504
522
|
validateObject(options, 'options', methodName)
|
|
505
|
-
const {
|
|
523
|
+
const { lag = 0, retLogs = false } = options;
|
|
506
524
|
|
|
507
525
|
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
526
|
+
validateBoolean(retLogs, 'options.retLogs', methodName)
|
|
508
527
|
|
|
509
|
-
this._registerIndicator({ key: methodName, order: 0, params: [size, offset, {lag}] });
|
|
528
|
+
this._registerIndicator({ key: methodName, order: 0, params: [size, offset, {lag, retLogs}] });
|
|
510
529
|
|
|
511
530
|
return this;
|
|
512
531
|
}
|
|
@@ -588,33 +607,4 @@ export default class OHLCV_INDICATORS {
|
|
|
588
607
|
return this
|
|
589
608
|
}
|
|
590
609
|
|
|
591
|
-
mapCols(newCols = ['change'], callback = null, options = {}) {
|
|
592
|
-
|
|
593
|
-
isAlreadyComputed(this)
|
|
594
|
-
|
|
595
|
-
const methodName = 'mapCols'
|
|
596
|
-
|
|
597
|
-
if(typeof callback === 'undefined' || callback === null)
|
|
598
|
-
{
|
|
599
|
-
callback = 'default'
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
validateObject(options, 'options', methodName)
|
|
603
|
-
|
|
604
|
-
const {lag = 0, isPriceBased = false, callbackParams = {}} = options
|
|
605
|
-
resolveMapCallback(callback)
|
|
606
|
-
const savedCallbackParams = copyConfig(callbackParams)
|
|
607
|
-
|
|
608
|
-
validateArray(newCols, 'newCols', methodName)
|
|
609
|
-
validateNumber(lag, {min: 0, allowDecimals: false}, 'options.lag', methodName)
|
|
610
|
-
validateBoolean(isPriceBased, 'isPriceBased', 'mapCols')
|
|
611
|
-
|
|
612
|
-
if(this.precision === false && isPriceBased) {
|
|
613
|
-
throw new Error(`Invalid param: If "mapCols.options.isPriceBased" is true, the "constructor.precision" param must be also true.`)
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
this._registerIndicator({key: methodName, params: [newCols, callback, {lag, isPriceBased, callbackParams: savedCallbackParams}]})
|
|
617
|
-
|
|
618
|
-
return this
|
|
619
|
-
}
|
|
620
610
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ohlcv-indicators",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.80",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "node test/test.js",
|
|
9
|
+
"test-benchmark": "npm test -- --benchmark --rows 20000 --repeats 9",
|
|
9
10
|
"build": "npx webpack",
|
|
10
11
|
"cross": "node src/studies/findCrosses2.js"
|
|
11
12
|
},
|
|
12
13
|
"author": "https://jaimelias.com/",
|
|
13
14
|
"license": "ISC",
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"ohlcv-indicators": "^3.5.72",
|
|
16
|
-
"trading-signals": "^5.0.4"
|
|
17
|
-
},
|
|
18
15
|
"devDependencies": {
|
|
19
16
|
"@babel/core": "^7.25.2",
|
|
20
17
|
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { areKeyValuesValid } from "./pushToMain.js";
|
|
2
2
|
import { validateInputParams } from "../utilities/validators.js";
|
|
3
3
|
import { rsi } from "../oscillators/rsi.js";
|
|
4
|
+
import { mfi } from "../oscillators/mfi.js";
|
|
4
5
|
import { stochastic } from "../oscillators/stochastic.js";
|
|
5
6
|
import { movingAverages } from "../moving-averages/movingAverages.js";
|
|
6
7
|
import { heikenAshi } from "../moving-averages/heikenAshi.js";
|
|
@@ -15,12 +16,12 @@ import { crossPairs } from "../studies/findCrosses.js";
|
|
|
15
16
|
import { dateTime } from "../studies/dateTime.js";
|
|
16
17
|
import { atr } from "../volatility/atr.js";
|
|
17
18
|
import { adx } from "../volatility/adx.js";
|
|
18
|
-
import { mapCols } from "../studies/mapCols.js";
|
|
19
19
|
|
|
20
20
|
import { buildArray } from "../utilities/assignTypes.js";
|
|
21
21
|
import { inputNumberFormatter } from "../utilities/numberUtilities.js";
|
|
22
22
|
import { dateFormaters } from "../utilities/dateUtilities.js";
|
|
23
23
|
import { candleFeatures } from "../studies/candleFeatures.js";
|
|
24
|
+
import { getPrecisionDecimals } from "../utilities/precisionMultiplier.js";
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
// Map indicator keys to their respective functions
|
|
@@ -29,6 +30,7 @@ const mainFunctions = {
|
|
|
29
30
|
dateTime,
|
|
30
31
|
heikenAshi,
|
|
31
32
|
rsi,
|
|
33
|
+
mfi,
|
|
32
34
|
stochastic,
|
|
33
35
|
atr,
|
|
34
36
|
adx,
|
|
@@ -40,7 +42,6 @@ const mainFunctions = {
|
|
|
40
42
|
donchianChannels,
|
|
41
43
|
bollingerBands,
|
|
42
44
|
volumeOscillator,
|
|
43
|
-
mapCols,
|
|
44
45
|
crossPairs,
|
|
45
46
|
lag
|
|
46
47
|
};
|
|
@@ -70,16 +71,16 @@ export const mainLoop = (input, main) => {
|
|
|
70
71
|
verticalOhlcv[key] = buildArray(arrayTypes[key], len)
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
const inputColumns = Object.entries(inputTypes).map(([key, formatterKey]) => {
|
|
75
|
+
const numeric = inputNumberFormatter.hasOwnProperty(formatterKey)
|
|
76
|
+
const formatter = numeric
|
|
77
|
+
? inputNumberFormatter[formatterKey]
|
|
78
|
+
: dateFormaters.hasOwnProperty(formatterKey) ? dateFormaters[formatterKey] : null
|
|
79
|
+
return { key, formatter, numeric }
|
|
80
|
+
})
|
|
81
|
+
const precisionDecimals = typeof precisionMultiplier === 'number'
|
|
82
|
+
? getPrecisionDecimals(precisionMultiplier) : undefined
|
|
83
|
+
let validationColumns = null
|
|
83
84
|
|
|
84
85
|
// Process each row in the input
|
|
85
86
|
for (let chunkStart = 0; chunkStart < len; chunkStart += chunkProcess) {
|
|
@@ -89,37 +90,13 @@ export const mainLoop = (input, main) => {
|
|
|
89
90
|
for (let index = chunkStart; index < chunkEnd; index++) {
|
|
90
91
|
const curr = input[index]
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
main.pushToMain({ index, key, value: formattedValue })
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
for(const [key, formaterKey] of Object.entries(inputTypes))
|
|
103
|
-
{
|
|
104
|
-
let value = curr[key]
|
|
105
|
-
|
|
106
|
-
if(typeof value === 'undefined') continue
|
|
107
|
-
|
|
108
|
-
if(inputNumberFormatter.hasOwnProperty(formaterKey))
|
|
109
|
-
{
|
|
110
|
-
const formatedValue = inputNumberFormatter[formaterKey](value, precisionMultiplier)
|
|
111
|
-
|
|
112
|
-
main.pushToMain({index, key, value: formatedValue})
|
|
113
|
-
}
|
|
114
|
-
else if(dateFormaters.hasOwnProperty(formaterKey))
|
|
115
|
-
{
|
|
116
|
-
main.pushToMain({index, key, value: dateFormaters[formaterKey](value)})
|
|
117
|
-
}
|
|
118
|
-
else
|
|
119
|
-
{
|
|
120
|
-
main.pushToMain({index, key, value})
|
|
121
|
-
}
|
|
122
|
-
}
|
|
93
|
+
for (const { key, formatter, numeric } of inputColumns) {
|
|
94
|
+
const value = curr[key]
|
|
95
|
+
if (typeof value === 'undefined') continue
|
|
96
|
+
|
|
97
|
+
const formattedValue = formatter === null ? value
|
|
98
|
+
: numeric ? formatter(value, precisionMultiplier, precisionDecimals) : formatter(value)
|
|
99
|
+
main.pushToMain({ index, key, value: formattedValue })
|
|
123
100
|
}
|
|
124
101
|
|
|
125
102
|
const midPrice = (verticalOhlcv.open[index] + verticalOhlcv.close[index]) / 2
|
|
@@ -134,10 +111,13 @@ export const mainLoop = (input, main) => {
|
|
|
134
111
|
|
|
135
112
|
input[index] = null //flusing data
|
|
136
113
|
|
|
137
|
-
|
|
114
|
+
// All built-in outputs (including generated lags) exist after row zero.
|
|
115
|
+
if (validationColumns === null) {
|
|
116
|
+
validationColumns = Object.values(verticalOhlcv)
|
|
117
|
+
}
|
|
138
118
|
|
|
139
|
-
//
|
|
140
|
-
if (!areKeyValuesValid(
|
|
119
|
+
// Preserve the missing/NaN/null predicate, including Infinity behavior.
|
|
120
|
+
if (!areKeyValuesValid(validationColumns, index)) {
|
|
141
121
|
main.invalidValueIndex = index
|
|
142
122
|
}
|
|
143
123
|
}
|
|
@@ -5,29 +5,12 @@ export const pushToMain = ({main, index, key, value}) => {
|
|
|
5
5
|
verticalOhlcv[key][index] = value
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export const areKeyValuesValid = (
|
|
9
|
-
|
|
10
|
-
if(keyNames.length === 0) return false
|
|
8
|
+
export const areKeyValuesValid = (columns, index) => {
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
for(let x = 0 ; x < keyNames.length; x++)
|
|
17
|
-
{
|
|
18
|
-
const key = keyNames[x]
|
|
19
|
-
if(!verticalOhlcv.hasOwnProperty(key)) {
|
|
20
|
-
output = false
|
|
21
|
-
break
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const val = verticalOhlcv[key][index]
|
|
25
|
-
|
|
26
|
-
if (val == null || Number.isNaN(val)) {
|
|
27
|
-
output = false
|
|
28
|
-
break
|
|
29
|
-
}
|
|
10
|
+
if (columns.length === 0) return false
|
|
11
|
+
for (let x = 0; x < columns.length; x++) {
|
|
12
|
+
const val = columns[x][index]
|
|
13
|
+
if (val == null || Number.isNaN(val)) return false
|
|
30
14
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# Core indicators
|
|
2
2
|
|
|
3
|
-
Local numeric
|
|
4
|
-
`trading-signals@5.0.4`. The existing constructor arguments, `update(value,
|
|
3
|
+
Local numeric `Faster*` indicator implementations. The existing constructor arguments, `update(value,
|
|
5
4
|
replace?)`, `isStable`, and `getResult()` interface remain available. The
|
|
6
5
|
numeric series also retain result/extrema and replacement semantics. These
|
|
7
|
-
|
|
8
|
-
of compatibility with other trading-signals versions.
|
|
6
|
+
use ordinary JavaScript numbers, not arbitrary-precision decimal arithmetic.
|
|
9
7
|
|
|
10
8
|
```js
|
|
11
9
|
import { FasterEMA } from './index.js'
|
|
@@ -45,11 +43,12 @@ of previously supplied objects. Private buffer layouts intentionally differ.
|
|
|
45
43
|
|
|
46
44
|
## Verification
|
|
47
45
|
|
|
48
|
-
`npm test`
|
|
49
|
-
|
|
50
|
-
public indicators, precision/log modes, lags,
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
`npm test` checks known core results and fixed behavior traces captured before
|
|
47
|
+
runtime optimizations, checks bounded retained storage, and exercises all
|
|
48
|
+
public indicators, precision/log modes, lags, and config replay.
|
|
49
|
+
These checks are self-contained: no third-party indicator library or test
|
|
50
|
+
oracle is installed. Preserve the trace expectations unless intentionally
|
|
51
|
+
changing the corresponding behavior.
|
|
53
52
|
|
|
54
53
|
`node test/test.js --baseline /path/to/original/index.js --benchmark` also
|
|
55
54
|
compares the full pipeline with a pre-change checkout. Benchmarks report
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { FasterBollingerBands } from '../core-indicators/index.js'
|
|
2
2
|
import { validateInputValues } from '../utilities/validators.js'
|
|
3
3
|
import { initializeColumns } from '../core-functions/initializeColumns.js'
|
|
4
|
+
import { mathLog } from '../utilities/math.js'
|
|
4
5
|
|
|
5
|
-
export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
|
|
6
|
+
export const bollingerBands = (main, index, size, stdDev, { lag, retLogs = false } = {}) => {
|
|
6
7
|
const { verticalOhlcv, instances, useFullNames } = main
|
|
7
8
|
const indicatorKey = `${size}_${stdDev}`
|
|
8
|
-
const
|
|
9
|
+
const instanceKey = `${retLogs ? 'ret_log_' : ''}${indicatorKey}`
|
|
10
|
+
const prefix = `${retLogs ? 'ret_log_' : ''}bollinger_bands`
|
|
9
11
|
const target = 'close'
|
|
10
12
|
|
|
11
13
|
// Initialization on the first call.
|
|
@@ -31,25 +33,28 @@ export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
instances.bollinger_bands.settings[
|
|
36
|
+
instances.bollinger_bands.settings[instanceKey] = new FasterBollingerBands(size, stdDev)
|
|
35
37
|
|
|
36
38
|
const suffix = (numberOfIndicators > 1 || useFullNames) ? `_${indicatorKey}` : ''
|
|
37
39
|
|
|
38
|
-
const keyNames = [
|
|
40
|
+
const keyNames = retLogs ? [
|
|
41
|
+
`${prefix}_width${suffix}`,
|
|
42
|
+
`${prefix}_position${suffix}`,
|
|
43
|
+
] : [
|
|
39
44
|
`${prefix}_upper${suffix}`,
|
|
40
45
|
`${prefix}_middle${suffix}`,
|
|
41
46
|
`${prefix}_lower${suffix}`,
|
|
42
47
|
]
|
|
43
48
|
|
|
44
49
|
initializeColumns(main, keyNames.map(key => ({
|
|
45
|
-
key, priceBased: priceBased.has(target)
|
|
50
|
+
key, priceBased: !retLogs && priceBased.has(target)
|
|
46
51
|
})), { lag })
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
const { numberOfIndicators } = instances.bollinger_bands
|
|
50
55
|
const suffix = (numberOfIndicators > 1 || useFullNames) ? `_${indicatorKey}` : ''
|
|
51
56
|
|
|
52
|
-
const instance = instances.bollinger_bands.settings[
|
|
57
|
+
const instance = instances.bollinger_bands.settings[instanceKey]
|
|
53
58
|
const value = verticalOhlcv[target][index]
|
|
54
59
|
instance.update(value)
|
|
55
60
|
|
|
@@ -59,6 +64,24 @@ export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
|
|
|
59
64
|
const middle = result?.middle ?? NaN
|
|
60
65
|
const lower = result?.lower ?? NaN
|
|
61
66
|
|
|
67
|
+
if (retLogs) {
|
|
68
|
+
let width = NaN
|
|
69
|
+
let position = NaN
|
|
70
|
+
const rangeRatio = upper / lower
|
|
71
|
+
if (lower > 0 && upper >= lower && Number.isFinite(upper) && Number.isFinite(rangeRatio)) {
|
|
72
|
+
const logRange = mathLog(rangeRatio, 1)
|
|
73
|
+
width = logRange / 2
|
|
74
|
+
const locationRatio = value / lower
|
|
75
|
+
if (logRange > 0 && value > 0 && locationRatio > 0 && Number.isFinite(locationRatio)) {
|
|
76
|
+
const normalized = 2 * mathLog(locationRatio, 1) / logRange - 1
|
|
77
|
+
if (Number.isFinite(normalized)) position = normalized
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
main.pushToMain({ index, key: `${prefix}_width${suffix}`, value: width })
|
|
81
|
+
main.pushToMain({ index, key: `${prefix}_position${suffix}`, value: position })
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
main.pushToMain({ index, key: `${prefix}_upper${suffix}`, value: upper })
|
|
63
86
|
main.pushToMain({ index, key: `${prefix}_middle${suffix}`, value: middle })
|
|
64
87
|
main.pushToMain({ index, key: `${prefix}_lower${suffix}`, value: lower })
|
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
import { validateInputValues } from '../utilities/validators.js'
|
|
2
2
|
import { initializeColumns } from '../core-functions/initializeColumns.js'
|
|
3
|
+
import { mathLog } from '../utilities/math.js'
|
|
3
4
|
|
|
4
5
|
export const donchianChannels = (main, index, size, offset, options) => {
|
|
5
6
|
const { verticalOhlcv, instances, len, inputParams, useFullNames } = main
|
|
6
|
-
const { lag: outputLag = 0 } = options
|
|
7
|
+
const { lag: outputLag = 0, retLogs = false } = options
|
|
7
8
|
const indicatorKey = `${size}_${offset}`
|
|
9
|
+
const instanceKey = `${retLogs ? 'ret_log_' : ''}${indicatorKey}`
|
|
10
|
+
const prefix = `${retLogs ? 'ret_log_' : ''}donchian_channel`
|
|
8
11
|
|
|
9
12
|
// ---- INIT (only at first bar) ----
|
|
10
13
|
if (index === 0) {
|
|
11
14
|
validateInputValues({ high: true, low: true }, verticalOhlcv, index, 'donchianChannels')
|
|
15
|
+
if (retLogs) validateInputValues({ close: true }, verticalOhlcv, index, 'donchianChannels')
|
|
12
16
|
|
|
13
17
|
const numberOfIndicators = inputParams.filter(o => o.key === 'donchianChannels').length
|
|
14
18
|
const useIndexedKeys = numberOfIndicators > 1 || useFullNames
|
|
15
19
|
|
|
16
20
|
const getKey = name =>
|
|
17
21
|
useIndexedKeys
|
|
18
|
-
?
|
|
19
|
-
:
|
|
22
|
+
? `${prefix}_${name}_${indicatorKey}`
|
|
23
|
+
: `${prefix}_${name}`
|
|
20
24
|
|
|
21
|
-
const keys = ['upper', 'basis', 'lower'].map(getKey)
|
|
25
|
+
const keys = (retLogs ? ['width', 'position'] : ['upper', 'basis', 'lower']).map(getKey)
|
|
22
26
|
|
|
23
27
|
if (!instances.donchian_channel) {
|
|
24
28
|
instances.donchian_channel = { numberOfIndicators, settings: {} }
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
instances.donchian_channel.numberOfIndicators = numberOfIndicators
|
|
28
|
-
instances.donchian_channel.settings[
|
|
32
|
+
instances.donchian_channel.settings[instanceKey] = {
|
|
29
33
|
maxDeque: { indices: [], head: 0, length: 0 },
|
|
30
34
|
minDeque: { indices: [], head: 0, length: 0 }
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
initializeColumns(main, keys.map(key => ({ key, priceBased:
|
|
37
|
+
initializeColumns(main, keys.map(key => ({ key, priceBased: !retLogs })), { lag: outputLag })
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
// ---- PER-BAR COMPUTATION ----
|
|
@@ -39,10 +43,10 @@ export const donchianChannels = (main, index, size, offset, options) => {
|
|
|
39
43
|
|
|
40
44
|
const getKey = name =>
|
|
41
45
|
useIndexedKeys
|
|
42
|
-
?
|
|
43
|
-
:
|
|
46
|
+
? `${prefix}_${name}_${indicatorKey}`
|
|
47
|
+
: `${prefix}_${name}`
|
|
44
48
|
|
|
45
|
-
const { maxDeque, minDeque } = settings[
|
|
49
|
+
const { maxDeque, minDeque } = settings[instanceKey]
|
|
46
50
|
const current = index - offset
|
|
47
51
|
const start = current - size + 1
|
|
48
52
|
|
|
@@ -74,6 +78,27 @@ export const donchianChannels = (main, index, size, offset, options) => {
|
|
|
74
78
|
const hasBounds = maxDeque.length && minDeque.length
|
|
75
79
|
const upper = hasBounds ? highs[maxDeque.indices[maxDeque.head]] : NaN
|
|
76
80
|
const lower = hasBounds ? lows[minDeque.indices[minDeque.head]] : NaN
|
|
81
|
+
|
|
82
|
+
if (retLogs) {
|
|
83
|
+
let width = NaN
|
|
84
|
+
let position = NaN
|
|
85
|
+
const rangeRatio = upper / lower
|
|
86
|
+
if (lower > 0 && upper >= lower && Number.isFinite(upper) && Number.isFinite(rangeRatio)) {
|
|
87
|
+
const logRange = mathLog(rangeRatio, 1)
|
|
88
|
+
width = logRange / 2
|
|
89
|
+
// Offset applies to the channel window, not to the current close.
|
|
90
|
+
const close = verticalOhlcv.close[index]
|
|
91
|
+
const locationRatio = close / lower
|
|
92
|
+
if (logRange > 0 && close > 0 && locationRatio > 0 && Number.isFinite(locationRatio)) {
|
|
93
|
+
const normalized = 2 * mathLog(locationRatio, 1) / logRange - 1
|
|
94
|
+
if (Number.isFinite(normalized)) position = normalized
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
main.pushToMain({ index, key: getKey('width'), value: width })
|
|
98
|
+
main.pushToMain({ index, key: getKey('position'), value: position })
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
const basis = hasBounds ? (upper + lower) / 2 : NaN
|
|
78
103
|
|
|
79
104
|
main.pushToMain({ index, key: getKey('upper'), value: upper })
|
|
@@ -13,7 +13,6 @@ export const heikenAshi = (
|
|
|
13
13
|
) => {
|
|
14
14
|
const { verticalOhlcv, instances, scaledGroups } = main;
|
|
15
15
|
const indicatorKey = `${smoothLength}_${afterSmoothLength}`;
|
|
16
|
-
const ohlcKeys = ['open', 'high', 'low', 'close'];
|
|
17
16
|
|
|
18
17
|
const getRet = (next, prev) => retLogs
|
|
19
18
|
? Math.log(next / prev)
|
|
@@ -25,13 +24,6 @@ export const heikenAshi = (
|
|
|
25
24
|
|
|
26
25
|
const getKey = key => `${prefix}heiken_ashi_${key}${paramsKey}`;
|
|
27
26
|
|
|
28
|
-
const featureKeys = [
|
|
29
|
-
'body',
|
|
30
|
-
'upper_wick',
|
|
31
|
-
'lower_wick',
|
|
32
|
-
'range'
|
|
33
|
-
];
|
|
34
|
-
|
|
35
27
|
const crossKey = bothNull
|
|
36
28
|
? 'heiken_ashi_cross'
|
|
37
29
|
: `heiken_ashi_cross_${indicatorKey}`;
|
|
@@ -43,6 +35,8 @@ export const heikenAshi = (
|
|
|
43
35
|
// ---- INIT ----
|
|
44
36
|
if (index === 0) {
|
|
45
37
|
validateInputValues({ open: true, high: true, low: true, close: true }, verticalOhlcv, index, 'heikenAshi');
|
|
38
|
+
const ohlcKeys = ['open', 'high', 'low', 'close'];
|
|
39
|
+
const featureKeys = ['body', 'upper_wick', 'lower_wick', 'range'];
|
|
46
40
|
|
|
47
41
|
instances[instanceKey] = {
|
|
48
42
|
emaPre: !bothNull
|
|
@@ -206,18 +200,15 @@ export const heikenAshi = (
|
|
|
206
200
|
const bodyTop = Math.max(smOpen, smClose);
|
|
207
201
|
const bodyBottom = Math.min(smOpen, smClose);
|
|
208
202
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
[getKey('range')]: getRet(smHigh, smLow),
|
|
214
|
-
};
|
|
203
|
+
const body = getRet(smClose, smOpen);
|
|
204
|
+
const upperWick = getRet(smHigh, bodyTop);
|
|
205
|
+
const lowerWick = getRet(bodyBottom, smLow);
|
|
206
|
+
const range = getRet(smHigh, smLow);
|
|
215
207
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
208
|
+
if (!isBadNumber(body)) main.pushToMain({ index, key: getKey('body'), value: body });
|
|
209
|
+
if (!isBadNumber(upperWick)) main.pushToMain({ index, key: getKey('upper_wick'), value: upperWick });
|
|
210
|
+
if (!isBadNumber(lowerWick)) main.pushToMain({ index, key: getKey('lower_wick'), value: lowerWick });
|
|
211
|
+
if (!isBadNumber(range)) main.pushToMain({ index, key: getKey('range'), value: range });
|
|
221
212
|
|
|
222
213
|
// Do not add return logs to cross.
|
|
223
214
|
main.pushToMain({ index, key: crossKey, value: cross });
|