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/test/test.js
CHANGED
|
@@ -2,9 +2,10 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import { performance } from 'node:perf_hooks'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
5
6
|
import OHLCV_INDICATORS from '../index.js'
|
|
6
7
|
import * as Core from '../src/core-indicators/index.js'
|
|
7
|
-
import
|
|
8
|
+
import { addPrecisionAsNumber, revertPrecisionAsNumber } from '../src/utilities/precisionMultiplier.js'
|
|
8
9
|
|
|
9
10
|
// npm test: offline deterministic regressions.
|
|
10
11
|
// npm test -- --baseline /path/to/pre-change/index.js --benchmark --rows 10000 --repeats 7
|
|
@@ -28,13 +29,8 @@ const makeInput = (length = 320, precision = false) => Array.from({ length }, (_
|
|
|
28
29
|
note: `row-${index}`, flag: index % 2 === 0, extra: index / 10
|
|
29
30
|
}
|
|
30
31
|
})
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
custom_vector: index === 19 ? undefined : [index % 2, (index + 1) % 2]
|
|
34
|
-
})
|
|
35
|
-
const configure = (Ctor, input, { precision = false, retLogs = false, chunkProcess = 100 } = {}) => {
|
|
36
|
-
Ctor.registerMapCallback('test.optimizations.v1', mapCallback)
|
|
37
|
-
return new Ctor({ input, chunkProcess, config: { precision, useFullNames: retLogs, timeZone: 'America/Panama', dateFormat: 'iso', skipNull: false } })
|
|
32
|
+
const configure = (Ctor, input, { precision = false, retLogs = false, chunkProcess = 100, includeMfi = true } = {}) => {
|
|
33
|
+
const main = new Ctor({ input, chunkProcess, config: { precision, useFullNames: retLogs, timeZone: 'America/Panama', dateFormat: 'iso', skipNull: false } })
|
|
38
34
|
.ema(5, { lag: 2 }).ema(9, { target: 'open' }).sma(7, { lag: 1 })
|
|
39
35
|
.bollingerBands(8, 2, { lag: 1 }).macd(4, 9, 3, { lag: 1 })
|
|
40
36
|
.relativeVolume(6, { lag: 2 }).volumeDelta({ lag: 1 }).volumeOscillator(3, 8, { retLogs, lag: 2 })
|
|
@@ -44,8 +40,9 @@ const configure = (Ctor, input, { precision = false, retLogs = false, chunkProce
|
|
|
44
40
|
.donchianChannels(1, 0, { lag: 1 }).donchianChannels(8, 2, { lag: 2 }).donchianChannels(31, 9)
|
|
45
41
|
.dateTime({ oneHot: retLogs, lag: 1 }).candleFeatures({ colKeys: ['close'], retLogs, lag: 1 })
|
|
46
42
|
.crossPairs([{ fast: 'close', slow: 'open' }, { fast: 'price', slow: 100 }], { limit: 5, oneHot: true })
|
|
47
|
-
.mapCols(['custom_value', 'custom_vector'], 'test.optimizations.v1', { callbackParams: { factor: 2 }, lag: 2 })
|
|
48
43
|
.lag(['close', 'volume'], 2).lag(['close_lag_1'], 2).lag(['one_hot_close_x_open'], 1)
|
|
44
|
+
if (includeMfi) main.mfi(7, { retLogs, lag: 1 })
|
|
45
|
+
return main
|
|
49
46
|
}
|
|
50
47
|
const deepFreeze = value => {
|
|
51
48
|
if (value && typeof value === 'object') { Object.values(value).forEach(deepFreeze); Object.freeze(value) }
|
|
@@ -81,24 +78,81 @@ const referenceDonchian = (highs, lows, size, offset) => {
|
|
|
81
78
|
return { upper, lower, basis }
|
|
82
79
|
}
|
|
83
80
|
|
|
81
|
+
// Use independent full windows and the original three-log geometric definition.
|
|
82
|
+
const referenceBollinger = (closes, size, stdDev) => Array.from(closes, (_, index) => {
|
|
83
|
+
if (index < size) return { upper: NaN, middle: NaN, lower: NaN }
|
|
84
|
+
const window = Array.from(closes.slice(index - size + 1, index + 1))
|
|
85
|
+
const middle = window.reduce((sum, value) => sum + value, 0) / size
|
|
86
|
+
const deviation = Math.sqrt(window.reduce((sum, value) => sum + (value - middle) ** 2, 0) / size) * stdDev
|
|
87
|
+
return { upper: middle + deviation, middle, lower: middle - deviation }
|
|
88
|
+
})
|
|
89
|
+
const referenceChannelLogs = (upper, lower, close) => {
|
|
90
|
+
if (!Number.isFinite(upper) || !Number.isFinite(lower) || lower <= 0 || upper < lower || !Number.isFinite(upper / lower)) {
|
|
91
|
+
return { width: NaN, position: NaN }
|
|
92
|
+
}
|
|
93
|
+
const logUpper = Math.log(upper), logLower = Math.log(lower)
|
|
94
|
+
const width = (logUpper - logLower) / 2
|
|
95
|
+
const ratio = close / lower
|
|
96
|
+
const position = width > 0 && Number.isFinite(close) && close > 0 && Number.isFinite(ratio) && ratio > 0
|
|
97
|
+
? (Math.log(close) - (logUpper + logLower) / 2) / width : NaN
|
|
98
|
+
return { width, position: Number.isFinite(position) ? position : NaN }
|
|
99
|
+
}
|
|
100
|
+
const assertNear = (actual, expected, label, tolerance = 1e-10) => {
|
|
101
|
+
if (Number.isNaN(expected)) assert.ok(Number.isNaN(actual), `${label}: expected NaN, received ${actual}`)
|
|
102
|
+
else assert.ok(Number.isFinite(actual) && Math.abs(actual - expected) <= tolerance * Math.max(1, Math.abs(expected)),
|
|
103
|
+
`${label}: expected ${expected}, received ${actual}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Independent full-history/window oracle: do not share the handler's ring state.
|
|
107
|
+
const referenceMfi = (columns, size, retLogs = false) => {
|
|
108
|
+
const typical = Array.from(columns.close, (_, index) =>
|
|
109
|
+
(columns.high[index] + columns.low[index] + columns.close[index]) / 3)
|
|
110
|
+
const flows = typical.map((value, index) => {
|
|
111
|
+
if (index === 0) return NaN
|
|
112
|
+
const difference = value - typical[index - 1], money = value * columns.volume[index]
|
|
113
|
+
if (!Number.isFinite(difference) || !Number.isFinite(money) || money < 0) return NaN
|
|
114
|
+
return difference > 0 ? money : difference < 0 ? -money : 0
|
|
115
|
+
})
|
|
116
|
+
return flows.map((_, index) => {
|
|
117
|
+
if (index < size) return NaN
|
|
118
|
+
const window = flows.slice(index - size + 1, index + 1)
|
|
119
|
+
if (window.some(Number.isNaN)) return NaN
|
|
120
|
+
let positive = 0, negative = 0
|
|
121
|
+
for (const value of window) {
|
|
122
|
+
if (value > 0) positive += value
|
|
123
|
+
else if (value < 0) negative -= value
|
|
124
|
+
}
|
|
125
|
+
if (!Number.isFinite(positive) || !Number.isFinite(negative)) return NaN
|
|
126
|
+
if (positive === 0 && negative === 0) return NaN
|
|
127
|
+
const value = negative === 0 ? 100 : positive === 0 ? 0 : 100 - 100 / (1 + positive / negative)
|
|
128
|
+
return retLogs ? Math.log((value === 0 ? 0.001 : value) / 50) : value
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
const mfiInput = (prices, volumes = prices.map(() => 1), precision = false) => prices.map((price, index) => {
|
|
132
|
+
const value = precision ? String(price) : price
|
|
133
|
+
return { open: value, high: value, low: value, close: value, volume: volumes[index] }
|
|
134
|
+
})
|
|
135
|
+
|
|
84
136
|
// One independent registration per public indicator/study. The combined chain
|
|
85
137
|
// above remains useful for detecting interactions between different handlers.
|
|
86
138
|
const indicatorCases = [
|
|
87
|
-
{ name: 'ema', register: (m, o) => m.ema(5, o), keys:
|
|
88
|
-
{ name: 'sma', register: (m, o) => m.sma(5, o), keys:
|
|
139
|
+
{ name: 'ema', register: (m, o) => m.ema(5, o), keys: logs => [`${logs ? 'ret_log_' : ''}ema_5`], warmup: 4 },
|
|
140
|
+
{ name: 'sma', register: (m, o) => m.sma(5, o), keys: logs => [`${logs ? 'ret_log_' : ''}sma_5`], warmup: 4 },
|
|
89
141
|
{ name: 'bollingerBands', register: (m, o) => m.bollingerBands(8, 2, o),
|
|
90
|
-
//
|
|
91
|
-
keys:
|
|
142
|
+
// Preserve the existing first bands after size + 1 updates.
|
|
143
|
+
keys: logs => logs ? ['width', 'position'].map(k => `ret_log_bollinger_bands_${k}`)
|
|
144
|
+
: ['upper', 'middle', 'lower'].map(k => `bollinger_bands_${k}`), warmup: 8 },
|
|
92
145
|
{ name: 'macd', register: (m, o) => m.macd(4, 9, 3, o),
|
|
93
146
|
keys: () => ['diff', 'dea', 'histogram'].map(k => `macd_${k}`) },
|
|
94
147
|
{ name: 'relativeVolume', register: (m, o) => m.relativeVolume(6, o),
|
|
95
|
-
keys:
|
|
148
|
+
keys: logs => [`${logs ? 'ret_log_' : ''}relative_volume_6`], warmup: 6 },
|
|
96
149
|
{ name: 'volumeDelta', register: (m, o) => m.volumeDelta(o),
|
|
97
150
|
keys: () => ['high', 'low', 'close', 'cross'].map(k => `volume_delta_${k}`) },
|
|
98
151
|
{ name: 'volumeOscillator', register: (m, o) => m.volumeOscillator(3, 8, o),
|
|
99
|
-
keys: logs => [
|
|
152
|
+
keys: logs => [`${logs ? 'ret_log_' : ''}volume_oscillator_3_8`], warmup: 7 },
|
|
100
153
|
{ name: 'rsi', register: (m, o) => m.rsi(7, o),
|
|
101
154
|
keys: logs => ['rsi_7', 'rsi_sma_7'].map(k => `${logs ? 'ret_log_' : ''}${k}`) },
|
|
155
|
+
{ name: 'mfi', register: (m, o) => m.mfi(7, o), keys: logs => [`${logs ? 'ret_log_' : ''}mfi_7`], warmup: 7 },
|
|
102
156
|
{ name: 'stochastic', register: (m, o) => m.stochastic(7, 3, 3, o),
|
|
103
157
|
keys: logs => ['d', 'k'].map(k => `${logs ? 'ret_log_' : ''}stochastic_${k}_7_3_3`) },
|
|
104
158
|
{ name: 'atr', register: (m, o) => m.atr(7, o), keys: logs => [`${logs ? 'ret_log_' : ''}atr_7`] },
|
|
@@ -106,7 +160,8 @@ const indicatorCases = [
|
|
|
106
160
|
{ name: 'heikenAshi', register: (m, o) => m.heikenAshi(null, null, o),
|
|
107
161
|
keys: logs => [...['body', 'upper_wick', 'lower_wick', 'range'].map(k => `${logs ? 'ret_log_' : 'ret_'}heiken_ashi_${k}`), 'heiken_ashi_cross'] },
|
|
108
162
|
{ name: 'donchianChannels', register: (m, o) => m.donchianChannels(8, 2, o),
|
|
109
|
-
keys:
|
|
163
|
+
keys: logs => logs ? ['width', 'position'].map(k => `ret_log_donchian_channel_${k}`)
|
|
164
|
+
: ['upper', 'basis', 'lower'].map(k => `donchian_channel_${k}`), warmup: 9 },
|
|
110
165
|
{ name: 'dateTime', register: (m, o) => m.dateTime({ lag: o.lag, oneHot: o.retLogs }),
|
|
111
166
|
keys: oneHot => oneHot
|
|
112
167
|
? ['month', 'day_of_the_month', 'day_of_the_week', 'hour', 'minute'].map(k => `one_hot_${k}`)
|
|
@@ -115,7 +170,6 @@ const indicatorCases = [
|
|
|
115
170
|
keys: logs => ['change', 'mid_price_change', 'upper_wick', 'lower_wick', 'gap', 'body', 'range', 'close'].map(k => `${logs ? 'ret_log_' : 'ret_'}${k}`), warmup: 1 },
|
|
116
171
|
{ name: 'crossPairs', register: (m, o) => m.crossPairs([{ fast: 'close', slow: 'open' }], { limit: 5, oneHot: o.retLogs }),
|
|
117
172
|
keys: oneHot => ['close_x_open', ...(oneHot ? ['one_hot_close_x_open'] : [])], autoLag: false },
|
|
118
|
-
{ name: 'mapCols', register: (m, o) => m.mapCols(['change'], null, { lag: o.lag }), keys: () => ['change'], warmup: 1 },
|
|
119
173
|
{ name: 'lag', register: m => m.lag(['close', 'volume'], 2),
|
|
120
174
|
keys: () => ['close_lag_1', 'close_lag_2', 'volume_lag_1', 'volume_lag_2'], autoLag: false }
|
|
121
175
|
]
|
|
@@ -143,47 +197,115 @@ const retainedStorage = (value, seen = new Set()) => {
|
|
|
143
197
|
return total
|
|
144
198
|
}
|
|
145
199
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
// Fixed pre-optimization snapshots, not recomputed from the implementation under test.
|
|
201
|
+
// Each digest covers every update/readiness/result/extrema observation below.
|
|
202
|
+
// Number encoding preserves IEEE-754 values, including -0 and non-finite values;
|
|
203
|
+
// undefined is tagged rather than silently dropped by JSON serialization.
|
|
204
|
+
const traceValue = (hash, value) => hash.update(JSON.stringify(value, (_, item) => {
|
|
205
|
+
if (item === undefined) return { undefined: true }
|
|
206
|
+
if (typeof item === 'number') {
|
|
207
|
+
if (Number.isNaN(item)) return { number: 'NaN' }
|
|
208
|
+
const bytes = Buffer.allocUnsafe(8)
|
|
209
|
+
bytes.writeDoubleBE(item)
|
|
210
|
+
return { float64: bytes.toString('hex') }
|
|
211
|
+
}
|
|
212
|
+
return item
|
|
213
|
+
}))
|
|
214
|
+
const traceState = instance => [instance.isStable, readCoreResult(instance),
|
|
215
|
+
...['highest', 'lowest', 'previousResult', 'pdi', 'mdi'].map(key => instance[key])]
|
|
216
|
+
const coreTrace = (lib, test) => {
|
|
217
|
+
const hash = createHash('sha256')
|
|
218
|
+
for (const size of [1, 2, 7, 32]) for (const pattern of ['mixed', 'zero', 'invalid', 'coercion']) for (const replacements of [false, true]) {
|
|
219
|
+
const instance = test.create(lib, size)
|
|
220
|
+
traceValue(hash, [size, pattern, replacements, traceState(instance)])
|
|
221
|
+
let previousObject, previousSnapshot
|
|
222
|
+
for (let index = 0; index < 180; index++) {
|
|
223
|
+
// Binary-exact inputs avoid platform differences in transcendental functions.
|
|
224
|
+
let value = pattern === 'zero' ? (index % 2 ? -0 : 0) : 100 + (index % 23 - 11) / 8 + index / 256
|
|
225
|
+
if (pattern === 'invalid') {
|
|
226
|
+
if (index === 13) value = NaN
|
|
227
|
+
if (index === 41) value = Infinity
|
|
228
|
+
if (index === 83) value = -Infinity
|
|
229
|
+
}
|
|
230
|
+
if (pattern === 'coercion') {
|
|
231
|
+
if (index === 9) value = null
|
|
232
|
+
if (index === 10) value = undefined
|
|
233
|
+
if (index === 11) value = '2'
|
|
234
|
+
if (index === 12) value = true
|
|
235
|
+
if (index === 13) value = '3'
|
|
236
|
+
}
|
|
237
|
+
const input = test.candle ? { high: value + 2, low: value - 2, close: value } : value
|
|
238
|
+
const actual = instance.update(input, replacements && index % 5 === 0)
|
|
239
|
+
traceValue(hash, [actual, traceState(instance)])
|
|
240
|
+
if (actual && typeof actual === 'object') {
|
|
241
|
+
if (previousObject) {
|
|
242
|
+
assert.notStrictEqual(actual, previousObject)
|
|
243
|
+
assert.deepStrictEqual(previousObject, previousSnapshot)
|
|
184
244
|
}
|
|
245
|
+
previousObject = actual; previousSnapshot = { ...actual }
|
|
185
246
|
}
|
|
186
247
|
}
|
|
248
|
+
}
|
|
249
|
+
return hash.digest('hex')
|
|
250
|
+
}
|
|
251
|
+
const candleMutationTrace = (lib, test) => {
|
|
252
|
+
const hash = createHash('sha256'), instance = test.create(lib, 3)
|
|
253
|
+
const candles = [{ high: 4, low: 1, close: 2 }, { high: 5, low: 2, close: 3 }]
|
|
254
|
+
for (let index = 0; index < 80; index++) {
|
|
255
|
+
const candle = candles[index % 2]
|
|
256
|
+
candle.high = 100 + index; candle.low = 90 + index; candle.close = 95 + index
|
|
257
|
+
traceValue(hash, [instance.update(candle), traceState(instance)])
|
|
258
|
+
}
|
|
259
|
+
return hash.digest('hex')
|
|
260
|
+
}
|
|
261
|
+
const batchReplacementTrace = (lib, name) => {
|
|
262
|
+
const hash = createHash('sha256'), instance = new lib[name](3)
|
|
263
|
+
traceValue(hash, [instance.updates([1, , 2, 3, 4]), traceState(instance)])
|
|
264
|
+
for (const value of [0, NaN, 7, -0]) traceValue(hash, [instance.replace(value), traceState(instance)])
|
|
265
|
+
return hash.digest('hex')
|
|
266
|
+
}
|
|
267
|
+
// Captured from the untouched local core before the runtime-allocation upgrade.
|
|
268
|
+
// These constants are intentionally not regenerated by npm test or --baseline.
|
|
269
|
+
const coreGolden = {
|
|
270
|
+
FasterEMA: { trace: '78ab81db0dfead350fe56bb3954b0daa5cdefa3106c2a8e0da74dfd680f5f7f8', batch: '151b515d79fa9df1c4a0e107d4499fc5a8ee28a2713f3f51de0bef8f19d50a52' },
|
|
271
|
+
FasterSMA: { trace: 'bcfc82bc43e647da69bdec38c6c42a4606d393af582e2c583616f065866f23e8', batch: '54804fe1d0439cf6f050298fc5bb8e35633df78751c5878d0b1cd9e3c1c61a78' },
|
|
272
|
+
FasterWSMA: { trace: '875a98e2e03267d4af5ad7fb73d54f9bfd57b9e7289c34dc0e452249b6d065e0', batch: 'db6bb9608a9796bc73395259c7faa506994dcb8cfcbcb20ac55a6eba85e135b7' },
|
|
273
|
+
FasterRSI: { trace: 'f6ac351918308e9c3242ed2da9fa67b49c4d51308e7d288f311eaa3710d4c4ed' },
|
|
274
|
+
FasterTR: { trace: '648182a18eedea9f52658b9223029f514edfdcb83c53eacb4b757c8a7b3288d0', mutation: 'd9ed0b39278293e54fd6f170c3956327e2f38e76c8562ff28bde8bb03319233b' },
|
|
275
|
+
FasterATR: { trace: 'd5d85c88d26d7f61c066eb7561ad0735a20a11b9f6380e175b820f845ea9a5c4', mutation: '6ba2e5337c05b61bcec38f0f7abd366ce84576a4813e36847e287476644aa85a' },
|
|
276
|
+
FasterDX: { trace: '87aaf629d97a65e39c71312c6a73c9349bbf6f86ab390e8cd1f4b6381079e1ca', mutation: 'da5b536cec355236455d19c7104fae1791573304b66c4729b91bc4e6fbcb6c5b' },
|
|
277
|
+
FasterADX: { trace: '20c42d867ed187c7d153f019e27404780e29e91a312814dcde7b7c0d66ad0768', mutation: '7cd635bf30ec272f24b335a884f4c386eacbe6699b3c07810117e451c33419a6' },
|
|
278
|
+
FasterBollingerBands: { trace: 'de88f538a1761f8e441e18232961432eb09a37d729a513fa7d88975a088614f9' },
|
|
279
|
+
FasterMACD: { trace: '963a46f587757bc1d2f759a2334aa5d9e7ce6796e94719eb349cb821e08da6ba' },
|
|
280
|
+
FasterStochasticOscillator: { trace: '3f31db0f8601f19e7be61069bb8b6bad2faf89b73c4545ce212269206024eec5', mutation: 'ef88bce5c228f29c5f511a5f5e28ed620b7b10936eaa79179d4b1b6a6bb2fed3' }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const runRegressions = Baseline => {
|
|
284
|
+
let passed = 0
|
|
285
|
+
const check = (name, callback) => { callback(); console.log(`PASS ${name}`); passed++ }
|
|
286
|
+
check('core known values, exact warm-up, signed zero and invalid-window recovery', () => {
|
|
287
|
+
const ema = new Core.FasterEMA(3), sma = new Core.FasterSMA(3), wsma = new Core.FasterWSMA(3)
|
|
288
|
+
assert.throws(() => ema.getResult(), { name: 'NotEnoughDataError' })
|
|
289
|
+
assert.deepStrictEqual([1, 2, 3, 4].map(value => ema.update(value)), [1, 1.5, 2.25, 3.125])
|
|
290
|
+
assert.deepStrictEqual([1, 2, 3, 4].map(value => sma.update(value)), [undefined, undefined, 2, 3])
|
|
291
|
+
assert.deepStrictEqual([1, 2, 3, 8].map(value => wsma.update(value)), [undefined, undefined, 2, 4])
|
|
292
|
+
assert.equal(ema.isStable, true)
|
|
293
|
+
sma.update(NaN); sma.update(5); assert.ok(Number.isNaN(sma.getResult()))
|
|
294
|
+
sma.update(6); assert.ok(Number.isNaN(sma.getResult()))
|
|
295
|
+
assert.equal(sma.update(7), 6)
|
|
296
|
+
const rsi = new Core.FasterRSI(3)
|
|
297
|
+
assert.deepStrictEqual([1, 2, 3, 4].map(value => rsi.update(value)), [undefined, undefined, undefined, 100])
|
|
298
|
+
const bands = new Core.FasterBollingerBands(3)
|
|
299
|
+
assert.deepStrictEqual([1, 2, 3].map(value => bands.update(value)), [undefined, undefined, undefined])
|
|
300
|
+
assert.deepStrictEqual(bands.update(4), { lower: 3 - Math.sqrt(2 / 3) * 2, middle: 3, upper: 3 + Math.sqrt(2 / 3) * 2 })
|
|
301
|
+
const tr = new Core.FasterTR()
|
|
302
|
+
assert.equal(tr.update({ high: 4, low: 1, close: 2 }), 3)
|
|
303
|
+
assert.equal(tr.update({ high: 8, low: 5, close: 6 }), 6)
|
|
304
|
+
const zero = new Core.FasterEMA(1)
|
|
305
|
+
assert.ok(Object.is(zero.update(-0), -0))
|
|
306
|
+
})
|
|
307
|
+
for (const test of coreCases) check(`core ${test.name}: fixed pre-change traces`, () => {
|
|
308
|
+
assert.equal(coreTrace(Core, test), coreGolden[test.name].trace, `${test.name}: update/readiness/result/extrema trace changed`)
|
|
187
309
|
})
|
|
188
310
|
check('core storage stays bounded and Wilder smoothing releases its seed window', () => {
|
|
189
311
|
for (const test of coreCases) {
|
|
@@ -206,26 +328,12 @@ const runRegressions = Baseline => {
|
|
|
206
328
|
})
|
|
207
329
|
check('core candle windows retain reference semantics, including reused objects', () => {
|
|
208
330
|
for (const test of coreCases.filter(test => test.candle)) {
|
|
209
|
-
|
|
210
|
-
const candles = [{ high: 4, low: 1, close: 2 }, { high: 5, low: 2, close: 3 }]
|
|
211
|
-
for (let index = 0; index < 80; index++) {
|
|
212
|
-
const candle = candles[index % 2]
|
|
213
|
-
candle.high = 100 + index; candle.low = 90 + index; candle.close = 95 + index
|
|
214
|
-
assert.deepStrictEqual(current.update(candle), reference.update(candle))
|
|
215
|
-
assert.deepStrictEqual(readCoreResult(current), readCoreResult(reference))
|
|
216
|
-
}
|
|
331
|
+
assert.equal(candleMutationTrace(Core, test), coreGolden[test.name].mutation, `${test.name}: mutable candle trace changed`)
|
|
217
332
|
}
|
|
218
333
|
})
|
|
219
334
|
check('core batch and replacement helpers preserve readiness and extrema', () => {
|
|
220
335
|
for (const name of ['FasterEMA', 'FasterSMA', 'FasterWSMA']) {
|
|
221
|
-
|
|
222
|
-
const prices = [1, , 2, 3, 4]
|
|
223
|
-
assert.deepStrictEqual(current.updates(prices), reference.updates(prices))
|
|
224
|
-
for (const value of [0, NaN, 7, -0]) {
|
|
225
|
-
assert.deepStrictEqual(current.replace(value), reference.replace(value))
|
|
226
|
-
assert.deepStrictEqual(readCoreResult(current), readCoreResult(reference))
|
|
227
|
-
assert.deepStrictEqual([current.highest, current.lowest], [reference.highest, reference.lowest])
|
|
228
|
-
}
|
|
336
|
+
assert.equal(batchReplacementTrace(Core, name), coreGolden[name].batch, `${name}: sparse batch/replacement trace changed`)
|
|
229
337
|
}
|
|
230
338
|
})
|
|
231
339
|
check('coverage includes every public indicator method', () => {
|
|
@@ -282,9 +390,383 @@ const runRegressions = Baseline => {
|
|
|
282
390
|
const replay = new OHLCV_INDICATORS({ input, config: deepFreeze(JSON.parse(JSON.stringify(before))) })
|
|
283
391
|
assert.deepStrictEqual(snapshot(replay), snapshot(main))
|
|
284
392
|
assert.throws(() => test.register(main, options), /already computed/)
|
|
285
|
-
|
|
393
|
+
// A pre-MFI baseline still exercises every previously supported method.
|
|
394
|
+
if (Baseline && (test.name !== 'mfi' || typeof Baseline.prototype.mfi === 'function')) {
|
|
395
|
+
assert.deepStrictEqual(snapshot(main), snapshot(create(Baseline).compute()))
|
|
396
|
+
}
|
|
286
397
|
})
|
|
287
398
|
}
|
|
399
|
+
check('EMA/SMA known values and target-relative logs preserve warm-up and raw arithmetic', () => {
|
|
400
|
+
const prices = [10, 20, 30, 40, 20, 10]
|
|
401
|
+
const expected = { ema: [NaN, NaN, 22.5, 31.25, 25.625, 17.8125], sma: [NaN, NaN, 20, 30, 30, 70 / 3] }
|
|
402
|
+
for (const precision of [false, true]) for (const method of ['ema', 'sma']) {
|
|
403
|
+
const main = new OHLCV_INDICATORS({ input: mfiInput(prices, undefined, precision), config: { precision } })
|
|
404
|
+
[method](3)[method](3, { retLogs: true }).compute()
|
|
405
|
+
const key = `${method}_3`, v = main.verticalOhlcv
|
|
406
|
+
const scale = precision ? main.precisionMultiplier : 1
|
|
407
|
+
const raw = method === 'ema' ? expected.ema.map(value => value * scale)
|
|
408
|
+
: [NaN, NaN, ...[60, 90, 90, 70].map(sum => sum * scale / 3)]
|
|
409
|
+
assert.deepStrictEqual(Array.from(v[key]), raw)
|
|
410
|
+
assert.deepStrictEqual(Array.from(v[`ret_log_${key}`]), prices.map((price, index) => Math.log(price * scale / raw[index])))
|
|
411
|
+
assert.equal(v[`ret_log_${key}`].findIndex(Number.isFinite), 2)
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
check('new log modes select one output with independent raw/log state, target metadata, lags and frozen replay', () => {
|
|
415
|
+
const registrations = [
|
|
416
|
+
...['ema', 'sma'].flatMap(method => ['close', 'open', 'volume'].map(target => ({
|
|
417
|
+
method, options: { target }, key: `${method}_3${target === 'close' ? '' : `_${target}`}`
|
|
418
|
+
}))),
|
|
419
|
+
{ method: 'relativeVolume', options: {}, key: 'relative_volume_3' }
|
|
420
|
+
]
|
|
421
|
+
for (const precision of [false, true]) for (const { method, options, key } of registrations) {
|
|
422
|
+
const input = makeInput(160, precision), config = { precision, timeZone: 'UTC', skipNull: false }
|
|
423
|
+
const main = new OHLCV_INDICATORS({ input, config })
|
|
424
|
+
[method](3, { ...options, lag: 2 })[method](3, { ...options, lag: 2, retLogs: true })
|
|
425
|
+
const before = main.exportConfig(), saved = deepFreeze(JSON.parse(JSON.stringify(before)))
|
|
426
|
+
main.compute()
|
|
427
|
+
for (const retLogs of [false, true]) {
|
|
428
|
+
const separate = new OHLCV_INDICATORS({ input, config })[method](3, { ...options, lag: 2, retLogs }).compute()
|
|
429
|
+
const selected = `${retLogs ? 'ret_log_' : ''}${key}`, absent = `${retLogs ? '' : 'ret_log_'}${key}`
|
|
430
|
+
for (const suffix of ['', '_lag_1', '_lag_2']) {
|
|
431
|
+
assert.ok(!(absent + suffix in separate.verticalOhlcv))
|
|
432
|
+
assert.deepStrictEqual(main.verticalOhlcv[selected + suffix], separate.verticalOhlcv[selected + suffix])
|
|
433
|
+
assert.equal(main.priceBased.has(selected + suffix), !retLogs && ['close', 'open'].includes(options.target))
|
|
434
|
+
}
|
|
435
|
+
if (method !== 'relativeVolume') for (let index = 0; index < input.length; index++) {
|
|
436
|
+
const value = main.verticalOhlcv[options.target][index], average = main.verticalOhlcv[key][index]
|
|
437
|
+
const ratio = value / average
|
|
438
|
+
const expected = value > 0 && average > 0 && Number.isFinite(ratio) && ratio > 0 ? Math.log(ratio) : NaN
|
|
439
|
+
assert.strictEqual(main.verticalOhlcv[`ret_log_${key}`][index], expected)
|
|
440
|
+
}
|
|
441
|
+
const rows = separate.getData({ skipNull: false })
|
|
442
|
+
if (retLogs) for (let index = 0; index < input.length; index++) {
|
|
443
|
+
assert.strictEqual(rows[index][selected], separate.verticalOhlcv[selected][index])
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
assert.deepStrictEqual(main.exportConfig(), before)
|
|
447
|
+
for (let replay = 0; replay < 2; replay++) {
|
|
448
|
+
assert.deepStrictEqual(snapshot(new OHLCV_INDICATORS({ input, config: saved })), snapshot(main))
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
check('relativeVolume logs use the prior completed nonzero-volume window and reject negative operands', () => {
|
|
453
|
+
for (const precision of [false, true]) {
|
|
454
|
+
const volumes = [0, 10, 0, 20, 30, 0, 60, 30]
|
|
455
|
+
const input = mfiInput(volumes.map(() => 10.25), volumes, precision)
|
|
456
|
+
const main = new OHLCV_INDICATORS({ input, config: { precision } })
|
|
457
|
+
.relativeVolume(2, { lag: 1 }).relativeVolume(2, { retLogs: true, lag: 1 }).compute()
|
|
458
|
+
const expected = [NaN, NaN, NaN, NaN, 2, NaN, 2.4, 2 / 3]
|
|
459
|
+
const logged = expected.map(Math.log)
|
|
460
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.relative_volume_2), expected)
|
|
461
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.ret_log_relative_volume_2), logged)
|
|
462
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.ret_log_relative_volume_2_lag_1), [NaN, ...logged.slice(0, -1)])
|
|
463
|
+
const negativeVolumes = [10, -100, -100, 10, 10, 10, 10]
|
|
464
|
+
const negative = new OHLCV_INDICATORS({ input: mfiInput(negativeVolumes.map(() => 10.25), negativeVolumes, precision), config: { precision } })
|
|
465
|
+
.relativeVolume(2).relativeVolume(2, { retLogs: true }).compute()
|
|
466
|
+
assert.equal(negative.verticalOhlcv.relative_volume_2[2], -100 / -45)
|
|
467
|
+
assert.deepStrictEqual(Array.from(negative.verticalOhlcv.ret_log_relative_volume_2), [NaN, NaN, NaN, NaN, NaN, 0, 0])
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
check('new moving-average logs leave invalid, nonpositive and unrepresentable ratios as NaN', () => {
|
|
471
|
+
for (const precision of [false, true]) for (const method of ['ema', 'sma']) {
|
|
472
|
+
for (const invalid of [0, -1000, NaN, Infinity, -Infinity]) {
|
|
473
|
+
const input = makeInput(12, precision)
|
|
474
|
+
input.forEach((row, index) => { row.signal = index === 4 ? invalid : 10 })
|
|
475
|
+
const main = new OHLCV_INDICATORS({ input, config: { precision } })
|
|
476
|
+
[method](3, { target: 'signal' })[method](3, { target: 'signal', retLogs: true }).compute()
|
|
477
|
+
const v = main.verticalOhlcv, raw = v[`${method}_3_signal`], logged = v[`ret_log_${method}_3_signal`]
|
|
478
|
+
assert.ok(Number.isNaN(logged[4]))
|
|
479
|
+
for (let index = 0; index < input.length; index++) {
|
|
480
|
+
const value = v.signal[index], ratio = value / raw[index]
|
|
481
|
+
const expected = value > 0 && raw[index] > 0 && Number.isFinite(ratio) && ratio > 0 ? Math.log(ratio) : NaN
|
|
482
|
+
assert.strictEqual(logged[index], expected, `${method}: ${invalid} at index ${index}`)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// The core average can remain finite while a positive ratio underflows.
|
|
487
|
+
for (const method of ['ema', 'sma']) {
|
|
488
|
+
const input = makeInput(5)
|
|
489
|
+
input.forEach((row, index) => { row.signal = index < 3 ? 1e200 : Number.MIN_VALUE })
|
|
490
|
+
const main = new OHLCV_INDICATORS({ input })[method](3, { target: 'signal', retLogs: true }).compute()
|
|
491
|
+
assert.ok(Number.isNaN(main.verticalOhlcv[`ret_log_${method}_3_signal`][3]))
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
check('new log options validate booleans, reject duplicates and preserve raw defaults in stored configs', () => {
|
|
495
|
+
const input = makeInput(120)
|
|
496
|
+
for (const method of ['ema', 'sma', 'relativeVolume']) {
|
|
497
|
+
for (const retLogs of [null, 0, 1, 'true', [], {}]) {
|
|
498
|
+
assert.throws(() => new OHLCV_INDICATORS({ input })[method](3, { retLogs }), /options.retLogs/)
|
|
499
|
+
}
|
|
500
|
+
for (const retLogs of [false, true]) {
|
|
501
|
+
assert.throws(() => new OHLCV_INDICATORS({ input })[method](3, { retLogs })[method](3, { retLogs }).compute(), /already|duplicate/i)
|
|
502
|
+
}
|
|
503
|
+
const defaults = new OHLCV_INDICATORS({ input, config: { skipNull: false } })[method](3, { lag: 1 })
|
|
504
|
+
const saved = defaults.exportConfig()
|
|
505
|
+
assert.equal(saved.inputParams[0].params.at(-1).retLogs, false)
|
|
506
|
+
delete saved.inputParams[0].params.at(-1).retLogs
|
|
507
|
+
deepFreeze(saved)
|
|
508
|
+
defaults.compute()
|
|
509
|
+
const restored = new OHLCV_INDICATORS({ input, config: saved })
|
|
510
|
+
assert.deepStrictEqual(restored.verticalOhlcv, defaults.verticalOhlcv)
|
|
511
|
+
assert.deepStrictEqual(restored.priceBased, defaults.priceBased)
|
|
512
|
+
assert.deepStrictEqual(restored.getData(), defaults.getData())
|
|
513
|
+
assert.deepStrictEqual(restored.exportConfig(), saved)
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
check('channel logs match independent formulas, preserve raw windows and support both registration orders', () => {
|
|
517
|
+
const cases = [
|
|
518
|
+
{ method: 'bollingerBands', prefix: 'bollinger_bands', args: [5, 2], rawParts: ['upper', 'middle', 'lower'], warmup: 5 },
|
|
519
|
+
{ method: 'donchianChannels', prefix: 'donchian_channel', args: [5, 2], rawParts: ['upper', 'basis', 'lower'], warmup: 6 }
|
|
520
|
+
]
|
|
521
|
+
for (const precision of [false, true]) for (const test of cases) for (const reverse of [false, true]) {
|
|
522
|
+
const input = makeInput(120, precision), config = { precision, skipNull: false, timeZone: 'UTC' }
|
|
523
|
+
const main = new OHLCV_INDICATORS({ input, config })
|
|
524
|
+
for (const retLogs of reverse ? [true, false] : [false, true]) main[test.method](...test.args, { retLogs, lag: 2 })
|
|
525
|
+
const before = main.exportConfig(), saved = deepFreeze(JSON.parse(JSON.stringify(before)))
|
|
526
|
+
main.compute()
|
|
527
|
+
const v = main.verticalOhlcv, suffix = `_${test.args.join('_')}`
|
|
528
|
+
const raw = test.method === 'bollingerBands' ? referenceBollinger(v.close, ...test.args)
|
|
529
|
+
: Array.from(v.close, (_, index) => {
|
|
530
|
+
const end = index - test.args[1], start = end - test.args[0] + 1
|
|
531
|
+
if (start < 0) return { upper: NaN, basis: NaN, lower: NaN }
|
|
532
|
+
const upper = Math.max(...v.high.slice(start, end + 1)), lower = Math.min(...v.low.slice(start, end + 1))
|
|
533
|
+
return { upper, basis: (upper + lower) / 2, lower }
|
|
534
|
+
})
|
|
535
|
+
for (const part of test.rawParts) {
|
|
536
|
+
assert.deepStrictEqual(Array.from(v[`${test.prefix}_${part}${suffix}`]), raw.map(row => row[part]))
|
|
537
|
+
}
|
|
538
|
+
for (const part of ['width', 'position']) {
|
|
539
|
+
const key = `ret_log_${test.prefix}_${part}${suffix}`
|
|
540
|
+
assert.equal(v[key].findIndex(Number.isFinite), test.warmup)
|
|
541
|
+
for (let index = 0; index < input.length; index++) {
|
|
542
|
+
const expected = referenceChannelLogs(raw[index].upper, raw[index].lower, v.close[index])
|
|
543
|
+
assertNear(v[key][index], expected[part], `${key}[${index}]`)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
for (const retLogs of [false, true]) {
|
|
547
|
+
const separate = new OHLCV_INDICATORS({ input, config: { ...config, useFullNames: true } })
|
|
548
|
+
[test.method](...test.args, { retLogs, lag: 2 }).compute()
|
|
549
|
+
const parts = retLogs ? ['width', 'position'] : test.rawParts
|
|
550
|
+
const keys = parts.map(part => `${retLogs ? 'ret_log_' : ''}${test.prefix}_${part}${suffix}`)
|
|
551
|
+
const expectedKeys = keys.flatMap(key => [key, `${key}_lag_1`, `${key}_lag_2`])
|
|
552
|
+
assert.deepStrictEqual(Object.keys(separate.verticalOhlcv).filter(key => key.includes(test.prefix)).sort(), expectedKeys.sort())
|
|
553
|
+
const rows = separate.getData({ skipNull: false })
|
|
554
|
+
for (const key of keys) for (let lag = 0; lag <= 2; lag++) {
|
|
555
|
+
const selected = key + (lag ? `_lag_${lag}` : '')
|
|
556
|
+
assert.deepStrictEqual(v[selected], separate.verticalOhlcv[selected])
|
|
557
|
+
assert.equal(main.priceBased.has(selected), !retLogs)
|
|
558
|
+
for (let index = 0; index < input.length; index++) {
|
|
559
|
+
assert.strictEqual(v[selected][index], index < lag ? NaN : v[key][index - lag])
|
|
560
|
+
if (retLogs) assert.strictEqual(rows[index][selected], separate.verticalOhlcv[selected][index])
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
assert.deepStrictEqual(main.exportConfig(), before)
|
|
565
|
+
const rows = main.getData({ skipNull: false })
|
|
566
|
+
assert.deepStrictEqual(main.getData({ skipNull: true }), rows.slice(main.invalidValueIndex + 1))
|
|
567
|
+
assert.deepStrictEqual(main.getLastValues(), rows.at(-1))
|
|
568
|
+
for (let replay = 0; replay < 2; replay++) {
|
|
569
|
+
assert.deepStrictEqual(snapshot(new OHLCV_INDICATORS({ input, config: saved })), snapshot(main))
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
check('channel log names separate multiple configurations and explicit lags replay unchanged', () => {
|
|
574
|
+
const input = makeInput(120)
|
|
575
|
+
for (const [method, prefix] of [['bollingerBands', 'bollinger_bands'], ['donchianChannels', 'donchian_channel']]) {
|
|
576
|
+
const main = new OHLCV_INDICATORS({ input, config: { skipNull: false } })
|
|
577
|
+
[method](3, 1, { retLogs: true })[method](7, 2, { retLogs: true })
|
|
578
|
+
.lag([`ret_log_${prefix}_width_3_1`, `ret_log_${prefix}_position_7_2`], 2)
|
|
579
|
+
const saved = deepFreeze(JSON.parse(JSON.stringify(main.exportConfig())))
|
|
580
|
+
main.compute()
|
|
581
|
+
for (const args of [[3, 1], [7, 2]]) {
|
|
582
|
+
const separate = new OHLCV_INDICATORS({ input, config: { useFullNames: true } })
|
|
583
|
+
[method](...args, { retLogs: true }).compute()
|
|
584
|
+
for (const part of ['width', 'position']) {
|
|
585
|
+
const key = `ret_log_${prefix}_${part}_${args.join('_')}`
|
|
586
|
+
assert.deepStrictEqual(main.verticalOhlcv[key], separate.verticalOhlcv[key])
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
assert.deepStrictEqual(snapshot(new OHLCV_INDICATORS({ input, config: saved })), snapshot(main))
|
|
590
|
+
}
|
|
591
|
+
})
|
|
592
|
+
check('flat channel logs retain zero width, undefined position and lag warm-up', () => {
|
|
593
|
+
for (const precision of [false, true]) for (const size of [1, 3]) {
|
|
594
|
+
const input = mfiInput(Array(12).fill(10), undefined, precision)
|
|
595
|
+
for (const [method, prefix, parameter, warmup] of [
|
|
596
|
+
['bollingerBands', 'bollinger_bands', 2, size],
|
|
597
|
+
['donchianChannels', 'donchian_channel', 2, size + 1]
|
|
598
|
+
]) {
|
|
599
|
+
const main = new OHLCV_INDICATORS({ input, config: { precision } })
|
|
600
|
+
[method](size, parameter, { retLogs: true, lag: 1 }).compute()
|
|
601
|
+
const v = main.verticalOhlcv, width = `ret_log_${prefix}_width`, position = `ret_log_${prefix}_position`
|
|
602
|
+
assert.deepStrictEqual(Array.from(v[width]), input.map((_, index) => index < warmup ? NaN : 0))
|
|
603
|
+
assert.deepStrictEqual(Array.from(v[`${width}_lag_1`]), [NaN, ...v[width].slice(0, -1)])
|
|
604
|
+
assert.ok(v[position].every(Number.isNaN))
|
|
605
|
+
assert.ok(v[`${position}_lag_1`].every(Number.isNaN))
|
|
606
|
+
assert.equal(main.invalidValueIndex, input.length - 1)
|
|
607
|
+
assert.deepStrictEqual(main.getData({ skipNull: true }), [])
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const main = new OHLCV_INDICATORS({ input: mfiInput([10, 20, 30]) })
|
|
611
|
+
.donchianChannels(1, 1, { retLogs: true }).compute()
|
|
612
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.ret_log_donchian_channel_width), [NaN, 0, 0])
|
|
613
|
+
assert.ok(main.verticalOhlcv.ret_log_donchian_channel_position.every(Number.isNaN))
|
|
614
|
+
})
|
|
615
|
+
check('Bollinger logs reject zero/negative lower bands and recover without altering raw bands', () => {
|
|
616
|
+
for (const prices of [[1, 1, 3, 4, 5, 6], [1, 1, 10, 10, 11, 12]]) for (const precision of [false, true]) {
|
|
617
|
+
const input = mfiInput(prices, undefined, precision)
|
|
618
|
+
const main = new OHLCV_INDICATORS({ input, config: { precision } })
|
|
619
|
+
.bollingerBands(2, 2).bollingerBands(2, 2, { retLogs: true }).compute()
|
|
620
|
+
const v = main.verticalOhlcv
|
|
621
|
+
assert.ok(v.bollinger_bands_lower_2_2[2] <= 0)
|
|
622
|
+
assert.ok(Number.isNaN(v.ret_log_bollinger_bands_width_2_2[2]))
|
|
623
|
+
assert.ok(Number.isNaN(v.ret_log_bollinger_bands_position_2_2[2]))
|
|
624
|
+
assert.ok(Number.isFinite(v.ret_log_bollinger_bands_width_2_2[4]))
|
|
625
|
+
assert.ok(Number.isFinite(v.ret_log_bollinger_bands_position_2_2[4]))
|
|
626
|
+
const raw = referenceBollinger(v.close, 2, 2)
|
|
627
|
+
assert.deepStrictEqual(Array.from(v.bollinger_bands_lower_2_2), raw.map(row => row.lower))
|
|
628
|
+
}
|
|
629
|
+
})
|
|
630
|
+
check('Donchian log position uses current close against offset bounds and does not clip breakouts', () => {
|
|
631
|
+
const input = [100, 110, 90, 150, 60].map(close => ({ open: close, high: 110, low: 90, close, volume: 1 }))
|
|
632
|
+
const main = new OHLCV_INDICATORS({ input }).donchianChannels(1, 1, { retLogs: true }).compute()
|
|
633
|
+
const v = main.verticalOhlcv
|
|
634
|
+
assert.ok(Number.isNaN(v.ret_log_donchian_channel_position[0]))
|
|
635
|
+
assertNear(v.ret_log_donchian_channel_position[1], 1, 'upper boundary')
|
|
636
|
+
assertNear(v.ret_log_donchian_channel_position[2], -1, 'lower boundary')
|
|
637
|
+
assert.ok(v.ret_log_donchian_channel_position[3] > 1)
|
|
638
|
+
assert.ok(v.ret_log_donchian_channel_position[4] < -1)
|
|
639
|
+
for (let index = 1; index < input.length; index++) {
|
|
640
|
+
const expected = referenceChannelLogs(110, 90, input[index].close)
|
|
641
|
+
assertNear(v.ret_log_donchian_channel_width[index], expected.width, 'offset width')
|
|
642
|
+
assertNear(v.ret_log_donchian_channel_position[index], expected.position, 'offset position')
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
check('channel widths and positions are invariant to price scaling and precision conversion', () => {
|
|
646
|
+
const prices = [10.25, 11.5, 9.75, 12.25, 11.75, 10.5, 13.25, 12.5, 11.25, 14.5]
|
|
647
|
+
for (const [method, prefix] of [['bollingerBands', 'bollinger_bands'], ['donchianChannels', 'donchian_channel']]) {
|
|
648
|
+
const base = new OHLCV_INDICATORS({ input: mfiInput(prices) })[method](3, 2, { retLogs: true }).compute()
|
|
649
|
+
for (const precision of [false, true]) for (const scale of [1, 10, 1000]) {
|
|
650
|
+
const input = mfiInput(prices.map(value => value * scale), undefined, precision)
|
|
651
|
+
const main = new OHLCV_INDICATORS({ input, config: { precision } })[method](3, 2, { retLogs: true }).compute()
|
|
652
|
+
for (const part of ['width', 'position']) {
|
|
653
|
+
const key = `ret_log_${prefix}_${part}`
|
|
654
|
+
for (let index = 0; index < prices.length; index++) {
|
|
655
|
+
assertNear(main.verticalOhlcv[key][index], base.verticalOhlcv[key][index], `${key}: precision=${precision}, scale=${scale}`)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
check('channel logs reject invalid bounds/ratios but preserve width when only current close is invalid', () => {
|
|
662
|
+
const triples = [
|
|
663
|
+
[110, 90, 100], [110, 0, 100], [110, -1, 100], [NaN, 90, 100],
|
|
664
|
+
[Infinity, 90, 100], [90, 110, 100], [110, 90, 0], [110, 90, -1],
|
|
665
|
+
[110, 90, NaN], [110, 90, Infinity], [1e308, 1e-308, 100],
|
|
666
|
+
[110, 90, Number.MIN_VALUE], [110, 90, 100]
|
|
667
|
+
]
|
|
668
|
+
const input = triples.map(([high, low, close]) => ({ open: 100, high, low, close, volume: 1 }))
|
|
669
|
+
const main = new OHLCV_INDICATORS({ input, config: { skipNull: false } })
|
|
670
|
+
.donchianChannels(1, 0, { retLogs: true, lag: 1 }).compute()
|
|
671
|
+
const v = main.verticalOhlcv
|
|
672
|
+
for (let index = 0; index < input.length; index++) {
|
|
673
|
+
const expected = referenceChannelLogs(...triples[index])
|
|
674
|
+
assertNear(v.ret_log_donchian_channel_width[index], expected.width, `invalid bounds width[${index}]`)
|
|
675
|
+
assertNear(v.ret_log_donchian_channel_position[index], expected.position, `invalid bounds position[${index}]`)
|
|
676
|
+
}
|
|
677
|
+
for (const index of [6, 7, 8, 9, 11]) {
|
|
678
|
+
assert.ok(Number.isFinite(v.ret_log_donchian_channel_width[index]))
|
|
679
|
+
assert.ok(Number.isNaN(v.ret_log_donchian_channel_position[index]))
|
|
680
|
+
}
|
|
681
|
+
assert.ok(Number.isFinite(v.ret_log_donchian_channel_position.at(-1)))
|
|
682
|
+
assert.ok(Number.isNaN(v.ret_log_donchian_channel_position_lag_1.at(-1)))
|
|
683
|
+
})
|
|
684
|
+
check('channel log options validate booleans, reject duplicates and preserve legacy raw config defaults', () => {
|
|
685
|
+
const input = makeInput(120)
|
|
686
|
+
for (const method of ['bollingerBands', 'donchianChannels']) {
|
|
687
|
+
for (const retLogs of [null, 0, 1, 'true', [], {}]) {
|
|
688
|
+
assert.throws(() => new OHLCV_INDICATORS({ input })[method](3, 2, { retLogs }), /retLogs/)
|
|
689
|
+
}
|
|
690
|
+
for (const retLogs of [false, true]) {
|
|
691
|
+
assert.throws(() => new OHLCV_INDICATORS({ input })[method](3, 2, { retLogs })[method](3, 2, { retLogs }).compute(), /already|duplicate/i)
|
|
692
|
+
}
|
|
693
|
+
const main = new OHLCV_INDICATORS({ input, config: { skipNull: false } })[method](3, 2, { lag: 1 })
|
|
694
|
+
const saved = main.exportConfig()
|
|
695
|
+
assert.equal(saved.inputParams[0].params.at(-1).retLogs, false)
|
|
696
|
+
delete saved.inputParams[0].params.at(-1).retLogs
|
|
697
|
+
deepFreeze(saved)
|
|
698
|
+
main.compute()
|
|
699
|
+
const replay = new OHLCV_INDICATORS({ input, config: saved })
|
|
700
|
+
assert.deepStrictEqual(replay.verticalOhlcv, main.verticalOhlcv)
|
|
701
|
+
assert.deepStrictEqual(replay.priceBased, main.priceBased)
|
|
702
|
+
assert.deepStrictEqual(replay.getData(), main.getData())
|
|
703
|
+
assert.deepStrictEqual(replay.exportConfig(), saved)
|
|
704
|
+
}
|
|
705
|
+
})
|
|
706
|
+
check('ATR logs and their lags remain dimensionless during precision output conversion', () => {
|
|
707
|
+
for (const precision of [false, true]) {
|
|
708
|
+
const input = makeInput(120, precision)
|
|
709
|
+
const main = new OHLCV_INDICATORS({ input, config: { precision, skipNull: false } })
|
|
710
|
+
.atr(7, { lag: 2 }).atr(7, { lag: 2, retLogs: true })
|
|
711
|
+
const saved = deepFreeze(main.exportConfig())
|
|
712
|
+
main.compute()
|
|
713
|
+
const v = main.verticalOhlcv, rows = main.getData()
|
|
714
|
+
for (const suffix of ['', '_lag_1', '_lag_2']) {
|
|
715
|
+
assert.ok(main.priceBased.has(`atr_7${suffix}`))
|
|
716
|
+
assert.ok(!main.priceBased.has(`ret_log_atr_7${suffix}`))
|
|
717
|
+
for (let index = 0; index < input.length; index++) {
|
|
718
|
+
assert.strictEqual(rows[index][`ret_log_atr_7${suffix}`], v[`ret_log_atr_7${suffix}`][index])
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
for (let index = 0; index < input.length; index++) {
|
|
722
|
+
assert.strictEqual(v.ret_log_atr_7[index], Math.log(v.atr_7[index] / v.close[index]))
|
|
723
|
+
const expected = Number.isNaN(v.atr_7[index]) ? NaN : precision
|
|
724
|
+
? revertPrecisionAsNumber(v.atr_7[index], main.precisionMultiplier) : v.atr_7[index]
|
|
725
|
+
assert.strictEqual(rows[index].atr_7, expected)
|
|
726
|
+
}
|
|
727
|
+
assert.deepStrictEqual(main.exportConfig(), saved)
|
|
728
|
+
assert.deepStrictEqual(snapshot(new OHLCV_INDICATORS({ input, config: saved })), snapshot(main))
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
check('volumeOscillator raw/log variants have independent state, selected lags and config replay', () => {
|
|
732
|
+
for (const precision of [false, true]) {
|
|
733
|
+
const input = makeInput(160, precision)
|
|
734
|
+
const config = { precision, timeZone: 'UTC', skipNull: false }
|
|
735
|
+
const main = new OHLCV_INDICATORS({ input, config })
|
|
736
|
+
.volumeOscillator(3, 8, { lag: 2 })
|
|
737
|
+
.volumeOscillator(3, 8, { retLogs: true, lag: 2 })
|
|
738
|
+
const before = main.exportConfig()
|
|
739
|
+
main.compute()
|
|
740
|
+
for (const retLogs of [false, true]) {
|
|
741
|
+
const separate = new OHLCV_INDICATORS({ input, config })
|
|
742
|
+
.volumeOscillator(3, 8, { retLogs, lag: 2 }).compute()
|
|
743
|
+
const key = `${retLogs ? 'ret_log_' : ''}volume_oscillator_3_8`
|
|
744
|
+
const otherKey = `${retLogs ? '' : 'ret_log_'}volume_oscillator_3_8`
|
|
745
|
+
for (const suffix of ['', '_lag_1', '_lag_2']) {
|
|
746
|
+
assert.ok(!(otherKey + suffix in separate.verticalOhlcv))
|
|
747
|
+
assert.deepStrictEqual(main.verticalOhlcv[key + suffix], separate.verticalOhlcv[key + suffix])
|
|
748
|
+
assert.ok(!main.priceBased.has(key + suffix))
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
assert.deepStrictEqual(main.exportConfig(), before)
|
|
752
|
+
const saved = deepFreeze(JSON.parse(JSON.stringify(before)))
|
|
753
|
+
for (let replay = 0; replay < 2; replay++) {
|
|
754
|
+
assert.deepStrictEqual(snapshot(new OHLCV_INDICATORS({ input, config: saved })), snapshot(main))
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
})
|
|
758
|
+
check('volumeOscillator explicit lags require their selected source column', () => {
|
|
759
|
+
const input = makeInput(160)
|
|
760
|
+
const missingRaw = new OHLCV_INDICATORS({ input })
|
|
761
|
+
.volumeOscillator(3, 8, { retLogs: true }).lag(['volume_oscillator_3_8'], 1)
|
|
762
|
+
assert.throws(() => new OHLCV_INDICATORS({ input, config: missingRaw.exportConfig() }), /volume_oscillator_3_8.*not found/)
|
|
763
|
+
const main = new OHLCV_INDICATORS({ input })
|
|
764
|
+
.volumeOscillator(3, 8, { retLogs: true })
|
|
765
|
+
.volumeOscillator(3, 8)
|
|
766
|
+
.lag(['volume_oscillator_3_8', 'ret_log_volume_oscillator_3_8'], 1)
|
|
767
|
+
const saved = main.exportConfig()
|
|
768
|
+
assert.deepStrictEqual(snapshot(main.compute()), snapshot(new OHLCV_INDICATORS({ input, config: saved })))
|
|
769
|
+
})
|
|
288
770
|
for (const precision of [false, true]) for (const retLogs of [false, true]) {
|
|
289
771
|
check(`all indicators: precision=${precision}, retLogs=${retLogs}, exact replay/chunks`, () => {
|
|
290
772
|
const input = makeInput(320, precision), options = { precision, retLogs }
|
|
@@ -306,9 +788,14 @@ const runRegressions = Baseline => {
|
|
|
306
788
|
assert.deepStrictEqual(snapshot(configure(OHLCV_INDICATORS, input, { ...options, chunkProcess: 2000 }).compute()), expected)
|
|
307
789
|
const exported = main.exportConfig()
|
|
308
790
|
exported.inputParams[0].params[1] = 99
|
|
309
|
-
exported.inputParams.find(job => job.key === '
|
|
791
|
+
exported.inputParams.find(job => job.key === 'crossPairs').params[0][0].fast = 'high'
|
|
792
|
+
exported.inputParams.find(job => job.key === 'lag').params[0][0] = 'open'
|
|
310
793
|
assert.deepStrictEqual(main.exportConfig(), before)
|
|
311
|
-
if (Baseline)
|
|
794
|
+
if (Baseline) {
|
|
795
|
+
const includeMfi = typeof Baseline.prototype.mfi === 'function'
|
|
796
|
+
const compatibleExpected = includeMfi ? expected : snapshot(configure(OHLCV_INDICATORS, input, { ...options, includeMfi: false }).compute())
|
|
797
|
+
assert.deepStrictEqual(compatibleExpected, snapshot(configure(Baseline, input, { ...options, includeMfi }).compute()))
|
|
798
|
+
}
|
|
312
799
|
})
|
|
313
800
|
}
|
|
314
801
|
for (const pattern of ['increasing', 'decreasing', 'equal', 'mixed-invalid']) for (const precision of [false, true]) {
|
|
@@ -378,73 +865,273 @@ const runRegressions = Baseline => {
|
|
|
378
865
|
assert.throws(() => new OHLCV_INDICATORS({ input }).lag(['missing'], 1).compute(), /not found/)
|
|
379
866
|
assert.throws(() => new OHLCV_INDICATORS({ input }).lag(['extra', 'extra'], 1).compute(), /already exists/)
|
|
380
867
|
})
|
|
381
|
-
check('
|
|
382
|
-
const input = makeInput(120)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
assert.
|
|
396
|
-
assert.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
for (const mutation of ['lookback', 'splice', 'replace']) {
|
|
402
|
-
const create = Ctor => new Ctor({ input }).mapCols(['marker'], ({ index, main }) => {
|
|
403
|
-
if (index === 3) {
|
|
404
|
-
const job = main.executionParams.find(job => job.key === 'lag')
|
|
405
|
-
if (mutation === 'lookback') job.params[1] = 0
|
|
406
|
-
if (mutation === 'splice') job.params[0].splice(0, 1)
|
|
407
|
-
if (mutation === 'replace') job.params[0] = ['high']
|
|
408
|
-
}
|
|
409
|
-
return { marker: index }
|
|
410
|
-
}).lag(['close', 'high'], 2).compute()
|
|
411
|
-
const main = create(OHLCV_INDICATORS)
|
|
412
|
-
assert.deepStrictEqual(main.inputParams[1].params, [['close', 'high'], 2])
|
|
413
|
-
assert.equal(main.verticalOhlcv.close_lag_1[2], input[1].close)
|
|
414
|
-
assert.ok(Number.isNaN(main.verticalOhlcv.close_lag_1[3]))
|
|
415
|
-
if (mutation === 'lookback') assert.ok(Number.isNaN(main.verticalOhlcv.high_lag_1[3]))
|
|
416
|
-
else assert.equal(main.verticalOhlcv.high_lag_1[3], input[2].high)
|
|
417
|
-
if (Baseline) assert.deepStrictEqual(main.verticalOhlcv, create(Baseline).verticalOhlcv)
|
|
868
|
+
check('removed custom-column APIs are absent and stored declarations are rejected', () => {
|
|
869
|
+
const input = makeInput(120), main = new OHLCV_INDICATORS({ input })
|
|
870
|
+
assert.equal('mapCols' in main, false)
|
|
871
|
+
assert.equal('registerMapCallback' in OHLCV_INDICATORS, false)
|
|
872
|
+
const config = main.exportConfig()
|
|
873
|
+
config.inputParams = [{ key: 'mapCols', params: [['custom'], 'default', { lag: 0, isPriceBased: false }] }]
|
|
874
|
+
assert.throws(() => new OHLCV_INDICATORS({ input, config }), /Unknown indicator "mapCols"/)
|
|
875
|
+
})
|
|
876
|
+
check('configuration rejects non-JSON values and runtime jobs do not share declarations', () => {
|
|
877
|
+
const input = makeInput(120), main = new OHLCV_INDICATORS({ input }).lag(['close', 'high'], 2)
|
|
878
|
+
const before = main.exportConfig()
|
|
879
|
+
main.compute()
|
|
880
|
+
const job = main.executionParams.find(job => job.key === 'lag')
|
|
881
|
+
job.params[0][0] = 'low'; job.params[1] = 1
|
|
882
|
+
assert.deepStrictEqual(main.inputParams[0].params, [['close', 'high'], 2])
|
|
883
|
+
assert.deepStrictEqual(main.exportConfig(), before)
|
|
884
|
+
const circular = {}; circular.self = circular
|
|
885
|
+
for (const value of [() => 1, undefined, NaN, Infinity, -Infinity, new Date(), new Map(), 1n, Symbol('invalid'), circular]) {
|
|
886
|
+
const config = { inputParams: [{ key: 'ema', params: ['ema', 5, { target: 'close', lag: 0, retLogs: false, extra: value }] }] }
|
|
887
|
+
assert.throws(() => new OHLCV_INDICATORS({ input, config }), /JSON-safe|plain objects|circular/)
|
|
418
888
|
}
|
|
419
889
|
})
|
|
420
890
|
check('output conversion: precision, dates, null/NaN, property order and identity', () => {
|
|
421
891
|
const input = makeInput(120, true)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
.mapCols(['vector'], ({ index }) => ({ vector: [index] })).compute()
|
|
426
|
-
return main
|
|
427
|
-
}
|
|
892
|
+
input.forEach((row, index) => { row.vector = index === 1 ? null : [index] })
|
|
893
|
+
const create = Ctor => new Ctor({ input, config: { precision: true, timeZone: 'UTC', skipNull: false } })
|
|
894
|
+
.lag(['close'], 1).dateTime({ oneHot: true, lag: 1 }).compute()
|
|
428
895
|
const main = create(OHLCV_INDICATORS), expected = Baseline ? create(Baseline) : null
|
|
429
|
-
assert.
|
|
896
|
+
assert.deepStrictEqual(snapshot(new OHLCV_INDICATORS({ input, config: main.exportConfig() })), snapshot(main))
|
|
430
897
|
for (const dateFormat of ['milliseconds', 'seconds', 'iso', 'toISOString', 'object', 'string', 'toString', 'toUTCString']) for (const skipNull of [false, true]) {
|
|
431
898
|
const rows = main.getData({ dateFormat, skipNull })
|
|
432
899
|
if (expected) assert.deepStrictEqual(rows, expected.getData({ dateFormat, skipNull }))
|
|
433
900
|
if (!skipNull) {
|
|
434
|
-
assert.ok(Number.isNaN(rows[0].
|
|
435
|
-
assert.equal(rows[2].
|
|
901
|
+
assert.ok(Number.isNaN(rows[0].close_lag_1)); assert.equal(rows[1].vector, null)
|
|
902
|
+
assert.equal(rows[2].close_lag_1, Number(input[1].close))
|
|
436
903
|
assert.strictEqual(rows[2].vector, main.verticalOhlcv.vector[2])
|
|
904
|
+
assert.strictEqual(rows[2].one_hot_hour, main.verticalOhlcv.one_hot_hour[2])
|
|
905
|
+
assert.equal(rows[0].one_hot_hour_lag_1, null)
|
|
437
906
|
if (dateFormat === 'object') assert.strictEqual(rows[2].date, main.verticalOhlcv.date[2])
|
|
438
907
|
}
|
|
439
908
|
assert.deepStrictEqual(Object.keys(rows[0]), Object.keys(main.verticalOhlcv))
|
|
440
909
|
}
|
|
441
910
|
})
|
|
442
|
-
check('
|
|
443
|
-
const input = makeInput(120), main = new OHLCV_INDICATORS({ input }).
|
|
911
|
+
check('empty declarations replay and first-row validation', () => {
|
|
912
|
+
const input = makeInput(120), main = new OHLCV_INDICATORS({ input }).compute()
|
|
444
913
|
assert.deepStrictEqual(snapshot(new OHLCV_INDICATORS({ input, config: main.exportConfig() })), snapshot(main))
|
|
445
914
|
const invalid = makeInput(120); invalid[0].high = 0
|
|
446
915
|
assert.throws(() => new OHLCV_INDICATORS({ input: invalid }).donchianChannels(3).compute(), /invalid/i)
|
|
447
|
-
if (Baseline) assert.deepStrictEqual(snapshot(main), snapshot(new Baseline({ input }).
|
|
916
|
+
if (Baseline) assert.deepStrictEqual(snapshot(main), snapshot(new Baseline({ input }).compute()))
|
|
917
|
+
})
|
|
918
|
+
check('cached validation includes input columns and generated outputs', () => {
|
|
919
|
+
const input = makeInput(120)
|
|
920
|
+
delete input[11].high; input[77].extra = NaN
|
|
921
|
+
const create = Ctor => new Ctor({ input, config: { timeZone: 'UTC' } })
|
|
922
|
+
.ema(5, { lag: 2 }).dateTime().crossPairs([{ fast: 'close', slow: 100 }]).compute()
|
|
923
|
+
const main = create(OHLCV_INDICATORS)
|
|
924
|
+
assert.equal(main.invalidValueIndex, 77)
|
|
925
|
+
assert.equal(main.getData().length, 42)
|
|
926
|
+
if (Baseline) assert.deepStrictEqual(snapshot(main), snapshot(create(Baseline)))
|
|
927
|
+
const generated = new OHLCV_INDICATORS({ input: makeInput(120), config: { timeZone: 'UTC' } })
|
|
928
|
+
.ema(5, { lag: 2 }).dateTime({ oneHot: true, lag: 1 }).compute()
|
|
929
|
+
assert.equal(generated.invalidValueIndex, 5)
|
|
930
|
+
assert.equal(generated.getData().length, 114)
|
|
931
|
+
})
|
|
932
|
+
check('cross counters retain bounded state and independent one-hot output vectors', () => {
|
|
933
|
+
const input = makeInput(2048)
|
|
934
|
+
input.forEach((row, index) => { row.close = row.open + (index % 2 ? -1 : 1) })
|
|
935
|
+
const observed = []
|
|
936
|
+
const main = new OHLCV_INDICATORS({ input })
|
|
937
|
+
.crossPairs([{ fast: 'close', slow: 'open' }], { limit: 3, oneHot: true })
|
|
938
|
+
const push = main.pushToMain
|
|
939
|
+
main.pushToMain = output => {
|
|
940
|
+
push(output)
|
|
941
|
+
if (output.key === 'one_hot_close_x_open' && (output.index === 127 || output.index === input.length - 1)) {
|
|
942
|
+
const run = main.instances.crossPairs.close_x_open.run
|
|
943
|
+
observed.push(retainedStorage(run))
|
|
944
|
+
assert.ok(!('crossIndexes' in run), 'unused event history must not be retained')
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
main.compute()
|
|
948
|
+
assert.deepStrictEqual(observed, [{ slots: 0, bytes: 0 }, { slots: 0, bytes: 0 }])
|
|
949
|
+
const v = main.verticalOhlcv
|
|
950
|
+
assert.deepStrictEqual(Array.from(v.close_x_open), input.map((_, index) => index % 2 ? -1 : 1))
|
|
951
|
+
assert.notStrictEqual(v.one_hot_close_x_open[0], v.one_hot_close_x_open[2])
|
|
952
|
+
v.one_hot_close_x_open[0][4] = 0
|
|
953
|
+
assert.equal(v.one_hot_close_x_open[2][4], 1)
|
|
954
|
+
})
|
|
955
|
+
check('calendar scalar writes preserve DST, timezone boundaries and one-hot/lag identity', () => {
|
|
956
|
+
const input = makeInput(5)
|
|
957
|
+
const dates = ['2024-03-10T06:30:00Z', '2024-03-10T07:30:00Z', '2024-11-03T05:30:00Z', '2024-11-03T06:30:00Z', '2025-01-01T04:30:00Z']
|
|
958
|
+
input.forEach((row, index) => { row.date = dates[index] })
|
|
959
|
+
for (const oneHot of [false, true]) {
|
|
960
|
+
const create = Ctor => new Ctor({ input, config: { timeZone: 'America/New_York', skipNull: false } }).dateTime({ oneHot, lag: 1 }).compute()
|
|
961
|
+
const main = create(OHLCV_INDICATORS), v = main.verticalOhlcv
|
|
962
|
+
if (Baseline) assert.deepStrictEqual(snapshot(main), snapshot(create(Baseline)))
|
|
963
|
+
if (oneHot) {
|
|
964
|
+
assert.deepStrictEqual(v.one_hot_hour.map(vector => vector.indexOf(1)), [1, 3, 1, 1, 23])
|
|
965
|
+
assert.deepStrictEqual(v.one_hot_day_of_the_week.map(vector => vector.indexOf(1)), [0, 0, 0, 0, 2])
|
|
966
|
+
assert.notStrictEqual(v.one_hot_hour[2], v.one_hot_hour[3])
|
|
967
|
+
assert.strictEqual(v.one_hot_hour_lag_1[3], v.one_hot_hour[2])
|
|
968
|
+
assert.equal(v.one_hot_hour_lag_1[0], null)
|
|
969
|
+
v.one_hot_hour[2][1] = 0
|
|
970
|
+
assert.equal(v.one_hot_hour[3][1], 1)
|
|
971
|
+
} else {
|
|
972
|
+
assert.deepStrictEqual(Array.from(v.hour), [1, 3, 1, 1, 23])
|
|
973
|
+
assert.deepStrictEqual(Array.from(v.day_of_the_week), [7, 7, 7, 7, 2])
|
|
974
|
+
assert.equal(v.year[4], 2024); assert.equal(v.month[4], 12); assert.equal(v.day_of_the_month[4], 31)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
})
|
|
978
|
+
check('precision metadata caching preserves truncation, unusual values and per-call scale', () => {
|
|
979
|
+
const additions = [
|
|
980
|
+
['123.456789', 10000, 1234567], ['-0.00019', 10000, -1],
|
|
981
|
+
['000123.4500', 10000, 1234500], ['12.999', 1, 12], ['-0', 10000, -0],
|
|
982
|
+
['0.000000000123456789', 1e14, 12345], ['NaN', 10000, NaN], ['1e2', 10000, 1]
|
|
983
|
+
]
|
|
984
|
+
for (const [value, multiplier, expected] of additions) assert.strictEqual(addPrecisionAsNumber(value, multiplier), expected)
|
|
985
|
+
const reversions = [
|
|
986
|
+
[1234567.999, 10000, 123.4567], [-1234567.999, 10000, -123.4567],
|
|
987
|
+
[2.999, 1, 2], [-2.999, 1, -2], [-0, 10000, 0], [-0.1, 10000, -0],
|
|
988
|
+
[12345, 1e14, 0.00000000012345], [1, 10000, 0.0001]
|
|
989
|
+
]
|
|
990
|
+
for (const [value, multiplier, expected] of reversions) assert.strictEqual(revertPrecisionAsNumber(value, multiplier), expected)
|
|
991
|
+
assert.throws(() => addPrecisionAsNumber(12, 10000), /expects a string/)
|
|
992
|
+
for (const value of [NaN, Infinity, -Infinity, null, '123']) assert.throws(() => revertPrecisionAsNumber(value, 10000), /finite number/)
|
|
993
|
+
const input = makeInput(120, true)
|
|
994
|
+
const main = new OHLCV_INDICATORS({ input, config: { precision: true } }).compute()
|
|
995
|
+
const before = main.exportConfig()
|
|
996
|
+
for (const multiplier of [10000, 100000, 1, 10000]) {
|
|
997
|
+
main.precisionMultiplier = multiplier
|
|
998
|
+
const rows = main.getData({ skipNull: false })
|
|
999
|
+
assert.equal(rows[0].close, revertPrecisionAsNumber(main.verticalOhlcv.close[0], multiplier))
|
|
1000
|
+
}
|
|
1001
|
+
assert.deepStrictEqual(main.exportConfig(), before)
|
|
1002
|
+
})
|
|
1003
|
+
check('MFI known values: neutral logs, bounds, independent raw/log state and lagged replay', () => {
|
|
1004
|
+
for (const precision of [false, true]) {
|
|
1005
|
+
const input = mfiInput([10, 20, 10, 20, 10], [1, 1, 2, 1, 2], precision)
|
|
1006
|
+
const main = new OHLCV_INDICATORS({ input, config: { precision, useFullNames: true, timeZone: 'UTC', skipNull: false } })
|
|
1007
|
+
.mfi(2, { lag: 1 }).mfi(2, { retLogs: true, lag: 1 }).mfi(1)
|
|
1008
|
+
const before = main.exportConfig(), frozen = deepFreeze(JSON.parse(JSON.stringify(before)))
|
|
1009
|
+
main.compute()
|
|
1010
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.mfi_2), [NaN, NaN, 50, 50, 50])
|
|
1011
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.ret_log_mfi_2), [NaN, NaN, 0, 0, 0])
|
|
1012
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.mfi_2_lag_1), [NaN, NaN, NaN, 50, 50])
|
|
1013
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.mfi_1), [NaN, 100, 0, 100, 0])
|
|
1014
|
+
for (const key of ['mfi_1', 'mfi_2', 'ret_log_mfi_2', 'mfi_2_lag_1', 'ret_log_mfi_2_lag_1']) {
|
|
1015
|
+
assert.ok(main.verticalOhlcv[key] instanceof Float64Array)
|
|
1016
|
+
assert.ok(!main.priceBased.has(key), `${key} must remain dimensionless`)
|
|
1017
|
+
}
|
|
1018
|
+
assert.deepStrictEqual(main.exportConfig(), before)
|
|
1019
|
+
for (let replay = 0; replay < 2; replay++) {
|
|
1020
|
+
assert.deepStrictEqual(snapshot(new OHLCV_INDICATORS({ input, config: frozen })), snapshot(main))
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
for (const direction of [1, -1]) {
|
|
1024
|
+
const input = mfiInput(Array.from({ length: 12 }, (_, index) => 20 + direction * index))
|
|
1025
|
+
const main = new OHLCV_INDICATORS({ input }).mfi(3).mfi(3, { retLogs: true }).compute()
|
|
1026
|
+
const raw = direction === 1 ? 100 : 0, logged = Math.log((raw || 0.001) / 50)
|
|
1027
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.mfi_3), [NaN, NaN, NaN, ...Array(9).fill(raw)])
|
|
1028
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv.ret_log_mfi_3), [NaN, NaN, NaN, ...Array(9).fill(logged)])
|
|
1029
|
+
}
|
|
1030
|
+
})
|
|
1031
|
+
check('MFI ties and zero-volume candles advance the window; empty directional windows stay NaN', () => {
|
|
1032
|
+
const ties = new OHLCV_INDICATORS({ input: mfiInput([10, 20, 20, 10], [1, 1, 999, 2]) }).mfi(2).compute()
|
|
1033
|
+
assert.deepStrictEqual(Array.from(ties.verticalOhlcv.mfi_2), [NaN, NaN, 100, 0])
|
|
1034
|
+
const zeros = new OHLCV_INDICATORS({ input: mfiInput([10, 20, 30, 20, 10], [0, 0, 0, 1, 1]) }).mfi(2).compute()
|
|
1035
|
+
assert.deepStrictEqual(Array.from(zeros.verticalOhlcv.mfi_2), [NaN, NaN, NaN, 0, 0])
|
|
1036
|
+
const zeroWindow = new OHLCV_INDICATORS({ input: mfiInput([10, 20, 30, 40, 50], [1, 1, 0, 0, 1]) }).mfi(2).compute()
|
|
1037
|
+
assert.deepStrictEqual(Array.from(zeroWindow.verticalOhlcv.mfi_2), [NaN, NaN, 100, NaN, 100])
|
|
1038
|
+
for (const input of [mfiInput([10, 10, 10, 10, 10]), mfiInput([10, 20, 10, 20, 10], [0, 0, 0, 0, 0])]) {
|
|
1039
|
+
const main = new OHLCV_INDICATORS({ input }).mfi(2).mfi(2, { retLogs: true }).compute()
|
|
1040
|
+
assert.ok(Array.from(main.verticalOhlcv.mfi_2).every(Number.isNaN))
|
|
1041
|
+
assert.ok(Array.from(main.verticalOhlcv.ret_log_mfi_2).every(Number.isNaN))
|
|
1042
|
+
assert.deepStrictEqual(main.getData(), [])
|
|
1043
|
+
}
|
|
1044
|
+
})
|
|
1045
|
+
check('MFI exact independent chronological-window reference across precision, logs and ring wraps', () => {
|
|
1046
|
+
for (const precision of [false, true]) for (const retLogs of [false, true]) {
|
|
1047
|
+
for (const pattern of ['mixed', 'flat', 'invalid', 'negative-volume']) for (const size of [1, 3, 7, 128]) {
|
|
1048
|
+
const prices = Array.from({ length: 257 }, (_, index) => pattern === 'flat' ? 100 : 100 + (index % 23 - 11) / 8 + index / 256)
|
|
1049
|
+
const volumes = prices.map((_, index) => index % 17 === 0 ? 0 : 20 + index % 11)
|
|
1050
|
+
if (pattern === 'invalid') prices[55] = NaN
|
|
1051
|
+
if (pattern === 'negative-volume') volumes[55] = -10
|
|
1052
|
+
const main = new OHLCV_INDICATORS({ input: mfiInput(prices, volumes, precision), chunkProcess: 100, config: { precision } })
|
|
1053
|
+
.mfi(size, { retLogs }).compute()
|
|
1054
|
+
const key = `${retLogs ? 'ret_log_' : ''}mfi_${size}`
|
|
1055
|
+
assert.deepStrictEqual(Array.from(main.verticalOhlcv[key]), referenceMfi(main.verticalOhlcv, size, retLogs), `${key}: ${pattern}, precision=${precision}`)
|
|
1056
|
+
if (!retLogs) for (const value of main.verticalOhlcv[key]) {
|
|
1057
|
+
assert.ok(Number.isNaN(value) || (value >= 0 && value <= 100))
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
})
|
|
1062
|
+
check('MFI invalid samples recover only after affected flows leave the window', () => {
|
|
1063
|
+
const prices = Array.from({ length: 14 }, (_, index) => 10 + index)
|
|
1064
|
+
const invalidTypical = mfiInput(prices)
|
|
1065
|
+
invalidTypical[5].high = NaN
|
|
1066
|
+
const main = new OHLCV_INDICATORS({ input: invalidTypical }).mfi(3).compute()
|
|
1067
|
+
assert.equal(main.verticalOhlcv.mfi_3[4], 100)
|
|
1068
|
+
for (const index of [5, 6, 7, 8]) assert.ok(Number.isNaN(main.verticalOhlcv.mfi_3[index]))
|
|
1069
|
+
assert.equal(main.verticalOhlcv.mfi_3[9], 100)
|
|
1070
|
+
for (const kind of ['money-overflow', 'negative-flow']) {
|
|
1071
|
+
const input = mfiInput(prices)
|
|
1072
|
+
if (kind === 'money-overflow') Object.assign(input[5], { high: 1e300, low: 1e300, close: 1e300, volume: 1000000000 })
|
|
1073
|
+
else input[5].volume = -1
|
|
1074
|
+
const changed = new OHLCV_INDICATORS({ input }).mfi(3).compute()
|
|
1075
|
+
for (const index of [5, 6, 7]) assert.ok(Number.isNaN(changed.verticalOhlcv.mfi_3[index]))
|
|
1076
|
+
assert.ok(Number.isFinite(changed.verticalOhlcv.mfi_3[8]))
|
|
1077
|
+
if (kind === 'negative-flow') assert.equal(changed.verticalOhlcv.mfi_3[8], 100)
|
|
1078
|
+
assert.deepStrictEqual(Array.from(changed.verticalOhlcv.mfi_3), referenceMfi(changed.verticalOhlcv, 3))
|
|
1079
|
+
}
|
|
1080
|
+
})
|
|
1081
|
+
check('MFI argument/source validation, defaults, duplicates and computation guards', () => {
|
|
1082
|
+
const input = makeInput(120)
|
|
1083
|
+
const defaults = new OHLCV_INDICATORS({ input }).mfi()
|
|
1084
|
+
assert.deepStrictEqual(defaults.exportConfig().inputParams, [{ key: 'mfi', params: [14, { lag: 0, retLogs: false }] }])
|
|
1085
|
+
for (const size of [0, -1, 1.5, '2', 121, NaN, Infinity]) assert.throws(() => new OHLCV_INDICATORS({ input }).mfi(size))
|
|
1086
|
+
for (const lag of [-1, 1.5, 121, NaN, Infinity]) assert.throws(() => new OHLCV_INDICATORS({ input }).mfi(3, { lag }))
|
|
1087
|
+
for (const retLogs of [null, 0, 1, 'true']) assert.throws(() => new OHLCV_INDICATORS({ input }).mfi(3, { retLogs }))
|
|
1088
|
+
assert.throws(() => new OHLCV_INDICATORS({ input }).mfi(3).mfi(3).compute(), /already|duplicate/i)
|
|
1089
|
+
assert.throws(() => new OHLCV_INDICATORS({ input }).mfi(3, { retLogs: true }).mfi(3, { retLogs: true }).compute(), /already|duplicate/i)
|
|
1090
|
+
const computed = defaults.compute()
|
|
1091
|
+
assert.throws(() => computed.mfi(3), /already computed/)
|
|
1092
|
+
const guarded = new OHLCV_INDICATORS({ input }).mfi(3)
|
|
1093
|
+
const push = guarded.pushToMain
|
|
1094
|
+
let checked = false
|
|
1095
|
+
guarded.pushToMain = output => {
|
|
1096
|
+
if (output.index === 3) {
|
|
1097
|
+
assert.throws(() => guarded.mfi(3), /computation is in progress/)
|
|
1098
|
+
assert.throws(() => guarded.compute(), /already in progress/)
|
|
1099
|
+
checked = true
|
|
1100
|
+
}
|
|
1101
|
+
push(output)
|
|
1102
|
+
}
|
|
1103
|
+
guarded.compute()
|
|
1104
|
+
assert.equal(checked, true)
|
|
1105
|
+
for (const key of ['high', 'low', 'close', 'volume']) {
|
|
1106
|
+
const missing = makeInput(120); delete missing[0][key]
|
|
1107
|
+
assert.throws(() => new OHLCV_INDICATORS({ input: missing }).mfi(3).compute())
|
|
1108
|
+
const invalid = makeInput(120); invalid[0][key] = key === 'volume' ? -1 : 0
|
|
1109
|
+
assert.throws(() => new OHLCV_INDICATORS({ input: invalid }).mfi(3).compute(), /Invalid or missing/)
|
|
1110
|
+
}
|
|
1111
|
+
})
|
|
1112
|
+
check('MFI retains one period-sized Float64Array per independent configuration', () => {
|
|
1113
|
+
const sizes = [1, 7, 37], observed = []
|
|
1114
|
+
const main = new OHLCV_INDICATORS({ input: makeInput(8192) })
|
|
1115
|
+
for (const size of sizes) main.mfi(size).mfi(size, { retLogs: true })
|
|
1116
|
+
const push = main.pushToMain
|
|
1117
|
+
main.pushToMain = output => {
|
|
1118
|
+
push(output)
|
|
1119
|
+
if (output.key === 'ret_log_mfi_37' && (output.index === 255 || output.index === main.len - 1)) {
|
|
1120
|
+
const buffers = []
|
|
1121
|
+
for (const size of sizes) for (const prefix of ['', 'ret_log_']) {
|
|
1122
|
+
const state = main.instances[`${prefix}mfi_${size}`]
|
|
1123
|
+
assert.deepStrictEqual(retainedStorage(state), { slots: 0, bytes: size * 8 })
|
|
1124
|
+
const arrays = Object.values(state).filter(ArrayBuffer.isView)
|
|
1125
|
+
assert.equal(arrays.length, 1)
|
|
1126
|
+
assert.ok(arrays[0] instanceof Float64Array)
|
|
1127
|
+
assert.ok(!buffers.includes(arrays[0])); buffers.push(arrays[0])
|
|
1128
|
+
}
|
|
1129
|
+
observed.push(buffers.map(buffer => buffer.byteLength))
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
main.compute()
|
|
1133
|
+
assert.deepStrictEqual(observed[0], observed[1])
|
|
1134
|
+
assert.equal(observed.length, 2)
|
|
448
1135
|
})
|
|
449
1136
|
console.log(`\n${passed} deterministic regression groups passed${Baseline ? '; exact baseline comparisons passed' : ''}.`)
|
|
450
1137
|
}
|
|
@@ -453,24 +1140,39 @@ const benchmark = Baseline => {
|
|
|
453
1140
|
const rows = Number(option('--rows') ?? 10000), repeats = Number(option('--repeats') ?? 7)
|
|
454
1141
|
assert.ok(Number.isInteger(rows) && rows >= 256 && rows <= 100000, '--rows must be 256..100000')
|
|
455
1142
|
assert.ok(Number.isInteger(repeats) && repeats >= 3 && repeats <= 30, '--repeats must be 3..30')
|
|
456
|
-
const input = makeInput(rows)
|
|
457
1143
|
const entries = Baseline ? [['baseline', Baseline], ['current', OHLCV_INDICATORS]] : [['current', OHLCV_INDICATORS]]
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
for (const [name, Ctor] of repetition % 2 ? [...entries].reverse() : entries) {
|
|
462
|
-
global.gc?.()
|
|
463
|
-
const main = new Ctor({ input, config: { timeZone: 'UTC' } })
|
|
1144
|
+
const profiles = [
|
|
1145
|
+
{ name: 'numeric', input: makeInput(rows), create: (Ctor, input) =>
|
|
1146
|
+
new Ctor({ input, config: { timeZone: 'UTC' } })
|
|
464
1147
|
.ema(10, { lag: 3 }).sma(20, { lag: 3 }).rsi(14, { retLogs: true, lag: 2 })
|
|
465
1148
|
.donchianChannels(128, 0, { lag: 2 }).donchianChannels(255, 5, { lag: 1 })
|
|
466
|
-
.volumeOscillator(5, 10, { retLogs: true, lag: 2 }).lag(['close', 'volume'], 3)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1149
|
+
.volumeOscillator(5, 10, { retLogs: true, lag: 2 }).lag(['close', 'volume'], 3) },
|
|
1150
|
+
{ name: 'features+precision', input: makeInput(rows, true), create: (Ctor, input) =>
|
|
1151
|
+
new Ctor({ input, config: { timeZone: 'America/New_York', precision: true } })
|
|
1152
|
+
.sma(10, { lag: 2 }).heikenAshi(3, 4, { retLogs: true, lag: 1 })
|
|
1153
|
+
.candleFeatures({ retLogs: true }).dateTime({ lag: 1 })
|
|
1154
|
+
.crossPairs([{ fast: 'close', slow: 'open' }], { limit: 5, oneHot: true })
|
|
1155
|
+
}
|
|
1156
|
+
]
|
|
1157
|
+
const samples = {}
|
|
1158
|
+
for (const profile of profiles) {
|
|
1159
|
+
for (const [name] of entries) samples[`${profile.name} / ${name}`] = { compute: [], output: [] }
|
|
1160
|
+
for (let repetition = -2; repetition < repeats; repetition++) {
|
|
1161
|
+
// Alternate order to reduce warm-up/GC order bias. No timing assertions.
|
|
1162
|
+
for (const [name, Ctor] of repetition % 2 ? [...entries].reverse() : entries) {
|
|
1163
|
+
global.gc?.()
|
|
1164
|
+
const main = profile.create(Ctor, profile.input)
|
|
1165
|
+
const start = performance.now()
|
|
1166
|
+
main.compute()
|
|
1167
|
+
const computed = performance.now()
|
|
1168
|
+
const output = main.getData({ skipNull: false, dateFormat: 'milliseconds' })
|
|
1169
|
+
const converted = performance.now()
|
|
1170
|
+
assert.equal(output.length, rows)
|
|
1171
|
+
if (repetition >= 0) {
|
|
1172
|
+
const sample = samples[`${profile.name} / ${name}`]
|
|
1173
|
+
sample.compute.push(computed - start); sample.output.push(converted - computed)
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
474
1176
|
}
|
|
475
1177
|
}
|
|
476
1178
|
const median = values => {
|
|
@@ -489,30 +1191,25 @@ const benchmarkCore = () => {
|
|
|
489
1191
|
const candles = prices.map(close => ({ high: close + 2, low: close - 2, close }))
|
|
490
1192
|
const results = {}
|
|
491
1193
|
for (const test of coreCases) {
|
|
492
|
-
const samples =
|
|
493
|
-
|
|
1194
|
+
const samples = []
|
|
1195
|
+
let storage
|
|
494
1196
|
for (let repetition = -2; repetition < repeats; repetition++) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (repetition === repeats - 1) storage[name] = retainedStorage(instance)
|
|
503
|
-
}
|
|
1197
|
+
const instance = test.create(Core, 32), input = test.candle ? candles : prices
|
|
1198
|
+
const start = performance.now()
|
|
1199
|
+
for (const value of input) instance.update(value)
|
|
1200
|
+
const elapsed = performance.now() - start
|
|
1201
|
+
assert.equal(instance.isStable, true)
|
|
1202
|
+
if (repetition >= 0) samples.push(elapsed)
|
|
1203
|
+
if (repetition === repeats - 1) storage = retainedStorage(instance)
|
|
504
1204
|
}
|
|
505
|
-
assert.deepStrictEqual(last.core, last.reference)
|
|
506
1205
|
const median = values => {
|
|
507
1206
|
const sorted = [...values].sort((a, b) => a - b), mid = Math.floor(sorted.length / 2)
|
|
508
1207
|
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
|
|
509
1208
|
}
|
|
510
1209
|
results[test.name] = {
|
|
511
|
-
'
|
|
512
|
-
'core
|
|
513
|
-
'
|
|
514
|
-
'core array slots': storage.core.slots,
|
|
515
|
-
'core typed bytes': storage.core.bytes
|
|
1210
|
+
'core ms': Number(median(samples).toFixed(3)),
|
|
1211
|
+
'core array slots': storage.slots,
|
|
1212
|
+
'core typed bytes': storage.bytes
|
|
516
1213
|
}
|
|
517
1214
|
}
|
|
518
1215
|
console.log(`\nCore benchmark: ${rows} updates, period 32, 2 warm-ups, ${repeats} samples; median milliseconds`)
|