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/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, registerMapCallback, resolveMapCallback, exportInputParams } from './src/utilities/config.js'
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: exportInputParams(this.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, true))])
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, true)
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 { lag = 0} = options;
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.79",
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
- // Callbacks can modify inputTypes through main; retain dynamic lookup for them.
74
- const inputColumns = executionParams.some(({ key }) => key === 'mapCols')
75
- ? null
76
- : Object.entries(inputTypes).map(([key, formatterKey]) => {
77
- const numeric = inputNumberFormatter.hasOwnProperty(formatterKey)
78
- const formatter = numeric
79
- ? inputNumberFormatter[formatterKey]
80
- : dateFormaters.hasOwnProperty(formatterKey) ? dateFormaters[formatterKey] : null
81
- return { key, formatter, numeric }
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
- if (inputColumns !== null) {
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) : formatter(value)
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
- const keyNames = Object.keys(verticalOhlcv)
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
- // if any value at this index is missing/NaN/null, mark it invalid
140
- if (!areKeyValuesValid(main, index, keyNames)) {
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 = (main, index, keyNames) => {
9
-
10
- if(keyNames.length === 0) return false
8
+ export const areKeyValuesValid = (columns, index) => {
11
9
 
12
- const {verticalOhlcv} = main
13
-
14
- let output = true
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
- return output
33
- }
15
+ return true
16
+ }
@@ -1,11 +1,9 @@
1
1
  # Core indicators
2
2
 
3
- Local numeric replacements for the `Faster*` implementations used from
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
- are not replacements for the arbitrary-precision `Big` classes or a promise
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` compares the core classes directly with the installed
49
- trading-signals reference, checks bounded retained storage, and exercises all
50
- public indicators, precision/log modes, lags, callbacks, and config replay.
51
- The dependency is retained as a regression oracle; production source imports
52
- only these local classes. Preserve the reference version in the lockfile.
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,5 +1,4 @@
1
1
  /*!
2
- * Portions adapted from trading-signals 5.0.4.
3
2
  * MIT License
4
3
  * Copyright (c) 2020 Benny Neugebauer
5
4
  *
@@ -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 prefix = 'bollinger_bands'
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[indicatorKey] = new FasterBollingerBands(size, stdDev)
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[indicatorKey]
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
- ? `donchian_channel_${name}_${indicatorKey}`
19
- : `donchian_channel_${name}`
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[indicatorKey] = {
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: true })), { lag: outputLag })
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
- ? `donchian_channel_${name}_${indicatorKey}`
43
- : `donchian_channel_${name}`
46
+ ? `${prefix}_${name}_${indicatorKey}`
47
+ : `${prefix}_${name}`
44
48
 
45
- const { maxDeque, minDeque } = settings[indicatorKey]
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 row = {
210
- [getKey('body')]: getRet(smClose, smOpen),
211
- [getKey('upper_wick')]: getRet(smHigh, bodyTop),
212
- [getKey('lower_wick')]: getRet(bodyBottom, smLow),
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
- for (const [key, value] of Object.entries(row)) {
217
- if (!isBadNumber(value)) {
218
- main.pushToMain({ index, key, value });
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 });