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/AGENTS.md CHANGED
@@ -12,7 +12,7 @@ Input rows normally contain `open`, `high`, `low`, `close`, and `volume`; `date`
12
12
  2. `compute()`, `getData()`, or `getLastValues()` starts the one-shot calculation. Do not allow indicators to be registered after computation.
13
13
  3. `compute()` copies declarations into `main.executionParams` and sorts only that runtime queue. `src/core-functions/mainLoop.js` creates input columns in `main.verticalOhlcv`, walks rows in chronological order, writes the raw row and `mid_price`, and invokes every queued handler from `mainFunctions`.
14
14
  4. Indicator handlers initialize state and output columns when `index === 0`, keep rolling state in `main.instances`, update once per row, and write through `main.pushToMain()`.
15
- 5. Output columns start as `NaN` during warm-up or after an unexpected value. `areKeyValuesValid()` records the last invalid row in `main.invalidValueIndex`; `getData({ skipNull: true })` starts after that row.
15
+ 5. Output columns start as `NaN` during warm-up or after an unexpected value. `areKeyValuesValid()` determines the last invalid row recorded in `main.invalidValueIndex`; `getData({ skipNull: true })` starts after that row. Cache validation column references only after all row-zero handlers and generated lags finish.
16
16
  6. `verticalToHorizontal()` converts the column store back to row objects. Temporary columns in `verticalOhlcvTempCols` are not returned.
17
17
 
18
18
  ### Indicator contract
@@ -25,45 +25,53 @@ Input rows normally contain `open`, `high`, `low`, `close`, and `volume`; `date`
25
25
  - Add an output to `main.priceBased` only when it remains in scaled price units. Ratios, percentages, return logs, counters, dates, volume, and one-hot vectors are not price-based.
26
26
  - `retLogs` outputs use natural log ratios through `mathLog(a, b)` where applicable. Logged values are dimensionless.
27
27
  - Keep output names deterministic. When multiple configurations are supported, include enough parameters and target information to prevent collisions.
28
- - `src/core-indicators/` supplies compatible numeric EMA, SMA, WSMA, RSI, MACD, Bollinger Bands, stochastic, ATR, and ADX implementations. Use cheap `isStable` checks before `getResult()`; do not use exceptions for routine warm-up. The installed `trading-signals@5.0.4` remains a test oracle, not a production import.
28
+ - `src/core-indicators/` supplies numeric EMA, SMA, WSMA, RSI, MACD, Bollinger Bands, stochastic, ATR, and ADX implementations without an external indicator dependency. Use cheap `isStable` checks before `getResult()`; do not use exceptions for routine warm-up. Core tests use known results and fixed behavior traces, not a third-party test oracle.
29
29
  - Preserve reference arithmetic order, warm-up, zero/NaN behavior, and update/replacement sequencing in core classes. SMA/Bollinger use bounded chronological rings rather than rolling sums that change rounding. RSI history and MACD counters must stay bounded; WSMA releases its seed window. Runtime state must never enter exported config.
30
+ - Cross detection retains only state needed for the next update, not arrays of historical cross indexes. Preserve output counters and clipping behavior.
31
+ - Allocation optimizations must preserve handler evaluation/write order, optional-target all-or-nothing validation, and independent one-hot vectors. Never share mutable result vectors between unrelated rows.
30
32
 
31
33
  ## Configuration export and replay guards
32
34
 
33
35
  - `config` contains `schemaVersion` (currently `1`), `precision`, `useFullNames`, `inputParams`, `dateFormat`, `skipNull`, and `timeZone`.
34
36
  - Omitting `config.inputParams` (or setting it to `null`) creates a chainable instance. Supplying an array, including `[]`, imports independent copies and computes immediately.
35
- - `exportConfig()` returns a new JSON-safe snapshot. Export before and after `compute()`, `getData()`, or `getLastValues()` must produce equivalent configuration. Never include generated lag jobs, indicator instances, buffers, cached column references, or resolved callback functions in that snapshot.
37
+ - `exportConfig()` returns a new JSON-safe snapshot. Export before and after `compute()`, `getData()`, or `getLastValues()` must produce equivalent configuration. Never include generated lag jobs, indicator instances, buffers, or cached column references in that snapshot.
36
38
  - `this.inputParams` is persistent configuration, not a work queue. Keep declarations deeply copied and frozen. Register public methods through `_registerIndicator()`; never sort, push into, or edit existing declarations directly. Runtime caching and parameter mutation belong only in `executionParams` or `instances`.
37
39
  - Sorting and generated jobs must affect only `executionParams`. Import must never mutate the caller's config, even when the same saved object is replayed repeatedly. Exports must not share nested arrays or objects with the instance or the imported config.
38
40
  - Every public indicator method must reject registration during or after computation. Preserve the `isComputing` reentrancy guard and the `isComputed` guard, including for `stochastic()`.
39
- - `mapCols` accepts a callback function or a registered string name. Register portable callbacks with `OHLCV_INDICATORS.registerMapCallback('name.v1', callback)` before loading configs. The built-in callback uses the reserved name `default`. Never overwrite a registered name with different code; use a new versioned name.
40
- - Callback arguments belong in `mapCols` options as JSON-safe `callbackParams`; callbacks receive them as `params` in `{ index, main, params }`. Resolve callbacks and copy their arguments only into the runtime queue. Registered callbacks must derive results from supplied input/params, not changing closure state, clocks, randomness, or external services.
41
- - Unregistered callbacks may execute locally, but export must throw explicitly. Do not serialize function source, evaluate stored code, or silently let JSON replace functions, undefined, or non-finite numbers with null. Reject unsupported config versions, circular references, and non-JSON objects.
41
+ - All indicator parameters must be JSON-safe. Reject functions, undefined, non-finite numbers, circular references, and non-JSON objects instead of silently allowing JSON to omit or replace them with null. Reject unsupported config versions and unknown indicator methods.
42
42
  - `dateFormat` and `skipNull` in config are output defaults. Per-call getter overrides must not modify exported settings. `timeZone` is stored explicitly (defaults to the resolved runtime timezone) and controls `dateTime` calendar features through a per-instance formatter.
43
- - Cross-runtime replay requires identical inputs, callback implementations, and library versions. Use unambiguous input dates (valid Date objects, supported timestamps, or ISO strings with `Z`/offsets). Timezone-less date strings and local `dateFormat: 'string'/'toString'` output still depend on runtime date parsing/formatting; prefer `iso`, `milliseconds`, or `seconds` for portable output.
44
- - Verify JSON export/import both before and after computation, repeated reuse of one frozen saved config, nested-copy isolation, generated and explicit lags, callback resolution, constructor/output defaults, and calendar features across runtime timezones.
43
+ - Cross-runtime replay requires identical inputs and library versions. Use unambiguous input dates (valid Date objects, supported timestamps, or ISO strings with `Z`/offsets). Timezone-less date strings and local `dateFormat: 'string'/'toString'` output still depend on runtime date parsing/formatting; prefer `iso`, `milliseconds`, or `seconds` for portable output.
44
+ - Verify JSON export/import both before and after computation, repeated reuse of one frozen saved config, nested-copy isolation, generated and explicit lags, constructor/output defaults, and calendar features across runtime timezones.
45
45
 
46
46
  ## Indicator files
47
47
 
48
48
  ### Moving averages and channels
49
49
 
50
- - `src/moving-averages/movingAverages.js` — shared runtime for public `ema()` and `sma()`.
51
- - `src/moving-averages/bollingerBands.js` — Bollinger upper, middle, and lower bands.
52
- - `src/moving-averages/donchianChannel.js` — rolling Donchian upper, basis, and lower channels with optional offset.
50
+ - `src/moving-averages/movingAverages.js` — shared runtime for public `ema()` and `sma()`, optionally returning `ln(currentTarget / currentAverage)`. Raw/log variants have independent keys/state; only the selected output and its lags are allocated. Logged outputs are not price-based. Missing/nonpositive operands or non-finite/nonpositive ratios remain `NaN`; raw arithmetic and warm-up are unchanged.
51
+ - `src/moving-averages/bollingerBands.js` — Bollinger upper, middle, and lower bands, or only dimensionless logarithmic width and position with `retLogs: true`.
52
+ - `src/moving-averages/donchianChannel.js` — rolling Donchian upper, basis, and lower channels with optional offset, or only logarithmic width and position. Offset shifts bounds, never the current close used for position.
53
53
  - `src/moving-averages/heikenAshi.js` — optional pre/post-smoothed Heiken-Ashi structure returns and trend counter.
54
54
  - `src/moving-averages/macd.js` — MACD difference, signal/DEA, and histogram for a selectable target.
55
- - `src/moving-averages/relativeVolume.js` — current volume divided by the previous completed volume-SMA window.
55
+ - `src/moving-averages/relativeVolume.js` — current volume divided by the previous completed volume-SMA window, optionally its natural log. Zero-volume rows still skip state updates. Raw/log variants have independent keys/state and selected-output lags; neither is price-based. Old configs omitting `retLogs` retain raw behavior, as do EMA/SMA configs.
56
+
57
+ #### Channel log-feature guards
58
+
59
+ - Bollinger/Donchian `retLogs` defaults to `false`; omitted options in old configs preserve raw outputs, arithmetic, and warm-up. Raw/log variants at identical periods have independent runtime state and may coexist. Preserve selected mode in config export/replay.
60
+ - Log mode emits only `ret_log_bollinger_bands_width/position` or `ret_log_donchian_channel_width/position`. Append existing parameter suffixes when `useFullNames` is enabled or total registrations of the same method exceed one, counting both raw and logged modes. Generate lags only for the selected outputs; logged columns and lags must never be price-based.
61
+ - For finite positive ordered bounds, use `logRange = ln(upper / lower)`, `width = logRange / 2`, and `position = 2 * ln(currentClose / lower) / logRange - 1`. Require finite representable ratios/results. Position is geometrically centered and unclipped, not a temporal return. Do not allocate an absolute log-center column.
62
+ - Invalid/nonpositive bounds (possible for Bollinger even with valid prices) leave both outputs `NaN`; never clamp or silently switch transforms. Valid equal bounds produce width `0` and position `NaN`. Width depends only on bounds and must not be invalidated solely by an invalid current close. Invalid position operands/results stay `NaN`. Retain normal `skipNull` semantics, including an empty result if the final position is invalid.
56
63
 
57
64
  ### Oscillators and volume
58
65
 
59
66
  - `src/oscillators/rsi.js` — RSI plus an SMA of RSI, optionally expressed as log ratios to 50.
67
+ - `src/oscillators/mfi.js` — money flow index from HLC3 and volume, with a bounded signed-flow window, optional log ratio to 50, and generated lags. The first row seeds typical price; a period of N needs N + 1 rows. Unlike the three zero-skipping volume indicators, MFI counts zero-volume rows as zero flow. Equal-price rows contribute zero; a window with no directional flow stays `NaN`. Raw/log variants have independent runtime state and neither output is price-based.
60
68
  - `src/oscillators/stochastic.js` — stochastic K and D, optionally expressed as log ratios to 50.
61
69
  - `src/oscillators/volumeDelta.js` — signed per-bar volume and consecutive buy/sell direction counter.
62
70
  - `src/oscillators/volumeOscillator.js` — percentage difference and optional log ratio between fast and slow volume EMAs.
63
71
 
64
72
  ### Volatility
65
73
 
66
- - `src/volatility/atr.js` — ATR or its log ratio to close.
74
+ - `src/volatility/atr.js` — ATR or its log ratio to close; only raw ATR and its lags are price-based, never the dimensionless logged variant.
67
75
  - `src/volatility/adx.js` — ADX or its log ratio to the neutral reference value.
68
76
 
69
77
  ### Studies and feature utilities
@@ -72,7 +80,6 @@ Input rows normally contain `open`, `high`, `low`, `close`, and `volume`; `date`
72
80
  - `src/studies/dateTime.js` — numeric or one-hot calendar features.
73
81
  - `src/studies/findCrosses.js` — signed intervals since a cross, optional clipping, and optional one-hot encoding.
74
82
  - `src/studies/lag.js` — lag columns for existing vertical columns.
75
- - `src/studies/mapCols.js` — custom user-defined columns produced by a callback.
76
83
 
77
84
  ## Minimum required helpers
78
85
 
@@ -82,7 +89,7 @@ Input rows normally contain `open`, `high`, `low`, `close`, and `volume`; `date`
82
89
  - `src/core-functions/pushToMain.js` — column writes and per-row validity checks.
83
90
  - `src/utilities/assignTypes.js` — input type inference, typed-array allocation, and array-type lookup used by lagging.
84
91
  - `src/utilities/validators.js` — public API and queued-parameter validation.
85
- - `src/utilities/config.js` — strict configuration copying/freezing, schema version, named callback registration/resolution, and JSON-safe parameter export.
92
+ - `src/utilities/config.js` — strict configuration copying/freezing, schema version, and JSON-safe parameter export.
86
93
  - `src/utilities/verticalToHorizontal.js` — output materialization, invalid-prefix skipping, temporary-column filtering, and precision restoration.
87
94
  - `src/utilities/numberUtilities.js` — input/output numeric formatters, numeric classification, and volume-number predicates.
88
95
  - `src/utilities/precisionMultiplier.js` — fixed-point conversion for exact price-string processing when `precision: true`.
@@ -94,6 +101,7 @@ Input rows normally contain `open`, `high`, `low`, `close`, and `volume`; `date`
94
101
 
95
102
  - With `precision: false`, price inputs use normal floating-point values.
96
103
  - With `precision: true`, OHLC price strings are converted to fixed-point numbers using one shared multiplier. Price-based indicator outputs stay in those units internally and are restored only during output conversion.
104
+ - Cache numeric multiplier decimal metadata once per computation/output conversion, outside exported configuration. Preserve string parsing, truncation, signed-zero, and error behavior; do not replace conversion formulas merely to avoid allocations.
97
105
  - Volume is currently stored in an `Int32Array`; do not assume that integer typed arrays preserve `NaN`.
98
106
  - `Array` columns are used for objects such as dates and one-hot vectors; numeric indicator outputs normally use `Float64Array`.
99
107
 
@@ -101,5 +109,4 @@ Input rows normally contain `open`, `high`, `low`, `close`, and `volume`; `date`
101
109
 
102
110
  - Preserve unrelated working-tree changes; indicator files are frequently edited together.
103
111
  - Do not edit `dist/ohlcv-indicators.min.js` manually. Run `npm run build` after source changes that should ship in the browser bundle.
104
- - Use deterministic local OHLCV arrays to test warm-up boundaries, zero/invalid inputs, lagged columns, multiple configurations, `retLogs`, `precision: true`, and both `getData()` modes.
105
- - Run `git diff --check`, `npm test`, and `npm run build`. `test/test.js` runs deterministic local regression checks by default; `npm test -- --live` runs the optional integration demo against its configured market-data service.
112
+ - Use deterministic local OHLCV arrays to test warm-up boundaries, zero/invalid inputs, lagged columns, multiple configurations, `retLogs`, `precision: true`, and both `getData()` modes.
package/README.md CHANGED
@@ -15,7 +15,6 @@ This README describes the API in this repository. Keep the same library version
15
15
  - [Indicator reference](#indicator-reference)
16
16
  - [Advanced: combine indicators and lagged features](#advanced-combine-indicators-and-lagged-features)
17
17
  - [Save and reuse a configuration](#save-and-reuse-a-configuration)
18
- - [Advanced: custom columns with portable callbacks](#advanced-custom-columns-with-portable-callbacks)
19
18
  - [Precision mode](#precision-mode)
20
19
  - [Current limitations and troubleshooting](#current-limitations-and-troubleshooting)
21
20
  - [Notes for developers and LLMs](#notes-for-developers-and-llms)
@@ -169,39 +168,124 @@ Supported output date formats: `milliseconds`, `seconds`, `iso` / `toISOString`,
169
168
 
170
169
  All methods below return the instance for chaining. Periods and lookbacks are measured in rows, not minutes or days. Periods must be positive integers within the input length; allow extra history for warm-up. Avoid repeating configurations that produce the same output names.
171
170
 
172
- Defaults are shown below. `target` means the numeric source column; `lag: n` creates output copies from each of the previous `1` through `n` rows. `retLogs` means natural-log ratios, not application logging.
171
+ Defaults are shown below. `target` means the numeric source column; `lag: n` creates output copies from each of the previous `1` through `n` rows. `retLogs` selects natural-log-based features, not application logging; see each indicator's formula below.
173
172
 
174
173
  | Method | Options and behavior |
175
174
  | --- | --- |
176
- | `ema(size = 5, options = {})` | Exponential moving average. `{ target: 'close', lag: 0 }`. |
177
- | `sma(size = 5, options = {})` | Simple moving average. `{ target: 'close', lag: 0 }`. |
178
- | `bollingerBands(size = 20, stdDev = 2, options = {})` | Upper, middle, and lower close-price bands. `{ lag: 0 }`. |
179
- | `donchianChannels(size = 20, offset = 0, options = {})` | Rolling high/low channel and midpoint. `{ lag: 0 }`; positive offset shifts the window into the past. |
175
+ | `ema(size = 5, options = {})` | Exponential moving average, or `ln(target / EMA)` when logged. `{ target: 'close', lag: 0, retLogs: false }`. |
176
+ | `sma(size = 5, options = {})` | Simple moving average, or `ln(target / SMA)` when logged. `{ target: 'close', lag: 0, retLogs: false }`. |
177
+ | `bollingerBands(size = 20, stdDev = 2, options = {})` | Upper, middle, and lower close-price bands, or logarithmic width and position. `{ lag: 0, retLogs: false }`. |
178
+ | `donchianChannels(size = 20, offset = 0, options = {})` | Rolling high/low channel and midpoint, or logarithmic width and position. `{ lag: 0, retLogs: false }`; positive offset shifts the channel window into the past, not the close used for position. |
180
179
  | `macd(fast = 12, slow = 26, signal = 9, options = {})` | Difference, signal (`dea`), and histogram. `{ target: 'close', lag: 0 }`. |
181
180
  | `rsi(size = 14, options = {})` | RSI and a same-period SMA of RSI. `{ target: 'close', lag: 0, retLogs: false }`. |
181
+ | `mfi(size = 14, options = {})` | Money flow index using high, low, close, and volume. `{ lag: 0, retLogs: false }`; logged output uses `mathLog(MFI, 50)`. |
182
182
  | `stochastic(kPeriod = 14, kSlowingPeriod = 3, dPeriod = 3, options = {})` | K and D outputs. `{ lag: 0, retLogs }`; explicitly supply `retLogs: false` or `true` in this version. |
183
183
  | `atr(size = 14, options = {})` | Average true range. `{ lag: 0, retLogs: false }`. |
184
184
  | `adx(size = 14, options = {})` | Average directional index. `{ lag: 0, retLogs: false }`. |
185
185
  | `heikenAshi(smoothLength = null, afterSmoothLength = null, options = {})` | Heiken-Ashi body/wick/range returns and direction counter, not raw HA OHLC columns. `{ lag: 0, retLogs: false }`; use either two `null`s or two positive smoothing periods. |
186
- | `relativeVolume(size = 10, options = {})` | Current volume / previous completed volume-SMA window, excluding the current sample from the denominator. `{ lag: 0 }`. |
186
+ | `relativeVolume(size = 10, options = {})` | Current volume / previous completed volume-SMA window, or its natural log, excluding the current sample from the denominator. `{ lag: 0, retLogs: false }`. |
187
187
  | `volumeDelta(options = {})` | Candle-direction-based signed volume, delta high/low/close, and direction counter; not trade-by-trade bid/ask delta. `{ lag: 0 }`. |
188
- | `volumeOscillator(fast = 5, slow = 10, options = {})` | `100 * (fastVolumeEMA - slowVolumeEMA) / slowVolumeEMA`. `{ lag: 0, retLogs: false }`. |
188
+ | `volumeOscillator(fast = 5, slow = 10, options = {})` | One output: percentage difference, or `ln(fastVolumeEMA / slowVolumeEMA)` when logged. `{ lag: 0, retLogs: false }`. |
189
189
  | `candleFeatures(options = {})` | Change, gap, body, wick, and range returns. `{ lag: 0, colKeys: [], retLogs: false }`. Extra `colKeys` compare current close to each selected price column, not that column's previous value. |
190
190
  | `dateTime(options = {})` | Calendar features from `date`. `{ lag: 0, oneHot: false }`. |
191
191
  | `crossPairs(pairs = [], options = {})` | Signed direction/run counters for `{ fast, slow }` pairs. `{ limit: null, oneHot: false }`; `slow` can be a column name or numeric constant. Call once with all pairs. |
192
- | `mapCols(newCols = ['change'], callback = null, options = {})` | Custom columns. `{ lag: 0, isPriceBased: false, callbackParams: {} }`. See the callback example below. |
193
192
  | `lag(colKeys = ['close'], lookback = 1)` | Copies existing columns into `<column>_lag_1`, ..., `<column>_lag_<lookback>`. |
194
193
 
195
194
  ### Output names and units
196
195
 
197
- Examples of names: `ema_5`, `sma_5_open`, `rsi_14`, `rsi_sma_14`, `atr_14`, `relative_volume_10`, and `volume_oscillator_5_10`.
196
+ Examples of names: `ema_5`, `sma_5_open`, `rsi_14`, `rsi_sma_14`, `mfi_14`, `atr_14`, `relative_volume_10`, and `volume_oscillator_5_10`.
198
197
 
199
198
  A single Bollinger configuration normally uses `bollinger_bands_upper`, `bollinger_bands_middle`, and `bollinger_bands_lower`. With `useFullNames: true`, `.bollingerBands(20, 2)` adds `_20_2`. MACD and Donchian also expand their names when multiple configurations are registered. Use full names for persistent downstream schemas, and inspect `Object.keys(instance.getLastValues())` rather than guessing names.
200
199
 
201
- For `retLogs: true`, RSI and stochastic use `ln(value / 50)`, ADX uses `ln(value / 20)`, and ATR uses `ln(ATR / close)`. Their logged columns replace the corresponding raw outputs. Volume oscillator is different: it keeps its percentage column and **adds** `ret_log_volume_oscillator_<fast>_<slow>` containing `ln(fastEMA / slowEMA)`.
200
+ For `retLogs: true`, RSI, MFI, and stochastic use `ln(value / 50)`, ADX uses `ln(value / 20)`, and ATR uses `ln(ATR / close)`. Their logged columns replace the corresponding raw outputs.
201
+
202
+ EMA and SMA use `ln(currentTarget / currentAverage)`; relative volume uses `ln(currentVolume / previousCompletedSMA)`. These are log-relative features, not changes from the previous row. Their names are `ret_log_ema_<size>`, `ret_log_sma_<size>`, and `ret_log_relative_volume_<size>`; non-close moving-average targets append `_<target>`, for example `ret_log_sma_3_open`. A value of zero means the source equals its reference. Raw mode remains the default, including replay of older configs that omit `retLogs`.
203
+
204
+ Each of these three methods emits only the selected raw or logged column, with generated lags only for that column. Register both modes to obtain both outputs; raw/log instances have independent state. Logs and their lags are dimensionless numbers even with `precision: true`. Logged ATR and its lags are also dimensionless and are no longer incorrectly rescaled as prices in precision mode; replaying older configs with this combination now produces the corrected output.
205
+
206
+ ```js
207
+ const logFeatures = new OHLCV_INDICATORS({ input })
208
+ .ema(3, { retLogs: true, lag: 1 })
209
+ .sma(3, { target: 'open', retLogs: true })
210
+ .relativeVolume(3, { retLogs: true });
211
+
212
+ const savedLogConfig = JSON.parse(JSON.stringify(logFeatures.exportConfig()));
213
+ const replayedLogFeatures = new OHLCV_INDICATORS({ input, config: savedLogConfig });
214
+ console.log(replayedLogFeatures.getLastValues().ret_log_ema_3);
215
+ ```
216
+
217
+ The EMA/SMA/relative-volume log modes leave unavailable or invalid ratios as `NaN`: operands must be positive and the computed ratio finite and greater than zero. They do not apply epsilon substitution to zero targets. Their underlying averages still update exactly as in raw mode; relative volume still skips zero-volume rows without updating its state. MACD and volume delta have no `retLogs` option.
218
+
219
+ `volumeOscillator()` also emits only the selected output:
220
+
221
+ - `retLogs: false` creates `volume_oscillator_<fast>_<slow>` containing `100 * (fastEMA - slowEMA) / slowEMA`.
222
+ - `retLogs: true` creates `ret_log_volume_oscillator_<fast>_<slow>` containing `mathLog(fastEMA, slowEMA)`; it does **not** create the percentage column.
223
+
224
+ Its `lag` option generates lags only for that selected column. To obtain both forms, register both modes before computation, for example `.volumeOscillator(5, 10, { lag: 2 }).volumeOscillator(5, 10, { retLogs: true, lag: 2 })`. Each mode has independent EMA state; both outputs and their lags are dimensionless.
225
+
226
+ Older saved configs using `retLogs: true` may expect the percentage column that was previously emitted too. If an explicit lag or downstream consumer still needs that percentage column, add a raw-mode registration before the consumer. Change a reference to the logged column only when you intend to consume log ratios instead of percentages. Current configs preserve the selected mode on export/replay.
202
227
 
203
228
  Non-log candle and Heiken-Ashi returns are fractions, not percentages: `0.01` means 1%. Unavailable indicator values can remain `NaN`. The shared `mathLog()` helper substitutes `0.001` for a zero numerator or denominator and throws for incompatible signs or a non-finite result; its zero handling is not literal `ln(0)`. Candle and Heiken-Ashi features have their own log calculations.
204
229
 
230
+ ### Bollinger and Donchian: logarithmic channel features
231
+
232
+ With `{ retLogs: true }`, these methods replace their three raw price columns with two dimensionless features. Given upper bound `U`, lower bound `L`, and the current candle's close `C`:
233
+
234
+ ```text
235
+ logRange = ln(U / L)
236
+ width = logRange / 2
237
+ position = 2 * ln(C / L) / logRange - 1
238
+ ```
239
+
240
+ `position` is `-1` at the lower bound, `+1` at the upper bound, and `0` at their geometric midpoint, `sqrt(U * L)`. Breakouts remain below `-1` or above `+1`; values are not clipped. These describe the current channel, not returns from the previous candle. Donchian's `offset` shifts only the bounds: position always compares the **current close** to that historical channel.
241
+
242
+ The compact output names are `ret_log_bollinger_bands_width`, `ret_log_bollinger_bands_position`, `ret_log_donchian_channel_width`, and `ret_log_donchian_channel_position`. With `useFullNames: true` or more than one registration of the same method (counting both modes), they append `_<size>_<stdDev>` for Bollinger or `_<size>_<offset>` for Donchian. Prefer full names for a stable downstream schema.
243
+
244
+ This example reuses the five quick-start candles and includes saved-config replay:
245
+
246
+ ```js
247
+ const channelFeatures = new OHLCV_INDICATORS({
248
+ input,
249
+ config: { useFullNames: true },
250
+ })
251
+ .bollingerBands(3, 2, { retLogs: true, lag: 1 })
252
+ .donchianChannels(3, 1, { retLogs: true, lag: 1 });
253
+
254
+ const savedChannelConfig = JSON.parse(JSON.stringify(channelFeatures.exportConfig()));
255
+ const replayedChannels = new OHLCV_INDICATORS({ input, config: savedChannelConfig });
256
+ const lastChannel = replayedChannels.getLastValues();
257
+ console.log(lastChannel.ret_log_bollinger_bands_width_3_2);
258
+ console.log(lastChannel.ret_log_donchian_channel_position_3_1);
259
+ console.log(lastChannel.ret_log_donchian_channel_position_3_1_lag_1);
260
+ ```
261
+
262
+ Both bounds must be finite, positive, ordered (`U >= L`), and have a finite representable ratio. Invalid bounds leave both features `NaN`. In particular, Bollinger's lower band can be zero or negative despite valid positive input prices; it is not clamped and there is no fallback transform. Valid equal bounds produce width `0`, but position remains `NaN` because it is undefined. Width depends only on the bounds; an invalid close or close/bound ratio leaves position `NaN` without discarding an otherwise valid width.
263
+
264
+ Warm-up and raw calculations are unchanged. The default remains `{ retLogs: false }`, including replay of older configs that omit the option. To obtain raw bands as well, register the same method again in raw mode before computation; raw/log variants maintain independent state. Each registration generates lags only for its selected outputs. Logged columns and their lags are not price-based and remain dimensionless in precision mode. No absolute log-center column is allocated: two numeric outputs instead of three save approximately `8 * input.length * (1 + lag)` bytes per registration.
265
+
266
+ A flat or otherwise invalid final channel can make default `getData()` return `[]`, because `skipNull` removes the prefix through the last invalid row. Use `getData({ skipNull: false })` to inspect these rows and their `NaN` positions.
267
+
268
+ ### Money flow index: raw, logged, and lagged
269
+
270
+ MFI weights typical price `(high + low + close) / 3` by volume, then compares positive and negative flow over the selected period. Rising typical prices contribute positive flow, falling prices negative flow, and unchanged prices zero. The result is `100 - 100 / (1 + positiveFlow / negativeFlow)`. See the [MFI calculation reference](https://www.tradingview.com/support/solutions/43000502348-money-flow-mfi/).
271
+
272
+ This example reuses the five quick-start candles, so it uses a short period:
273
+
274
+ ```js
275
+ const moneyFlow = new OHLCV_INDICATORS({ input })
276
+ .mfi(3, { lag: 1 })
277
+ .mfi(3, { lag: 1, retLogs: true });
278
+
279
+ const lastMoneyFlow = moneyFlow.getLastValues();
280
+ console.log(lastMoneyFlow.mfi_3); // Raw index, between 0 and 100
281
+ console.log(lastMoneyFlow.ret_log_mfi_3); // 50 maps to 0; below 50 is negative
282
+ console.log(lastMoneyFlow.mfi_3_lag_1); // Previous candle's MFI
283
+ ```
284
+
285
+ For ordinary use, call `.mfi()` for period 14, or `.mfi(14, { lag: 2, retLogs: true })` for logged output and two lags. Logged output is named `ret_log_mfi_14`; raw and logged variants can coexist at the same period without sharing state. Both are dimensionless, including their lags, even with `precision: true`.
286
+
287
+ MFI needs one starting candle plus `size` flow observations: period 14 first becomes available at index 14 (the fifteenth row). Zero-volume candles contribute zero **and count toward this window**; unlike `relativeVolume`, `volumeDelta`, and `volumeOscillator`, MFI does not skip them. A positive-only window returns 100, a negative-only window returns 0, and a window with no directional flow returns `NaN`. Logged zero uses the existing `mathLog` epsilon behavior. Invalid flow/comparisons leave output `NaN` until they leave the window. Storage is bounded by the period; summation is chronological to avoid rolling-subtraction drift.
288
+
205
289
  ## Advanced: combine indicators and lagged features
206
290
 
207
291
  This runnable example generates deterministic demonstration candles. Replace them with your own chronological dataset in a real application.
@@ -227,6 +311,7 @@ const features = new OHLCV_INDICATORS({
227
311
  .ema(5, { lag: 2 })
228
312
  .ema(20)
229
313
  .rsi(14)
314
+ .mfi(14, { lag: 2, retLogs: true })
230
315
  .macd(12, 26, 9)
231
316
  .bollingerBands(20, 2)
232
317
  .donchianChannels(20, 1)
@@ -248,7 +333,7 @@ console.log(lastFeature.macd_histogram_12_26_9);
248
333
  console.log(lastFeature.one_hot_hour); // Uint8Array of length 24
249
334
  ```
250
335
 
251
- Register producers before consumers: for example, create a custom column before asking `lag()` to read it. Do not create the same lag twice using both an indicator's `lag` option and an explicit `.lag()` call.
336
+ Register producers before consumers: for example, register `.ema(5)` before asking `.lag(['ema_5'], 1)` to read its output. Do not create the same lag twice using both an indicator's `lag` option and an explicit `.lag()` call.
252
337
 
253
338
  A lag always refers to an earlier row, not an earlier nonzero-volume row. Features on the current candle can depend on its final high, low, close, and volume; account for candle completion when using them in a backtest.
254
339
 
@@ -298,56 +383,13 @@ Important replay rules:
298
383
 
299
384
  - Passing `config.inputParams` as an array computes immediately, even if it is empty. Do **not** chain more indicators onto a restored instance.
300
385
  - Preserve the whole exported config; saving only `inputParams` loses settings that affect the result.
301
- - For identical results, reuse identical ordered input, the same library version, the same callback implementations, and unambiguous dates. The config schema version alone does not pin library behavior.
386
+ - For identical results, reuse identical ordered input, the same library version, and unambiguous dates. The config schema version alone does not pin library behavior.
302
387
  - Exported objects are independent copies. Runtime-generated lag jobs are not added to the saved declarations; replay regenerates them from their options.
303
- - Custom callbacks must be registered in each process before importing their configs. Function source and closures are not serialized.
304
- - Configuration values must be JSON-safe. Functions without registered names, `undefined`, `NaN`, `Infinity`, circular references, and non-plain objects cannot be exported as configuration.
388
+ - Configuration values must be JSON-safe. Functions, `undefined`, `NaN`, `Infinity`, circular references, and non-plain objects are not supported as configuration.
389
+ - `mapCols()` and `registerMapCallback()` have been removed. Older configurations containing `mapCols` are rejected as unknown indicators; they cannot be replayed unchanged in this version.
305
390
 
306
391
  JSON safety applies to exported **configuration**, not arbitrary result rows. Serializing results converts `NaN` to `null`, and typed-array vectors are not serialized as ordinary arrays automatically.
307
392
 
308
- ## Advanced: custom columns with portable callbacks
309
-
310
- `mapCols()` runs a callback once per input row. It receives `{ index, main, params }`; input and previously calculated columns are available at `main.verticalOhlcv[column][index]`.
311
-
312
- Register a versioned callback name so that its configuration can be saved and replayed. This example reuses `input` from the quick start and the `assert` import from the save-and-reuse example:
313
-
314
- ```js
315
- const bodyPercent = ({ index, main, params }) => {
316
- const { open, close } = main.verticalOhlcv;
317
- return {
318
- body_percent: ((close[index] - open[index]) / open[index]) * params.scale,
319
- };
320
- };
321
-
322
- OHLCV_INDICATORS.registerMapCallback('body-percent.v1', bodyPercent);
323
-
324
- const custom = new OHLCV_INDICATORS({
325
- input,
326
- config: { timeZone: 'UTC', dateFormat: 'iso' },
327
- }).mapCols(['body_percent'], 'body-percent.v1', {
328
- callbackParams: { scale: 100 },
329
- lag: 1,
330
- });
331
-
332
- const customConfig = JSON.parse(JSON.stringify(custom.exportConfig()));
333
- const customRows = custom.getData();
334
- const customReplay = new OHLCV_INDICATORS({ input, config: customConfig });
335
- assert.deepStrictEqual(customReplay.getData(), customRows);
336
- console.log(customReplay.getLastValues().body_percent);
337
- ```
338
-
339
- In a new process, define and register `body-percent.v1` before constructing `customReplay`. Registration is runtime-local. Do not replace an existing callback name with different code; register `body-percent.v2` instead.
340
-
341
- Callback guidelines:
342
-
343
- - Declare every output name in `newCols`, return only those keys, and do not overwrite existing columns.
344
- - Return `null` or `undefined` to leave that row's custom outputs unavailable.
345
- - Put configurable values in JSON-safe `callbackParams`. Do not depend on changing closure state, randomness, clocks, or external services if you need reproducible replay.
346
- - Treat `main` as read-only apart from returning your declared values. Do not mutate input, internal queues, configuration, or other columns.
347
- - With precision mode, price columns in `verticalOhlcv` are scaled internal values. Ratios such as the example above cancel that scale. Use `isPriceBased: true` only for custom outputs that remain in scaled price units; that option requires `precision: true`.
348
-
349
- You may pass an unregistered function directly for local use, but `exportConfig()` will throw until it has a registered name. The built-in callback name `default` is reserved. With no callback, `.mapCols()` creates `change = 100 * (close[i] - open[i - 1]) / open[i - 1]`, not the usual close-to-close return; use an explicit callback when you want another formula.
350
-
351
393
  ## Precision mode
352
394
 
353
395
  Use numeric strings for all OHLC prices when enabling precision:
@@ -376,10 +418,9 @@ Prices are scaled internally using a shared multiplier derived from the first ro
376
418
  - **“Already computed” error:** register all methods before calling any getter or `compute()`. Imported configs already computed during construction.
377
419
  - **Empty output:** inspect `getData({ skipNull: false })`. You may need more history, or a late missing/zero-volume row may have moved the `skipNull` cutoff.
378
420
  - **Stochastic options error:** explicitly call, for example, `.stochastic(14, 3, 3, { retLogs: false })`; the current implementation does not supply that boolean's default.
379
- - **A derived target fails at row zero:** source validation requires positive finite values at the first row for non-volume targets. This restricts using warming-up outputs (such as an EMA) or signed/zero-valued custom columns as inputs to another indicator or `crossPairs()`. Registering the producer first does not bypass that validation.
421
+ - **A derived target fails at row zero:** source validation requires positive finite values at the first row for non-volume targets. This restricts using warming-up outputs (such as an EMA) or signed/zero-valued extra input columns as inputs to another indicator or `crossPairs()`. Registering the producer first does not bypass that validation.
380
422
  - **Date errors:** use `Date` objects or timezone-qualified ISO strings consistently. Numeric millisecond input currently has a formatter-name mismatch; wrap it with `new Date(timestampMs)` first. Millisecond *output* is supported. Local/timezone-less strings can parse differently across runtimes.
381
423
  - **Unexpected volume:** storage is signed 32-bit integer. Normalize data externally; larger counts and fractional volumes are not preserved.
382
- - **Logged ATR with precision:** the current handler marks even logged ATR as price-based, so `precision: true` rescales that dimensionless output incorrectly. Avoid that combination until corrected; use `precision: false` for logged ATR.
383
424
  - **MACD on a non-price target:** its outputs are always marked price-based. With `precision: true`, targets such as volume or a custom ratio are therefore rescaled incorrectly; use `precision: false` for those targets.
384
425
  - **Comparing raw and logged variants:** use separate instances for ADX, stochastic, or Heiken-Ashi at identical periods/smoothing settings. Their raw/log variants can share internal state despite having different output names.
385
426
  - **Cross one-hot output:** provide a finite integer `limit` of at least 2 (within the input length) when `oneHot: true` so the vector has a meaningful fixed size.
@@ -391,25 +432,10 @@ These are descriptions of the current implementation, not promises that invalid
391
432
  Use this section as a compact implementation contract, alongside [AGENTS.md](./AGENTS.md).
392
433
 
393
434
  - Public entry point: default ESM export `OHLCV_INDICATORS` from `index.js`. Constructor shape: `{ input, ticker?, chunkProcess?, config? }`. There are no public `scaler()` or `vidya()` methods.
394
- - Registration is separate from execution. `inputParams` contains immutable declarations; `executionParams` is the mutable runtime queue. Never serialize runtime instances, buffers, generated jobs, or resolved callback functions.
435
+ - Registration is separate from execution. `inputParams` contains immutable declarations; `executionParams` is the mutable runtime queue. Never serialize runtime instances, buffers, or generated jobs. All configuration parameters must be JSON-safe.
395
436
  - Handlers execute chronologically, allocate columns on `index === 0`, and keep rolling state in `main.instances`. The runtime is column-oriented even though input and returned data are row-oriented.
396
437
  - `initializeColumns()` owns output allocation, naming collision checks, price metadata, and generated lags. Numeric outputs normally use `Float64Array` with `NaN`; object/one-hot columns use arrays. Volume input uses `Int32Array`.
397
- - `src/core-indicators/` contains the local numeric indicator classes used at runtime. `trading-signals` is retained as a regression reference, not a production indicator import. Preserve warm-up, arithmetic order, and intentional zero/NaN behavior when changing these classes.
438
+ - `src/core-indicators/` contains the local numeric indicator classes used at runtime, with no external indicator dependency. Self-contained tests cover known results and fixed behavior traces. Preserve warm-up, arithmetic order, and intentional zero/NaN behavior when changing these classes.
398
439
  - Full output storage grows with both row count and column count. Each `Float64Array` column uses approximately `8 * input.length` bytes, excluding other overhead. Lags add full columns; `getData()` allocates row objects on each call. Request only needed features, or use `getLastValues()` when you only need the final row.
399
440
  - `chunkProcess` does not turn the API into asynchronous, streaming, or bounded-memory processing. Create a new instance for each independent run and retain the input separately if you need replay.
400
- - Read the implementation for exact output naming and formulas; do not assume every third-party indicator convention or parameter combination is supported.
401
-
402
- ### Development checks
403
-
404
- From a checkout with dependencies installed:
405
-
406
- ```sh
407
- npm test
408
- npm test -- --benchmark --rows 20000 --repeats 9
409
- npm run build
410
- git diff --check
411
- ```
412
-
413
- `test/test.js` runs deterministic local regressions by default, including indicator behavior, configuration replay, and core-class comparisons. `npm test -- --live` is an optional integration demo requiring its configured market-data service. Build the browser bundle with `npm run build`; do not edit the minified file manually.
414
-
415
- The package declares the ISC license. Adapted core indicator code retains its upstream MIT notice; preserve that notice when redistributing it and the generated bundle's accompanying license file.
441
+ - Read the implementation for exact output naming and formulas; do not assume every third-party indicator convention or parameter combination is supported.