ohlcv-indicators 3.5.79 → 3.5.82

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/package.json CHANGED
@@ -1,28 +1,18 @@
1
1
  {
2
2
  "name": "ohlcv-indicators",
3
- "version": "3.5.79",
3
+ "version": "3.5.82",
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
- "test": "node test/test.js",
9
8
  "build": "npx webpack",
10
- "cross": "node src/studies/findCrosses2.js"
9
+ "test": "node test/test.js"
11
10
  },
12
11
  "author": "https://jaimelias.com/",
13
12
  "license": "ISC",
14
- "dependencies": {
15
- "ohlcv-indicators": "^3.5.72",
16
- "trading-signals": "^5.0.4"
17
- },
18
13
  "devDependencies": {
19
- "@babel/core": "^7.25.2",
20
- "@babel/plugin-proposal-class-properties": "^7.18.6",
21
- "@babel/preset-env": "^7.25.3",
22
- "babel-loader": "^9.1.3",
23
- "dotenv": "^16.4.5",
24
- "terser-webpack-plugin": "^5.3.14",
25
- "webpack": "^5.93.0",
26
- "webpack-cli": "^5.1.4"
14
+ "dotenv": "^16.6.1",
15
+ "webpack": "^5.111.1",
16
+ "webpack-cli": "^7.2.3"
27
17
  }
28
18
  }
@@ -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'
@@ -41,21 +39,4 @@ provided. This preserves existing JavaScript coercion for custom mapped
41
39
  columns without adding object storage for normal numeric input. NaN and
42
40
  Infinity remain numeric values and do not trigger this fallback. Candle
43
41
  references are retained where the reference implementation observes mutations
44
- of previously supplied objects. Private buffer layouts intentionally differ.
45
-
46
- ## Verification
47
-
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.
53
-
54
- `node test/test.js --baseline /path/to/original/index.js --benchmark` also
55
- compares the full pipeline with a pre-change checkout. Benchmarks report
56
- local timings and retained array slots/buffer bytes, not total heap usage or
57
- performance guarantees. Results must remain exact; changing summation order,
58
- warm-up, or quirks is a separate behavior change.
59
-
60
- Adapted portions retain the upstream MIT notice in `indicator.js`, also
61
- preserved in the generated distribution's license notice.
42
+ of previously supplied objects. Private buffer layouts intentionally differ.
@@ -1,27 +1,3 @@
1
- /*!
2
- * Portions adapted from trading-signals 5.0.4.
3
- * MIT License
4
- * Copyright (c) 2020 Benny Neugebauer
5
- *
6
- * Permission is hereby granted, free of charge, to any person obtaining a copy
7
- * of this software and associated documentation files (the "Software"), to deal
8
- * in the Software without restriction, including without limitation the rights
9
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- * copies of the Software, and to permit persons to whom the Software is
11
- * furnished to do so, subject to the following conditions:
12
- *
13
- * The above copyright notice and this permission notice shall be included in all
14
- * copies or substantial portions of the Software.
15
- *
16
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- * SOFTWARE.
23
- */
24
-
25
1
  export class NotEnoughDataError extends Error {
26
2
  constructor(message = 'Not enough data') {
27
3
  super(message)
@@ -30,7 +6,9 @@ export class NotEnoughDataError extends Error {
30
6
  }
31
7
 
32
8
  export class Indicator {
33
- result
9
+ constructor() {
10
+ this.result = undefined
11
+ }
34
12
 
35
13
  get isStable() {
36
14
  return this.result !== undefined
@@ -44,11 +22,14 @@ export class Indicator {
44
22
 
45
23
  // Keep the public result/extrema and replacement semantics without price history.
46
24
  export class NumberIndicator extends Indicator {
47
- previousResult
48
- highest
49
- lowest
50
- previousHighest
51
- previousLowest
25
+ constructor() {
26
+ super()
27
+ this.previousResult = undefined
28
+ this.highest = undefined
29
+ this.lowest = undefined
30
+ this.previousHighest = undefined
31
+ this.previousLowest = undefined
32
+ }
52
33
 
53
34
  setResult(value, replace = false) {
54
35
  if (replace) {
@@ -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 })