@pond-ts/process 0.64.0 → 0.66.0

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.
Files changed (3) hide show
  1. package/API.md +181 -13
  2. package/CHANGELOG.md +1113 -1
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  under a single `v*` tag, so this file covers them all. Pre-1.0: minor bumps may
9
9
  include new features and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pond-ts/pond/compare/v0.64.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.66.0...HEAD
12
+ [0.66.0]: https://github.com/pond-ts/pond/compare/v0.65.0...v0.66.0
13
+ [0.65.0]: https://github.com/pond-ts/pond/compare/v0.64.0...v0.65.0
12
14
  [0.64.0]: https://github.com/pond-ts/pond/compare/v0.63.0...v0.64.0
13
15
  [0.63.0]: https://github.com/pond-ts/pond/compare/v0.62.0...v0.63.0
14
16
  [0.62.0]: https://github.com/pond-ts/pond/compare/v0.61.0...v0.62.0
@@ -67,6 +69,1116 @@ include new features and type-level changes; patch bumps are strictly additive.
67
69
 
68
70
  ## [Unreleased]
69
71
 
72
+ ## [0.66.0] — 2026-09-07
73
+
74
+ ### Added
75
+
76
+ - `@pond-ts/financial`: **the volume and miscellaneous leftovers** (corpus
77
+ §6.6 / §6.4 / §6.1) — six studies in the uniform shape (bar columns plus an
78
+ `output` or `prefix`, bar-count periods, a length-preserving per-column
79
+ warm-up, a fluent method), each with oracle cases. Two internal kernel
80
+ helpers ride with them: `trueRangeBoundsValues` (the true high / true low
81
+ the true range is the width of) and `moneyFlowVolumeValues` (the `clv ·
82
+ volume` term the A/D line already accumulated, named so Twiggs can share
83
+ it).
84
+ - **`twiggsMoneyFlow({ period = 21, high, low, close, volume, output =
85
+ 'tmf' })`** — Colin Twiggs' correction to Chaikin Money Flow: the close's
86
+ location in the bar's **true** range (so a gap bar is scored against the
87
+ ground it covered), Wilder-smoothed against a matching smoothing of
88
+ volume rather than summed over a flat window. Bounded −1…1. **F-AMBIG**
89
+ on the smoothing: Wilder's exponential form ships (Incredible Charts /
90
+ Twiggs' own published algorithm) and the window-sum fork is measured at
91
+ **0.0706** away at `period 21`, where TMF itself spans −0.0279…0.1493;
92
+ `chaikinMoneyFlow(21)` sits **0.1494** away — wider than the whole
93
+ reading. Warm-up is `period` (bar 0 has no true range), and an interior
94
+ gap in any input **ends** the reading, where CMF recovers a window later.
95
+ - **`tradeVolumeIndex({ minTick, column = 'close', volume, output = 'tvi'
96
+ })`** — tick-direction volume accumulation: `+1` above the dead band, `−1`
97
+ below it, and **the previous direction** on an undecided bar, which is
98
+ what the exchange tick rule does and what distinguishes it from `obv`
99
+ (which adds nothing for an unchanged close). **`minTick` is required**
100
+ and has no default — a fact about the instrument, like `swingIndex`'s
101
+ `limit`; `0`, negative and non-finite are rejected. The level starts at
102
+ `0`, no first direction is invented (the up-seeded vendor fork is pinned
103
+ by a unit test — the oracle input's first bar carries its largest move,
104
+ so the fork is invisible there, and the generator asserts that), and an
105
+ interior gap **ends** the index the way `obv` does rather than re-seeding
106
+ the way `negativeVolumeIndex` does.
107
+ - **`shinoharaIntensityRatio({ period = 26, open, high, low, close, prefix =
108
+ 'sir' })`** → `sirStrong`, `sirWeak` — Shinohara's A and B ratios,
109
+ `100·Σup/Σdown` over the window: A measured against each bar's **own
110
+ open**, B against the **previous close**. **F-AMBIG** on the A/B naming:
111
+ the arithmetic is the standard pair and the `strong` / `weak` labels come
112
+ from the corpus' own list; the alternative convention charts them the
113
+ other way round, and the two lines sit **23,312.47** apart on the oracle
114
+ input, so the labels carry information. Per-column warm-up (25 and 26 at
115
+ the default). Neither ratio is bounded, and **B inverts on a gappy tape**
116
+ — `prevClose − low` is negative on a bar that gapped up, which is the
117
+ definition, not a defect (documented, with the oracle input's own
118
+ −22,761…7,620 range as the worked case).
119
+ - **`elderImpulse({ emaPeriod = 13, fastPeriod = 12, slowPeriod = 26,
120
+ signalPeriod = 9, column = 'close', output = 'impulse' })`** — Elder's
121
+ Impulse System: `+1` when the 13-bar EMA **and** the MACD histogram both
122
+ rise, `−1` when both fall, `0` otherwise. The column is **numeric**, not
123
+ the categorical colour the corpus describes — `TimeSeries.withColumn` has
124
+ no string door, so a study cannot append one; `+1 / 0 / −1` is signed, so
125
+ a chart maps it to Elder's green/blue/red in one expression. It calls
126
+ `macd()` under a scratch prefix rather than re-deriving the histogram, so
127
+ it is by construction the same MACD a caller charts beside it. Ties are
128
+ `0` (strict comparison on both sides), so it carries no state and needs
129
+ no gap rule of its own; warm-up 34 at the defaults.
130
+ - **`movingAverageCross({ fastPeriod = 10, slowPeriod = 30, maType = 'sma',
131
+ column = 'close', output = 'maCross' })`** — a **signal** column: `+1` on
132
+ the bar the fast average crosses above the slow one, `−1` below, `0`
133
+ otherwise. The averages are not emitted (that is `movingAverage`'s job);
134
+ what this adds is the event, which needs memory of which side the pair
135
+ was last on — a K6 `foldRows` machine. **The tie rule is the design**: an
136
+ exact tie is not a cross, a touch-and-retreat is not a cross (the machine
137
+ carries the last **non-zero** sign), and a crossing _through_ a tie fires
138
+ on the far-side bar. `maType`, not `type`, because the column is a signal
139
+ rather than an average. The first bar on which both averages exist
140
+ reports `undefined`, not `0` — it is the seed. **What a gap costs depends
141
+ entirely on `maType`**, and the docstring carries the measured table: at
142
+ the default `sma` a hole costs _nothing_ (the K2 column door counts rows),
143
+ at `ema` two bars, at `wma`/`hull` a window, and at `smma`/`kama` the rest
144
+ of the series.
145
+ - **`anchoredVwap({ anchor, high, low, close, volume, output = 'avwap' })`**
146
+ — the cumulative `Σ typicalPrice·volume / Σ volume` from a chosen bar
147
+ onwards: the VWAP of the execution desk, and the half of VWAP that
148
+ `vwap()`'s docstring named and left open. **`anchor` is required** and is
149
+ a `Date` or epoch **milliseconds**, not a row index — the line starts at
150
+ the first bar at or after it, so an anchor between bars snaps forward, one
151
+ before the series covers all of it, and one after the last bar leaves the
152
+ column empty (not an error). The **session-reset** form still waits on
153
+ [PND-TCAL]; what makes this one shippable today is that its anchor is a
154
+ user parameter, so no calendar is consulted. An interior gap **ends** the
155
+ line (`obv`'s cumulative rule), and the volume sum is blanked wherever
156
+ the price sum is so the two consume the same bars.
157
+
158
+ - `@pond-ts/financial`: **the momentum and trend leftovers** (corpus §6.3 /
159
+ §6.4 / §6.1) — ten studies in the uniform shape (a `column` or the bar
160
+ columns plus an `output` or `prefix`, bar-count periods, a length-preserving
161
+ per-column warm-up, a fluent method), each with oracle cases. Two internal
162
+ kernels ride with them: `swingIndexValues` (shared by the swing pair) and
163
+ `randomWalkValues` (the multi-horizon sweep the corpus flags as **G2**).
164
+ - **`stochasticMomentumIndex({ period = 13, longPeriod = 25, shortPeriod =
165
+ 2, signalPeriod = 3, high, low, close, prefix = 'smi' })`** → `smi`,
166
+ `smiSignal` — William Blau's SMI: the close against the **midpoint** of
167
+ the `period`-bar range, with the distance and the half-range each
168
+ double-EMA smoothed before the division, ×100. Bounded −100…100 and
169
+ signed, where the classic stochastic reads 50 at the midpoint. Option
170
+ names are `trueStrengthIndex`'s (Blau's other double-smoothed
171
+ study, same author, same `r`/`s`). Blau's `(13, 25, 2)` ship; the short
172
+ `(5, 3, 3)` fork sits **108.99** away and the unsmoothed reading
173
+ **126.95**, on a line spanning −48.19…74.33 (measured). A flat range for
174
+ the whole smoothed history reads `undefined` — the guard is live via a
175
+ redirected `close`.
176
+ - **`fisherTransform({ period = 10, high, low, prefix = 'fisher' })`** →
177
+ `fisher`, `fisherSignal` — John Ehlers' Fisher Transform (TASC Nov 2002),
178
+ a `foldRows` state machine. The range is over the **median price's** own
179
+ extremes, not over the bars' highs and lows: that port sits **5.95** away
180
+ on a line spanning −4.69…7.60 (measured). `0.33/0.67`, the `±0.99` clamp
181
+ test with its `±0.999` replacement, and the `0.5/0.5` second smoothing are
182
+ Ehlers' constants, not options. The signal is the line **delayed one
183
+ bar**. A gap — or a flat window — resets the machine ([PND-SFOLD]).
184
+ - **`schaffTrendCycle({ fastPeriod = 23, slowPeriod = 50, cyclePeriod = 10,
185
+ column, output = 'stc' })`** — Doug Schaff's STC: a stochastic of the MACD,
186
+ `0.5`-smoothed, then a stochastic of that, `0.5`-smoothed again. Bounded
187
+ 0…100; the raw double stochastic is **98.30** away (measured). Both
188
+ recursions are `foldRows` steps, so a gap resets them rather than
189
+ repeating the previous reading as the common port does — and that is
190
+ visible: a sustained trend pins the first stochastic, leaving the second
191
+ window flat and the line `undefined`, which on the package's own oracle
192
+ input delays the start from bar 67 to bar **74**.
193
+ - **`prettyGoodOscillator({ period = 14, column, high, low, close, output =
194
+ 'pgo' })`** — Mark Johnson's PGO, `(close − SMA)/EMA(TR)`, in units of
195
+ average daily ranges. **F-AMBIG**: Johnson's span-EMA denominator ships
196
+ and the Wilder-`ATR` port is named and measured (**0.211** apart at
197
+ period 14 on a line spanning −3.71…4.23). `column` defaults to whatever
198
+ `close` resolves to, as `atrBands` does.
199
+ - **`swingIndex({ limit, open, high, low, close, output = 'si' })`** and
200
+ **`accumulativeSwingIndex({ …, output = 'asi' })`** — Wilder's 1978 swing
201
+ index and its running total. **`limit` (Wilder's `T`, the instrument's
202
+ limit move) is required** and has no default: it is a fact about the
203
+ instrument, every possible default silently rescales the reading, and the
204
+ ±100 bound the study is defined by depends on it. A `limit` of 0 (or
205
+ negative, or non-finite) is rejected. `R = 0` — a tape frozen for two
206
+ bars — reads `undefined`, which for the ASI ends the level, the same rule
207
+ OBV follows.
208
+ - **`randomWalkIndex({ period = 14, high, low, close, prefix = 'rwi' })`** →
209
+ `rwiHigh`, `rwiLow` — E. Michael Poulos' RWI: the **maximum over horizons
210
+ `2 … period`** of `(high − low[−n]) / (meanTR(n)·√n)`. The denominator is
211
+ the `n`-bar **mean** true range, not Wilder's ATR (that variant is
212
+ **0.190** away at period 14; the single-horizon form **2.374**, measured).
213
+ Deliberately **O(N·period)** — the G2 shape no window kernel expresses —
214
+ with O(N) memory; 328 ms at `period 14` and 1194 ms at `period 50` on 1M
215
+ bars. Both columns go **negative** when every horizon fell.
216
+ - **`ravi({ shortPeriod = 7, longPeriod = 65, column, output = 'ravi' })`** —
217
+ Tushar Chande's Range Action Verification Index,
218
+ `100·|SMA(short) − SMA(long)|/SMA(long)`. The **absolute value** is the
219
+ study: it answers "is this trending" (above 3%), not "which way" — the
220
+ signed form is **26.98** away (measured). Scale-invariant, not
221
+ shift-invariant.
222
+ - **`trendIntensityIndex({ period = 30, maPeriod = 60, column, output =
223
+ 'tii' })`** — M. H. Pee's TII, `100·Σpos/(Σpos + Σneg)` over the last
224
+ `period` deviations from a `maPeriod` SMA. **F-AMBIG**: Pee's **sums**
225
+ ship, weighted by how far price strayed; the common **count** form is
226
+ **28.85** away (measured) and would deserve its own name. Bounded 0…100,
227
+ warm-up `maPeriod + period − 2` = **88** bars at the defaults.
228
+ - **`specialK({ column, output = 'specialK' })`** — Martin Pring's Special
229
+ K: the KST construction extended to **twelve** weighted smoothed rates of
230
+ change across three groups. The thirty-six constants are the study, so
231
+ there are no period options (the `kst` decision). **Warm-up 724 bars** —
232
+ a series shorter than 725 comes back entirely `undefined`, with the row
233
+ count preserved.
234
+ - `@pond-ts/financial`: **the prime studies and Bill Williams' Market
235
+ Facilitation Index** (corpus §6.2 / §6.3 / §6.6) — three per-bar studies with
236
+ no window, no warm-up and no parameters beyond their column names.
237
+ - **`primeNumberBands({ high?, low?, prefix = 'pnb' })`** → `pnbUpper`,
238
+ `pnbLower` — the smallest prime at or above the bar's high and the largest
239
+ at or below its low, so the bands **contain** the bar. A step function of
240
+ the price _level_: it holds flat inside a prime gap and jumps across one,
241
+ and two instruments at the same price have identical bands.
242
+ - **`primeNumberOscillator({ column = 'close', output = 'pno' })`** —
243
+ `price − nearestPrime(price)`, positive when the price sits above the prime
244
+ nearest it and exactly `0` when it **is** an integer prime. Both open
245
+ conventions are pinned: the sign is `price − prime`, and an **equidistant
246
+ tie goes to the lower prime** (so `pno(6) = +1`).
247
+ - Both: **a price below 2 is outside the domain** and reads `undefined` — two
248
+ is the smallest prime, so there is nothing to bracket with. **Neither is
249
+ scale- nor shift-equivariant**, and the property tests assert that
250
+ _absence_ rather than skipping the check. **Cost grows with the price
251
+ magnitude**, the only operators in the package that do: measured at 1M
252
+ bars, ~78 ms and ~55 ms at ordinary equity prices and **~6.5 s each at
253
+ ~1e7**. There is no sieve; the kernel documents why and what would change
254
+ it.
255
+ - **`marketFacilitationIndex({ high?, low?, volume?, output = 'bwmfi' })`** —
256
+ Bill Williams' `(high − low) / volume`, the raw ratio with **no `scale`
257
+ option** (no vendor constant is standard, unlike `easeOfMovement`'s
258
+ published `100_000_000`). The default output is **`bwmfi`, not `mfi`**:
259
+ `moneyFlowIndex` — a different indicator published under the same
260
+ abbreviation — already owns that name, and both must be able to sit on one
261
+ series. A zero-volume bar reads `undefined`; a flat bar reads `0`.
262
+ - `@pond-ts/financial`: **the bands and channels tail** (corpus §6.2) — four
263
+ studies in the uniform shape, each with oracle cases and a fluent method.
264
+ - **`starcBands({ period = 20, atrPeriod = 15, multiplier = 2, maType =
265
+ 'sma', high?, low?, close?, prefix = 'starc' })`** → `starcMiddle`,
266
+ `starcUpper`, `starcLower` — Manning Stoller's channel: a moving average
267
+ of the **close** with bands a multiple of the ATR away. The close-centred
268
+ sibling of `keltner` (typical-price centre) and of `atrBands` (bands
269
+ around an existing column, no centre). It **is** `movingAverage` followed
270
+ by `atrBands` around that average, pinned bit-for-bit by a test; it ships
271
+ for the name and the centre column. Per-column warm-up: centre at the
272
+ MA's own bar, bands at `max(centre, ATR)`.
273
+ - **`highLowBands({ period = 10, percent = 1, maType = 'trima', high?, low?,
274
+ prefix = 'hlb' })`** → `hlbMiddle`, `hlbUpper`, `hlbLower` — a moving
275
+ average of the **median price** with bands `± percent`. It **is**
276
+ `envelope` over a `medianPrice` column, pinned bit-for-bit.
277
+ - **`bollingerBandwidth({ period = 20, stdDev = 2, column?, output =
278
+ 'bbWidth' })`** — `100·(upper − lower)/middle`, the **×100** StockCharts /
279
+ ChartIQ form rather than Bollinger's bare ratio. A **flat window reads
280
+ `0`**, not `undefined`: the numerator is `2·stdDev·σ`, forced to zero,
281
+ over a non-zero centre — so on a flat stretch it is deliberately **not**
282
+ recoverable from `bollinger`'s (missing) band columns, while everywhere
283
+ else it is bit-for-bit.
284
+ - **`bollingerPercentB({ period = 20, stdDev = 2, column?, output =
285
+ 'percentB' })`** — `(price − lower)/(upper − lower)`, the **decimal** form
286
+ (`0` = lower band, `1` = upper), unbounded so an out-of-band price still
287
+ reads. A flat window is a genuine `0/0` and reads `undefined` — the
288
+ opposite answer from BandWidth's on the same window, and the clearest
289
+ illustration of the "is the numerator _forced_ to zero?" test.
290
+ - `@pond-ts/financial`: **the K3 price transforms and Balance of Power**
291
+ (corpus §6.8) — five studies in the uniform shape (each bar-column named by
292
+ its own option defaulting to its `DEFAULT_OHLCV` name, an `output`, a
293
+ fluent method), all five **exact** against TA-Lib bar-for-bar.
294
+ - **`typicalPrice({ high?, low?, close?, output = 'typicalPrice' })`**
295
+ (`(h+l+c)/3`, TA-Lib `TYPPRICE`),
296
+ **`medianPrice({ high?, low?, output = 'medianPrice' })`** (`(h+l)/2`,
297
+ `MEDPRICE`),
298
+ **`weightedClose({ high?, low?, close?, output = 'weightedClose' })`**
299
+ (`(h+l+2c)/4`, `WCLPRICE`) and
300
+ **`averagePrice({ open?, high?, low?, close?, output = 'averagePrice' })`**
301
+ (`(o+h+l+c)/4`, `AVGPRICE`). These are the per-bar summaries `vwap`, `cci`,
302
+ `mfi`, `keltner` and the Awesome Oscillator derive privately, now available
303
+ as **columns** so a caller can chart one or run another study over it
304
+ (`sma({ column: 'typicalPrice' })`). **No warm-up at all** — a bar's own
305
+ prices are all any of them reads, so bar 0 carries a value.
306
+ - **`balanceOfPower({ period?, maType?, open?, high?, low?, close?, output =
307
+ 'bop' })`** — Igor Livshin's `(close − open) / (high − low)`, the body over
308
+ the range, bounded −1…1. **Raw by default** (exact against TA-Lib `BOP`);
309
+ the optional `period` is ChartIQ's smoothed form over the shared `MaType`
310
+ menu. **F-AMBIG** — both conventions are published, and the one an oracle
311
+ can pin ships as the default. Passing `maType` without a `period` **throws**
312
+ rather than silently doing nothing. A **flat bar reads `0`, not
313
+ `undefined`**: a bar with no range traded at one price, so the numerator is
314
+ forced to zero (the `clvValues` rule from the volume group), which is
315
+ TA-Lib's answer too.
316
+ - `@pond-ts/financial`: **the moving-average stacks and the smoothed-momentum
317
+ tail** (corpus §6.1 / §6.3) — seven studies in the uniform shape (a `column`
318
+ plus an `output` or `prefix`, bar-count periods, a length-preserving
319
+ per-column warm-up, a fluent method), each with oracle cases.
320
+ - **`guppy({ column = 'close', type = 'ema', prefix = 'gmma' })`** — Daryl
321
+ Guppy's Multiple Moving Average: the **fixed twelve** averages, short
322
+ `3, 5, 8, 10, 12, 15` as `gmmaS3 … gmmaS15` and long
323
+ `30, 35, 40, 45, 50, 60` as `gmmaL30 … gmmaL60`. There is deliberately no
324
+ "which periods" option — the twelve are the study — but `type` takes the
325
+ whole shared `MaType` menu. The periods are exported as
326
+ `GUPPY_SHORT_PERIODS` / `GUPPY_LONG_PERIODS` so a chart can label the
327
+ ribbon. Each column is checked against `talib.MA` **at its own period**
328
+ (exact for `sma`; for `ema` the formula on TA-Lib's SMA seed plus a
329
+ geometric-decay check on pond's first-sample seed). Warm-up is
330
+ **per column** — `gmmaS3` at bar 2, `gmmaL60` at bar 59, which is 57 real
331
+ values of the fast ribbon a shared warm-up would have discarded.
332
+ - **`rainbow({ column = 'close', period = 2, type = 'sma', prefix =
333
+ 'rainbow' })`** — Mel Widner's Rainbow Moving Average (TASC, July 1997):
334
+ ten **recursive** averages, each smoothing the previous one, as
335
+ `rainbow1 … rainbow10`. Stage `k` warms up at `k · (period − 1)`, so ten
336
+ 2-bar averages are a ten-bar look-back. Deliberately **not** the
337
+ ten-increasing-lengths variant also published under the name: a recursive
338
+ 2-bar mean is a binomial filter, and stage 10 sits **0.83** from the
339
+ 11-bar SMA covering the same support (measured; the fixture's whole close
340
+ range is 19.4).
341
+ - **`rainbowOscillator({ column = 'close', period = 2, lookback = 10,
342
+ prefix = 'rbo' })`** → `rbo`, `rboUpper`, `rboLower` — ChartIQ's:
343
+ `100·(price − mean of the ten)/(HH − LL)` over `lookback` bars of
344
+ `column`, with the stack's own `max − min` as bands mirrored about zero.
345
+ **F-AMBIG**, so the source is named and the alternatives measured: the
346
+ divide-by-price form is **67.27** away and a first-average numerator
347
+ **49.16**, against a reading spanning −68.6…63.8. A flat `lookback`
348
+ window reads `undefined` — nothing forces the numerators to zero, so it
349
+ is a real number over zero rather than a `0/0`.
350
+ - **`kst({ column = 'close', signalPeriod = 9, prefix = 'kst' })`** →
351
+ `kst`, `kstSignal` — Martin Pring's Know Sure Thing on his intermediate
352
+ **daily** set: four percent rates of change (look-backs 10 / 15 / 20 /
353
+ 30), each smoothed by a simple average (10 / 10 / 10 / 15), weighted
354
+ 1 / 2 / 3 / 4 and summed, with a simple-average signal. The twelve
355
+ constants are deliberately **not** options — Pring published several KSTs
356
+ (short daily, weekly, monthly) and they are different indicators, not one
357
+ with parameters — so only `signalPeriod` is exposed. The line warms up at
358
+ bar 44, the signal `signalPeriod − 1` later. Scale-invariant, not
359
+ shift-invariant.
360
+ - **`priceMomentumOscillator({ column = 'close', prefix = 'pmo' })`** →
361
+ `pmo`, `pmoSignal` — DecisionPoint's PMO. Its two smoothing stages use
362
+ DecisionPoint's **custom** multiplier `α = 2/n`, which is **not** the
363
+ span EMA's `2/(n+1)` and is the only non-span exponential in the package:
364
+ measured, building both stages on the span EMA instead puts the line
365
+ **0.106** away on a reading whose scale is 3.906 (2.7%). The **signal**
366
+ is a plain span `EMA(10)` — DecisionPoint's own asymmetry, and a
367
+ custom-smoothed signal would sit 0.088 away. No period options; the line
368
+ warms up at bar 54 and the signal at 63.
369
+ - **`stochasticRsi({ column = 'close', rsiPeriod = 14, stochPeriod = 14,
370
+ kPeriod = 3, dPeriod = 3, prefix = 'stochRsi' })`** → `stochRsiK`,
371
+ `stochRsiD` — Chande & Kroll's Stochastic RSI, composed on the shipped
372
+ `rsi` rather than a private copy. **`stochRsiK` equals TA-Lib
373
+ `STOCHRSI`'s `fastd`** (not its `fastk`) bar-for-bar with identical
374
+ masks, measured to 9.9e-14; `stochRsiD` has no TA-Lib counterpart, since
375
+ `STOCHRSI` stops at `fastd`. Crossing the two columns is a 45-point error
376
+ on the oracle input, and the generator asserts both the match and the
377
+ mismatch. The option names are TradingView's, so **`stochPeriod` here is
378
+ `stochastic`'s `kPeriod`** and **`kPeriod` here is its `slowing`** — a
379
+ table in the docstring maps all three vocabularies.
380
+ - **`trueStrengthIndex({ column = 'close', longPeriod = 25, shortPeriod =
381
+ 13, signalPeriod = 7, prefix = 'tsi' })`** → `tsi`, `tsiSignal` — William
382
+ Blau's TSI: the bar-over-bar change double-smoothed, over its own
383
+ magnitude double-smoothed the same way, ×100. Bounded −100…100 by
384
+ construction. The smoothing **order is the definition** — `longPeriod`
385
+ first, `shortPeriod` to its output — and the swap sits **15.93** away on
386
+ a line spanning −28.7…80.7 (measured; the generator asserts it). A
387
+ perfectly flat column gives a zero denominator with the numerator forced
388
+ to zero, a genuine `0/0` → `undefined`. Scale- **and** shift-invariant,
389
+ unlike `kst` and `priceMomentumOscillator`. Options are named
390
+ `longPeriod` / `shortPeriod`, not `long` / `short`, which read as
391
+ position vocabulary in a financial package.
392
+ - **`movingAverageDeviation({ column = 'close', period = 20, maType =
393
+ 'sma', output = 'maDev' })`** — `price − MA`, in **price units**, over
394
+ the shared `MaType` menu. The corpus lists this study as "points **or**
395
+ percent"; the percent half is **already shipped** as `disparityIndex`,
396
+ and the two agree **bit for bit** (`100·maDev/MA === disparity`,
397
+ measured 0.0, asserted in the oracle), so only the points form ships and
398
+ there is no `mode` flag — two indicators behind an option is the
399
+ `keltner` precedent this package avoids. It is the one study in the pair
400
+ that is **shift-invariant** (the constant cancels between the price and
401
+ its own average) and the one with no division and so no
402
+ zero-denominator case.
403
+ - `@pond-ts/financial`: **the K6 stateful-fold kernel and the state-machine
404
+ studies** (corpus §6.4 / §6.6, gap **G2**) — one new public kernel and six
405
+ studies in the uniform shape (`column` / `output` or a `prefix`, bar-count
406
+ periods, a length-preserving warm-up, a fluent method), each with oracle
407
+ cases.
408
+ - **`foldRows(inputs, outputCount, state, step)`** (kernel **K6**,
409
+ [PND-SFOLD]) — a per-bar fold with carried state over several row-aligned
410
+ `Float64Array` columns, the one shape the rolling kernels and core's
411
+ single-column `scan` cannot express. One O(N·k) pass, outputs allocated
412
+ and **NaN-filled** by the kernel, the state object the study's own and
413
+ mutated in place — no per-bar allocation. Exported with its `FoldStep`
414
+ type. Measured at 1M bars: **11.3 ms** for a two-column fold over a no-op
415
+ step, against `ema()`'s 6.3 ms and a 5.2 ms floor for the same scan with
416
+ no callback.
417
+ - **A missing cell RESETS the machine.** The step is offered only complete
418
+ rows, with `run` = how many consecutive complete rows end here; an
419
+ incomplete row leaves the outputs `undefined` and restarts `run` at 1.
420
+ The alternative — holding the state across the gap — was rejected: a
421
+ Parabolic SAR that did not see a bar cannot know whether it flipped, and
422
+ a recursion carried on the wrong side never recovers. `run` doubles as
423
+ the seeded flag, which is why there is no separate `seed` hook.
424
+ - **`parabolicSar({ step = 0.02, maxStep = 0.2, high?, low?, prefix = 'psar' })`**
425
+ → `psar` + `psarTrend` — Wilder's stop-and-reverse, **bar-for-bar
426
+ identical to TA-Lib `SAR`** (measured 0.0 maximum absolute difference at
427
+ `(0.02, 0.2)`, `(0.05, 0.5)` and `(0.01, 0.1)`, masks identical, first
428
+ value at bar 1). The three prose-ambiguous details — the `−DM` seed side,
429
+ the first bar's "yesterday is today" clamp, and the clamp against the last
430
+ two bars on a reversal — are pinned to TA-Lib's reading and **probed**
431
+ against it in the oracle generator.
432
+ - **`superTrend({ period = 10, multiplier = 3, high?, low?, close?, prefix = 'st' })`**
433
+ → `st` + `stTrend` — Olivier Seban's ratcheting ATR band, as TradingView's
434
+ `ta.supertrend` implements it, on the same Wilder `atrValues` `atr` and
435
+ `keltner` read. The two bands are **not** emitted: `st` already is
436
+ whichever one is live.
437
+ - **`atrTrailingStop({ period = 14, multiplier = 3, high?, low?, close?, prefix = 'ats' })`**
438
+ → `ats` + `atsTrend` — the close-anchored ratcheting stop (Vervoort's).
439
+ The **Chandelier** anchor (rolling extreme rather than the close) is a
440
+ different study, reachable as `donchian` + `atr`, and deliberately not a
441
+ knob here.
442
+ - **`negativeVolumeIndex` / `positiveVolumeIndex`
443
+ (`{ column = 'close', volume?, output = 'nvi' | 'pvi', start = 1000 }`)** —
444
+ Fosback's conditional cumulative indices. No period and no warm-up. A
445
+ **flat volume holds on both**, so the two do not partition the bars; a
446
+ zero previous close ends the index rather than inventing a level.
447
+ - **`klinger({ fastPeriod = 34, slowPeriod = 55, signalPeriod = 13, high?, low?, close?, volume?, prefix = 'kvo' })`**
448
+ → `kvo` + `kvoSignal` — Klinger's **original** volume force
449
+ (`volume × |2 × (dm/cm − 1)| × trend × 100`, with `cm` accumulating over
450
+ the trend leg) through an EMA pair plus a signal EMA. **F-AMBIG**:
451
+ TradingView's `ta.kvo`, which drops the `dm/cm` factor entirely, is a
452
+ different indicator sharing the name and is documented rather than offered
453
+ as an option.
454
+ - **The three stop machines append `${prefix}` and `${prefix}Trend`**, the
455
+ value under the bare prefix rather than `${prefix}Line`, with `+1` meaning
456
+ the line sits **below** price. The side is a separate column because it is
457
+ not recoverable from the value — all three clamp, so the line can print
458
+ exactly on an extreme or on the close.
459
+ - `@pond-ts/financial`: **the rolling linear-regression family** (corpus §6.7,
460
+ plus the two §6.3 studies that hang off it) — one new public kernel and four
461
+ studies in the uniform shape (a `column` / `output` or `prefix`, bar-count
462
+ periods, a length-preserving warm-up, a fluent method), each with oracle
463
+ cases at two periods.
464
+ - **`linearRegressionValues(values, period)`** (kernel **K7**) — the rolling
465
+ ordinary-least-squares fit of a raw `Float64Array` against the bar index,
466
+ returning `{ slope, intercept, r2 }` in **one O(N) pass that is flat in
467
+ `period`** (measured at 1M bars: 29 ms at `period 14`, 29 ms at
468
+ `period 200`). `x` is deterministic, so `Σx` and `Σx²` are closed forms and
469
+ only `Σy`, `Σxy` and `Σy²` roll. Exported alongside its `RollingRegression`
470
+ result type.
471
+ Near-flat windows (changing by ulps, or a plateau the anchor has gone
472
+ stale across) are recomputed two-pass on a fresh local anchor when the
473
+ rolling spread falls below `1e-3` of the gross magnitude that has passed
474
+ through its sums since the last rebuild — the sign is not the tell — and
475
+ `r2` is pinned to 1; every changing window is tested against an exact
476
+ BigInt-rational reference (review, 2026-09-07).
477
+ - **`linearRegression({ period = 14, column = 'close', prefix = 'linreg' })`**
478
+ → `linregValue`, `linregSlope`, `linregIntercept`, `linregAngle`,
479
+ `linregR2` — five readings of **one** fit, all warming up together at
480
+ `period − 1`. Four are **exact against TA-Lib** (`LINEARREG`,
481
+ `LINEARREG_SLOPE`, `LINEARREG_INTERCEPT`, `LINEARREG_ANGLE`; measured
482
+ ≤ 1.3e-12 at `period 14`, masks identical); `linregR2` has no TA-Lib
483
+ equivalent and is a pandas replication.
484
+ - **`linregIntercept` is the fit at the window's FIRST bar** (`x = 0`),
485
+ TA-Lib's convention — `linregValue` is the fit at its last. On the
486
+ oracle input at `period 14` the two differ by up to **13.68 points** on
487
+ a series whose entire range is 19.4, so reading one for the other is a
488
+ real error rather than a rounding one.
489
+ - **`linregAngle` is scale-DEPENDENT**, and deliberately so: TA-Lib's
490
+ `LINEARREG_ANGLE` is `atan(slope)` in degrees with no normalisation, so
491
+ the same instrument quoted in cents reads a different angle. Stated on
492
+ the study and pinned by a property test that asserts scaling **moves**
493
+ it.
494
+ - **`timeSeriesForecast({ period = 14, column, output = 'tsf' })`** — the
495
+ same fit projected one bar past the window (`intercept + slope·period`),
496
+ **exact against TA-Lib `TSF`** (≤ 2.1e-13). Also known as the "time series
497
+ moving average" and deliberately **not** added to the K2 `MaType` menu:
498
+ every type there is the identity at `period 1`, and a one-bar window has
499
+ no slope.
500
+ - **`chandeForecastOscillator({ period = 14, column, output = 'cfo' })`** —
501
+ `100 · (price − TSF) / price` (Tushar Chande). No TA-Lib function; the
502
+ oracle is a pandas replication on the TA-Lib-checked `TSF`, separated from
503
+ the version that subtracts the in-window endpoint (0.89 at `period 14`).
504
+ Scale-invariant, **not** shift-invariant.
505
+ - **`centerOfGravity({ period = 10, column, output = 'cog' })`** — Ehlers'
506
+ position-weighted balance point (_Stocks & Commodities_, May 2002),
507
+ `−Σ(k+1)·price[i−k] / Σprice[i−k]` with the **newest** bar carrying weight
508
+ 1. Negative and bounded `[−period, −1]` on positive prices (a
509
+ zero-crossing source column can read outside it), and a flat window balances at
510
+ `−(period+1)/2`. This is **TradingView's uncentred `ta.cog`**; Ehlers' own
511
+ EasyLanguage adds `(period+1)/2` to re-centre on zero, a constant offset
512
+ the oracle pins. It needs no new kernel: the descending weights are the
513
+ ascending ones subtracted from a constant, so the study is exactly
514
+ `(period + 1)·(WMA/(2·SMA) − 1)` over the K2 engine's `wma` and
515
+ `rollingMeanValues` — an identity pinned by a test against the naive
516
+ `O(N·period)` definition.
517
+ - **All four run the strict window**: a bar is emitted only when all
518
+ `period` cells are finite, because `x` names a _position_ and dropping a
519
+ cell would fit the line against the wrong abscissa (the `wma` rule). A
520
+ leading gap shifts the start; an interior gap blanks `period` bars and
521
+ then recovers.
522
+ - **A flat window: `slope` exactly `0`, `linregR2` `undefined`.** The
523
+ slope's numerator is forced to zero by the same condition that empties
524
+ the window, so `0` is a real reading; `R²` is a genuine `0/0` and is
525
+ missing. Both are decided by an O(1) count of the changes inside the
526
+ window rather than computed — without it the accumulator residue reads
527
+ `slope = −2.1e-14` and **`R² = −13.5`**, outside the statistic's own
528
+ range (measured).
529
+ - The regression periods must be **at least 2** (`n²(n²−1)/12` is `0` at
530
+ `n = 1`); `centerOfGravity` takes a moment rather than a fit, so it
531
+ accepts `period 1` and reads `−1`.
532
+ - `@pond-ts/financial`: **the two-series / comparison family** (corpus §6.7,
533
+ kernel K8) — `correlation`, `beta`, `priceRelative` and `performanceIndex`,
534
+ plus the public kernel `rollingBivariateValues`. Seven oracle cases; the two
535
+ TA-Lib-backed studies agree with `CORREL` and `BETA` bar-for-bar.
536
+ - **The comparison series is a `benchmark` COLUMN on the same series, never a
537
+ second `TimeSeries`.** The consumer aligns and joins first
538
+ (`align` + `TimeSeries.joinMany`), and the study reads two columns of one
539
+ row — exactly as `atr` reads `high`/`low`/`close`. A study that took a
540
+ second series would have to invent an alignment policy (hold?
541
+ interpolate? inner or outer?) that core already expresses, better and once
542
+ for the whole pipeline. A `benchmark` or `column` that is not on the
543
+ series **throws** rather than reading empty.
544
+ - **`correlation({ period = 30, column = 'close', benchmark, output = 'corr' })`**
545
+ → Pearson's `r` over the two **price** columns, `= talib.CORREL`
546
+ bar-for-bar. TA-Lib correlates the raw inputs, not their returns; the
547
+ return correlation is the same study over two `percentChange` columns.
548
+ - **`beta({ period = 5, column = 'close', benchmark, output = 'beta' })`** →
549
+ `cov(returns, benchmarkReturns) / var(benchmarkReturns)` over `period`
550
+ one-bar returns. **Pass prices** — the returns are taken inside, as
551
+ `percentChangeValues(v, 1)`. Equals `talib.BETA(benchmark, close)`
552
+ bar-for-bar: TA-Lib regresses its **second** input on its **first**
553
+ (measured), so the benchmark goes first there; pond's option names remove
554
+ the ambiguity.
555
+ - **`priceRelative({ column = 'close', benchmark, output = 'priceRel' })`** →
556
+ the bare ratio, with **no period and no warm-up**. ChartIQ's _Price
557
+ Relative_ and _Relative Strength (comparative)_ are one implementation;
558
+ neither is Wilder's `rsi`.
559
+ - **`performanceIndex({ period = 20, column = 'close', benchmark, output = 'perf' })`**
560
+ → each side's own `period`-bar growth, divided, so `1` is parity. The
561
+ **ratio** form ships (some vendors publish `× 100` or `− 1`);
562
+ `(perf − 1) × 100` is `percentChange(priceRelative, period)` identically,
563
+ which a test asserts.
564
+ - **`rollingBivariateValues(x, y, period)`** → `{ covariance, varianceX,
565
+ varianceY }`, population (`ddof = 0`), over a **strict** pair window: all
566
+ `period` rows of **both** columns, or nothing. Shifted-frame Welford with
567
+ the aligned rebuild ([PND-SHIFTFRAME] / [PND-PROCKERN]), never
568
+ `Σxy − ΣxΣy/n` — measured at 1e12-scale prices, the textbook form returns a
569
+ negative variance (a non-finite correlation) where this holds 2.4e-15.
570
+ Validates `period` (integer `≥ 2`) at the boundary like every public
571
+ kernel, and rebuilds a window on demand when a changing column's `m2`
572
+ falls below `1e-3` of the gross shifted magnitude that has passed through
573
+ it since the last rebuild, or `cxy² > m2x·m2y` past rounding slack — so
574
+ no near-flat window reports a false missing cell or an out-of-range
575
+ correlation; tested against an exact BigInt-rational reference (review,
576
+ 2026-09-07).
577
+ - **Two deliberate TA-Lib deltas, both measured**: a flat window is
578
+ `undefined` here and `0.0` in TA-Lib (`CORREL` and `BETA` both substitute
579
+ zero for a zero denominator), and a zero price is a **missing** return here
580
+ where TA-Lib substitutes a return of `0`.
581
+
582
+ - `@pond-ts/financial`: **the Wilder directional group** (corpus §6.4) — three
583
+ studies and two new public kernels, in the uniform shape (redirectable bar
584
+ inputs, bar-count periods, length-preserving warm-up, a fluent method), each
585
+ with oracle cases at two periods.
586
+ - **`directionalMovement({ period = 14, high?, low?, close?, prefix = 'dmi' })`**
587
+ → `dmiPlusDi`, `dmiMinusDi`, `dmiDx`, `dmiAdx`, `dmiAdxr`. Wilder's Directional
588
+ Movement System — `+DI`, `−DI`, `DX`, `ADX` and `ADXR` in **one** study
589
+ with a **per-column warm-up** (bars 14 / 14 / 14 / 27 / 40 at the default),
590
+ the `macd` precedent rather than five studies re-running the same pipeline.
591
+ The `DI` denominator **is** `atr()`'s array, so the two measure range
592
+ identically by construction.
593
+ - **Seeding is Wilder's, not TA-Lib's**, and this is the one delta.
594
+ Wilder's worksheet seeds `+DM`/`−DM`/`TR` on the mean of the first
595
+ `period` values (what `wilderValues` runs, and what TA-Lib's own `ATR`
596
+ uses); TA-Lib's `ADX` family instead seeds on the first `period − 1` and
597
+ takes one decayed step, so TA-Lib's `+DI` denominator disagrees with
598
+ TA-Lib's own `ATR` (measured on the oracle input: `1.515450` vs
599
+ `1.411787` at `period 14`). Warm-up masks are identical to TA-Lib's on
600
+ all five columns; the oracle proves the **formula** exactly by replaying
601
+ our pipeline on TA-Lib's seed (`≤ 2.9e-14`) and bounds the pond-seed
602
+ transient separately — at `period 14`, `+DI` 0.117 points at the first
603
+ shared bar decaying to 0.0092 by bar 79, `ADX` 0.421 → 0.0046.
604
+ - **`ADXR` looks back `period − 1` bars**, TA-Lib's reading and the
605
+ package's own bar-count convention (a `period`-bar window spans
606
+ `i − period + 1 … i`). The literal "`period` bars ago" reading differs
607
+ by up to 2.64 points at `period 14`.
608
+ - **`+DI + −DI = 0` → `DX = 0`**, not `undefined`: both legs are
609
+ non-negative, so a zero sum forces a zero numerator (the `clvValues`
610
+ flat-bar rule, not the `percentOfRangeValues` flat-window one). A zero
611
+ **true range** is a genuine `0/0` and does read `undefined`.
612
+ - **`aroon({ period = 25, high?, low?, prefix = 'aroon' })`** → `aroonUp`,
613
+ `aroonDown`, `aroonOsc`. `100 · (period − bars since the extreme) / period`
614
+ on each side, and their difference. **Matches TA-Lib `AROON` and
615
+ `AROONOSC` exactly** (delta `0`, identical masks). The window is
616
+ **`period + 1` bars** — `period` counts the oldest _age_ reportable, and
617
+ "`period` bars ago" is itself a reading — so the warm-up is `period` rows;
618
+ **ties go to the most recent bar** (measured against TA-Lib). Invariant to
619
+ any monotonic rescaling of price, not merely to scale and shift.
620
+ - **`vortex({ period = 14, high?, low?, close?, prefix = 'vi' })`** →
621
+ `viPlus`, `viMinus`. Botes & Siepman's `Σ|high − prevLow| / Σ TR` and
622
+ `Σ|low − prevHigh| / Σ TR`. No TA-Lib function; pandas replication, with
623
+ the plain-bar-range denominator asserted as a visible separation. Both
624
+ legs are positive and **not bounded by 1**; `Σ TR = 0` → `undefined`
625
+ (unlike `DX`, the numerator is not forced to zero with it).
626
+ - **`directionalMovementValues(high, low)`** (`kernels/directional-movement.ts`)
627
+ — Wilder's `+DM`/`−DM` split in one pass, with the tie and sign rules and
628
+ an explicit `NaN` guard (an unknown move must not read as a flat one).
629
+ - **`barsSinceExtremeValues(values, period, 'max' | 'min')`**
630
+ (`kernels/highest-lowest.ts`) — the corpus's **G3 argmax gap**: bars since
631
+ the window's extreme, via a **monotonic deque**, so it is O(N) and _flat in
632
+ `period`_. Measured at 1M bars: 26.7 ms at `period 25` and 26.5 ms at
633
+ `period 200`, against a naive re-scan's 78 ms and 545 ms. `aroon` costs
634
+ 102 ms at 1M bars against `donchian`'s 247 ms — the first measured
635
+ evidence for the monotonic-deque fast path core's rolling min/max wants.
636
+ - `@pond-ts/financial`: **the volatility tail** (assessment §6.5) — seven
637
+ studies of range and drawdown, **none of which TA-Lib implements**, so every
638
+ one is a pandas replication in the oracle with its analytic first-valid bar
639
+ asserted _and_ a measured separation from the plausible wrong turn. Each
640
+ ships with a fluent method, two oracle cases, hand-computed unit values,
641
+ missing-cell placement tests, property tests and a perf entry.
642
+ - **`chaikinVolatility({ period = 10, rocPeriod = 10, high?, low?, output = 'chaikinVol' })`**
643
+ → `chaikinVol`. The percent rate of change of `EMA(high − low, 10)` over
644
+ the last 10 bars — is the average bar getting wider? **Plain** range, not
645
+ true range (Chaikin's; 35.6 points from the true-range version on the
646
+ oracle input). Two separate periods, both defaulting to 10, because the
647
+ smoothing span and the look-back are different quantities. First valid at
648
+ `period − 1 + rocPeriod`.
649
+ - **`massIndex({ emaPeriod = 9, sumPeriod = 25, high?, low?, output = 'mass' })`**
650
+ → `mass`. Dorsey's `Σ EMA(range)/EMA(EMA(range))` — range expansion with no
651
+ direction at all, which is the point of it (the 27 → 26.5 "reversal bulge"
652
+ warns that a trend will turn without saying which way). A **sum**, so a
653
+ steady market reads ≈ `sumPeriod`, not 0 or 1; the mean version is 24.1
654
+ away and is asserted apart. The EMA∘EMA chain follows the `trix` rule, so
655
+ the first valid bar is `2·emaPeriod + sumPeriod − 3` (bar 40 at the
656
+ defaults).
657
+ - **`choppinessIndex({ period = 14, high?, low?, close?, output = 'chop' })`**
658
+ → `chop`, bounded `0..100`. Dreiss' `100·log10(ΣTR/(HH−LL))/log10(period)`:
659
+ path walked against ground covered, **high = choppy, low = trending**, and
660
+ silent about direction. **True** range, the package's own `trueRangeValues`
661
+ (asserted `== talib.TRANGE`), 7.7 points from the plain-range version. The
662
+ log base cancels, which the generator asserts rather than assumes.
663
+ `period` must be **≥ 2** — `log10(1)` is zero.
664
+ - **`ulcerIndex({ period = 14, column?, output = 'ulcer' })`** → `ulcer`.
665
+ Peter Martin's downside-only volatility: the RMS percentage drawdown from
666
+ the window's own highest close. Ships the **rolling (StockCharts) form**;
667
+ Martin's cumulative original is one number per portfolio, not a series, and
668
+ is a documented delta rather than an option — as is the mean-**absolute**
669
+ variant, which is the Pain Index (1.53 away, asserted apart). Warm-up is
670
+ **`2·period − 2`**: two chained windows.
671
+ - **`verticalHorizontalFilter({ period = 28, column?, output = 'vhf' })`** →
672
+ `vhf`, a **fraction** in `(0, 1]`. Adam White's `(HH−LL)/Σ|Δ|` — the
673
+ opposite polarity to `chop`. Both halves read the **same** column, which is
674
+ what makes it composable over another study's output. Warm-up is `period`,
675
+ not `period − 1`: `period` changes need `period + 1` values, and the
676
+ one-term-shorter version is asserted apart.
677
+ - **`gopalakrishnanRangeIndex({ period = 10, high?, low?, output = 'gapo' })`**
678
+ → `gapo`. `ln(HH − LL)/ln(period)` — which _is_ the log of the window's
679
+ range in base `period`, since the base cancels. The **one study in the
680
+ batch that is not scale-invariant**: scaling every price by `k` shifts the
681
+ reading by exactly `ln(k)/ln(period)`, pinned as an identity rather than an
682
+ invariance. `period` must be **≥ 2**.
683
+ - **`relativeVolatilityIndex({ period = 14, stdevPeriod = 10, column?, output = 'relVol' })`**
684
+ → `relVol`, bounded `0..100`. Dorsey's RSI form with the population σ of
685
+ the close in place of the price change, **Wilder-smoothed** (his
686
+ definition; TradingView's EMA-smoothed fork is 31.3 points away and
687
+ measured). **The column is `relVol`, not `rvi`** — that belongs to
688
+ `relativeVigorIndex`, a completely different study, and the two now sit on
689
+ one series without collision. An unchanged close counts as a **down** bar,
690
+ deliberately unlike `rsi`'s split, which gives a flat bar 0 on both legs.
691
+
692
+ **One new kernel**, `barRangeValues` (`kernels/typical-price.ts`, not
693
+ public): the plain `high − low`, shared by `chaikinVolatility` and
694
+ `massIndex`. It is deliberately **not** `trueRangeValues` — the Wilder family
695
+ (ATR, Keltner, Choppiness) takes true range and the Chaikin/Dorsey family
696
+ takes plain range, because that is what each author defined, and neither
697
+ study exposes a knob to swap them.
698
+
699
+ **Two sharp edges worth knowing.** (1) `ulcerIndex` is the first study to
700
+ take a **square root** of a rolling mean, and an incremental accumulator's
701
+ `O(ε)` residue in the mean of squares becomes `1.6e-9` in the reading — right
702
+ where the answer should be exactly `0` (a window at new highs). The
703
+ contributing bars are counted so that reading is exact; the oracle case at
704
+ `period 5` is what caught it. (2) `rollingValues`' answer to a **misnamed
705
+ column** depends on the reducer: `stdev`/`avg` take the range-exact path and
706
+ read all-missing, `max`/`min` fall through to core's sweep and throw. So
707
+ `relativeVolatilityIndex` answers empty where `ulcerIndex` and
708
+ `verticalHorizontalFilter` throw. Both behaviours are pinned by tests and
709
+ documented on the studies; the fix belongs in the kernel.
710
+
711
+ **Zero-denominator guards, where they are and are not.** `choppinessIndex`
712
+ and `gopalakrishnanRangeIndex` divide (or take a logarithm) at their
713
+ **output**, so a non-finite result would reach `withColumn` — which throws on
714
+ `±Infinity` — and their guards are load-bearing. `massIndex` and `ulcerIndex`
715
+ divide **upstream of a rolling summation**, which counts a non-finite cell as
716
+ missing exactly as it counts a `NaN`, so their guards were unobservable and
717
+ were removed after mutation testing; the behaviour (`undefined`, never an
718
+ infinity) is unchanged and still unit-tested. `verticalHorizontalFilter` and
719
+ `relativeVolatilityIndex` need none at all — a zero denominator forces a zero
720
+ numerator in both.
721
+
722
+ - `@pond-ts/financial`: **the volume & money-flow group** (corpus §6.6) — eight
723
+ studies over one new kernel, all in the uniform shape (redirectable bar
724
+ inputs, bar-count periods, length-preserving warm-up, a fluent method) and
725
+ each with oracle cases at two parameterisations where it has a period.
726
+ - **`accumulationDistribution({ high?, low?, close?, volume?, output = 'ad' })`**
727
+ → `ad`. Chaikin's running total of `CLV × volume`, where
728
+ `CLV = ((close − low) − (high − close)) / (high − low)` grades _where in
729
+ its own range_ a bar closed. Like `obv` it has **no period** and no
730
+ warm-up. **Matches TA-Lib `AD` exactly** (delta `0`) on bars with a range.
731
+ - **`chaikinOscillator({ fastPeriod = 3, slowPeriod = 10, high?, low?, close?, volume?, output = 'chaikinOsc' })`**
732
+ → `chaikinOsc`. `EMA(A/D, 3) − EMA(A/D, 10)`, the A/D line's momentum.
733
+ **Matches TA-Lib `ADOSC` exactly** (delta `0`, identical masks) — the one
734
+ EMA-family study here with **no seed delta**, because TA-Lib's own `ADOSC`
735
+ seeds both EMAs on the first A/D value, which is pond's convention. (The
736
+ oracle also asserts the SMA-seeded reconstruction is visibly different, so
737
+ the case pins which seed ships.)
738
+ - **`priceVolumeTrend({ close?, volume?, output = 'pvt' })`** → `pvt`. The
739
+ running total of `volume × (close − prevClose)/prevClose` — OBV's idea
740
+ scaled by _how far_ price moved. The **fraction**, not the percent: it
741
+ composes on the `ROC` kernel and divides by 100, which is the one place
742
+ that constant appears. **`pvt[0]` is `undefined`, not `0`** — the term
743
+ needs a previous close, and unlike OBV there is no vendor convention to
744
+ seed from; every later level is identical either way.
745
+ - **`chaikinMoneyFlow({ period = 20, high?, low?, close?, volume?, output = 'cmf' })`**
746
+ → `cmf`. `Σ CLV·volume / Σ volume` over the window — the A/D term as a
747
+ **bounded** `[−1, +1]` reading. Runs on the same `rollingWeightedMeanValues`
748
+ kernel as `vwap`, so it inherits its edge rules; `Σ volume = 0` →
749
+ `undefined`.
750
+ - **`moneyFlowIndex({ period = 14, high?, low?, close?, volume?, output = 'mfi' })`**
751
+ → `mfi`. The RSI form on raw money flow (`typical price × volume`, split
752
+ by the direction of the typical price). **Matches TA-Lib `MFI`** to
753
+ `2.8e-14` with identical masks. Warm-up is **`period` rows, not
754
+ `period − 1`** (the first bar has no previous typical price), the `rsi` /
755
+ `atr` off-by-one.
756
+ - **`forceIndex({ period = 13, close?, volume?, output = 'force' })`** →
757
+ `force`. Elder's `EMA((close − prevClose) × volume, 13)`; **`period: 1` is
758
+ the raw, unsmoothed force**, so no `smooth: false` flag is needed. No
759
+ `maType` — Elder names the EMA (the `elderRay` precedent).
760
+ - **`easeOfMovement({ period = 14, maType = 'sma', scale = 100_000_000, high?, low?, volume?, output = 'eom' })`**
761
+ → `eom`. Arms' midpoint move divided by the box ratio
762
+ `(volume / scale) / (high − low)`, MA-smoothed. `scale` is StockCharts' /
763
+ ChartIQ's constant and is a **pure linear multiplier**, exposed because the
764
+ right value depends on the instrument's volume units, not on taste. A flat
765
+ bar and a zero-volume bar both report `undefined` (two zero denominators).
766
+ - **`volumeOscillator({ fastPeriod = 5, slowPeriod = 10, maType = 'sma', volume?, output = 'volOsc' })`**
767
+ → `volOsc`. `100 · (MA(volume, 5) − MA(volume, 10)) / MA(volume, 10)`. This
768
+ is **literally `priceOscillator`'s percent mode over the volume column**, so
769
+ it delegates to it rather than restating the arithmetic — a test pins the
770
+ identity. What the wrapper adds is the name and the volume-appropriate
771
+ defaults (5/10/sma against 12/26/ema).
772
+
773
+ **Volume Rate of Change is not a study**: the corpus names it, but
774
+ `percentChange({ column: 'volume' })` _is_ its definition (and is
775
+ TA-Lib-verified through `ROC`), so what ships is a recipe note in API.md and
776
+ a test that pins it — step 0 of the studies README.
777
+
778
+ **One new kernel**, `clvValues` / `accumulationDistributionValues`
779
+ (`kernels/close-location.ts`), shared by A/D, the Chaikin oscillator and CMF.
780
+ A **flat bar (`high === low`) has a close location of exactly `0`** — its
781
+ numerator `(c − l) − (h − c)` is forced to zero, so `0` is the value rather
782
+ than a convention — and so A/D matches TA-Lib's `AD` on every bar, halts
783
+ included, and CMF counts the bar's volume in its denominator as every
784
+ conventional CMF does. A window with **no money flow at all**
785
+ makes `mfi` `undefined` where TA-Lib reports `0` — and TA-Lib reports `0`
786
+ for any window whose total flow is merely below `1.0`, measured returning `0`
787
+ where the answer is `100` on a rising series with `1e-9` volume.
788
+
789
+ - `@pond-ts/financial`: **the momentum tail** (assessment §6.3) — six
790
+ oscillators, all of them ratios rather than price-unit quantities, so all six
791
+ are invariant to **both** a scale factor and a constant shift (pinned by
792
+ property tests, which is the pair that catches a dropped normalisation).
793
+ Each ships with a fluent method, two oracle cases, hand-computed unit values,
794
+ missing-cell placement tests and a perf entry.
795
+ - **`chandeMomentum({ period = 14, column = 'close', output = 'cmo' })`** →
796
+ `cmo`, bounded `−100..100`. `100·(Σup − Σdown)/(Σup + Σdown)` over
797
+ **Chande's unsmoothed** window sums. **This is deliberately not TA-Lib's
798
+ `CMO`**, which Wilder-smooths the legs and is therefore exactly
799
+ `2 · rsi − 100` — a second name for a shipped column. The generator asserts
800
+ that identity (2.8e-14) as the reason, and measures the definition delta:
801
+ 68.28 points at `period 14`, 131.55 at `period 5`, warm-ups identical.
802
+ - **`ultimateOscillator({ shortPeriod = 7, mediumPeriod = 14, longPeriod = 28, high?, low?, close?, output = 'uo' })`**
803
+ → `uo`, bounded `0..100`. Williams' three-horizon blend of buying pressure
804
+ over true range, weighted 4/2/1. Matches **TA-Lib `ULTOSC`** to 7.1e-15 with
805
+ identical masks; the true range is the package's own kernel, asserted equal
806
+ to `talib.TRANGE` exactly. Three **named** periods (not a tuple — the
807
+ weights are positional), validated strictly increasing.
808
+ - **`commodityChannelIndex({ period = 20, high?, low?, close?, output = 'cci' })`**
809
+ → `cci`. `(tp − SMA(tp))/(0.015 · meanAbsDev(tp))`, matching **TA-Lib
810
+ `CCI`** to 3.6e-12. Unbounded — ±100 is a convention, not a limit. A
811
+ zero-deviation window is `undefined` rather than TA-Lib's `0`.
812
+ - **`intradayMomentumIndex({ period = 14, open?, close?, output = 'imi' })`**
813
+ → `imi`, bounded `0..100`. RSI's form over the candle **body**, with
814
+ **plain** window sums rather than Wilder smoothing (Chande's definition; the
815
+ corpus's "RSI form" is the ambiguity this pins). Warm-up is `period − 1`, a
816
+ row shorter than `rsi`/`chandeMomentum`, because a body needs no previous
817
+ bar. Over identical legs `imi = (cmo + 100)/2`, which a test pins.
818
+ - **`relativeVigorIndex({ period = 10, open?, high?, low?, close?, prefix = 'rvi' })`**
819
+ → `rvi` / `rviSignal`. TradingView's definition: ratio of `SWMA(close −
820
+ open)` sums to `SWMA(high − low)` sums, signal a fourth SWMA. **SWMA is the
821
+ symmetric `(1,2,2,1)/6` filter, not the K2 engine's linear `wma(4)`** — a
822
+ test pins the impulse response. The first study to read all four OHLC
823
+ columns.
824
+ - **`psychologicalLine({ period = 12, column = 'close', output = 'psy' })`** →
825
+ `psy`, bounded `0..100` and quantised to multiples of `100/period`. An
826
+ unchanged close is **not** an up bar (strict `>`), and a bar with no close
827
+ leaves both itself and its successor undirected.
828
+
829
+ **Kernels.** Three additions, none public: `rollingMeanAbsDevValues`
830
+ (CCI's denominator — the package's only **O(N·period)** kernel; the cost is
831
+ measured, and the `O(N log period)` order-statistic form that would replace
832
+ it is written down rather than left to be rediscovered),
833
+ `symmetricWeightedValues` (the 4-bar SWMA), and `upDownLegValues` (the
834
+ gain/loss split, now shared by `rsi`, `chandeMomentum` and
835
+ `intradayMomentumIndex` — `rsi` was moved onto it, with no change to its
836
+ numbers).
837
+
838
+ - `@pond-ts/financial`: **five K2 consumers** — the first studies built on the
839
+ moving-average engine, two channels and three smoothed rates. All five take
840
+ the uniform shape (bar-count periods, redirectable inputs, length-preserving
841
+ warm-up, a fluent method) and each has two oracle cases.
842
+ - **`keltner({ period = 20, atrPeriod = 10, multiplier = 2, maType = 'ema', high?, low?, close?, prefix = 'kc' })`**
843
+ → `kcMiddle` / `kcUpper` / `kcLower`. A moving average of **typical price**
844
+ with bands at ±`multiplier` × **ATR**. The variant is pinned: this is the
845
+ **modern** form (Chester Keltner via Linda Raschke, and ChartIQ's default) —
846
+ EMA(20) of typical price ± 2 × ATR(10). Keltner's 1960 original used ±1 ×
847
+ the SMA of the **plain** high−low range; the half-width here is always
848
+ _true_ range, which is a documented delta rather than an option.
849
+ Warm-up is **per column** (the `macd` rule): the centre from the MA's own
850
+ first bar, the bands from `max(centre, ATR)`.
851
+ - **`atrBands({ period = 14, multiplier = 2, column = 'close', high?, low?, close?, prefix = 'atrb' })`**
852
+ → `atrbUpper` / `atrbLower`. **Two** columns, not three: the middle is
853
+ `column` itself, already on the series. `column` (what the bands are drawn
854
+ around) is separate from `close` (what the ATR is measured from), so the
855
+ bands can sit on an `sma` while the volatility still comes off the raw bars.
856
+ `atrbUpper === column + multiplier × atr()` **bit-for-bit** — both call one
857
+ shared `atrValues` kernel — and a test pins it against the shipped `atr`.
858
+ - **`qstick({ period = 8, maType = 'sma', open?, close?, output = 'qstick' })`**
859
+ → `qstick`. Chande's moving average of the candle body, `close − open`; the
860
+ candlestick chart's colour, quantified, read against its zero line. The
861
+ **first study to read `open`**, which joins the named bar inputs.
862
+ - **`trix({ period = 15, signalPeriod = 9, column?, prefix = 'trix' })`** →
863
+ `trix` / `trixSignal`. The 1-bar **percent** rate of change of a
864
+ triple-smoothed EMA, plus an EMA signal (9 is ChartIQ's default; TA-Lib
865
+ returns the line alone). The line takes the prefix itself, not
866
+ `${prefix}Line`. It composes **three** EMA passes — TRIX wants `EMA³`, which
867
+ is _not_ the menu's `tema` (`3·EMA − 3·EMA² + EMA³`), and a test pins the
868
+ difference.
869
+ - **`coppock({ longPeriod = 14, shortPeriod = 11, wmaPeriod = 10, column?, output = 'coppock' })`**
870
+ → `coppock`. `WMA(ROC(14) + ROC(11), 10)`. Coppock's lengths are **months**
871
+ on a monthly index chart; the study is bar-count like every other here, so
872
+ on daily bars it is a short-horizon oscillator and not the indicator he
873
+ defined — stated on the study. The weighted average is part of the
874
+ definition, so there is no `maType` knob.
875
+
876
+ **Oracle.** `trix` is cross-checked against **TA-Lib `TRIX`**: the formula is
877
+ asserted **bit-exact on TA-Lib's own SMA seed** (2.2e-14, identical null
878
+ masks — which is what catches a log-vs-percent rate of change, a dropped
879
+ stage, or a `tema` substitution), with pond's first-sample EMA seed bounded as
880
+ a decaying transient (8.75% of scale at the first shared bar → 0.85% over the
881
+ last 20 at `period 15`, against 8.5% / 9.8% for the wrong smoothing rates).
882
+ TA-Lib has no Keltner, ATR-band, QStick or Coppock function, so those four are
883
+ pandas replications — but the ATR half-width and the rates of change reuse the
884
+ references TA-Lib already validates, so what each case adds is the assembly
885
+ and its composed warm-up, each asserted as an analytic first-valid bar. The
886
+ oracle fixture gains an **`opens`** column (the previous close pulled inside
887
+ its own bar; 30 of 80 bodies negative, all 80 distinct).
888
+
889
+ - `@pond-ts/financial`: **five price-vs-moving-average oscillators** — the
890
+ first consumers of the K2 moving-average engine. All five ship with fluent
891
+ methods, oracle cases at two parameterisations each, and property tests.
892
+ - **`priceOscillator({ fastPeriod = 12, slowPeriod = 26, maType = 'ema',
893
+ mode = 'percent', column = 'close', output = 'priceOsc' })`** — the
894
+ spread between two moving averages, as a percent of the slow one
895
+ (TA-Lib's **PPO**) or in price units (`mode: 'absolute'`, TA-Lib's
896
+ **APO**). **Percent is the default** for two reasons: the absolute form
897
+ at the default parameters is `macd()`'s line bar-for-bar (a test pins the
898
+ identity), and the percent form is the scale-invariant, cross-instrument
899
+ comparable one. `{ maType: 'sma', mode: 'absolute' }` matches TA-Lib's
900
+ `APO(matype=0)` to `7.1e-14` with an identical mask; on `'ema'` pond's
901
+ first-sample EMA seed is kept (the `macd` precedent), so the generator
902
+ proves the **formula** on TA-Lib's own SMA seed (`2.8e-14`) and bounds
903
+ the **seed transient** separately — 6.61% of scale at the first shared
904
+ bar, 0.41% at its worst over the last 20.
905
+ - **`disparityIndex({ period = 14, maType = 'sma', column, output =
906
+ 'disparity' })`** — `100·(price − MA)/MA`, scale-invariant. No TA-Lib
907
+ function; pandas replication with the analytic first-valid bar asserted.
908
+ - **`detrendedPriceOscillator({ period = 20, maType = 'sma', column,
909
+ output = 'dpo' })`** — `price[i] − MA[i − shift]` with
910
+ `shift = ⌊period/2⌋ + 1` (it **floors** on odd periods). This is the
911
+ **non-centered** alignment (TradingView's default); StockCharts'
912
+ centered `price[i − shift] − MA[i]` is a different series, not a
913
+ re-plotting of this one, and is documented as such on the study. First
914
+ value on bar `period − 1 + shift` (bar 30 at the defaults).
915
+ - **`elderRay({ period = 13, high, low, close, prefix = 'elder' })`** —
916
+ appends `elderBull` = `high − EMA(close)` and `elderBear` =
917
+ `low − EMA(close)`. Elder's definition names the EMA, so there is no
918
+ `maType` knob. No TA-Lib function; pandas replication on pond's EMA seed.
919
+ - **`awesomeOscillator({ fastPeriod = 5, slowPeriod = 34, high, low,
920
+ output = 'ao' })`** — `SMA(5) − SMA(34)` of the median price
921
+ `(high + low)/2`. Both legs wait for that many finite **values** (the
922
+ derived-array rule), so a gap masks the windows containing it. No TA-Lib
923
+ function; pandas replication.
924
+
925
+ Internally this adds one kernel helper, `medianPriceValues(high, low)`,
926
+ beside `typicalPriceValues` — 7.6 ms at 1M bars, and named consumers next
927
+ (Alligator, Gator, High-Low Bands). It is not a public export.
928
+
929
+ - `@pond-ts/financial`: **the K2 moving-average engine** — one **MA-type
930
+ vocabulary** shared by every study that exposes a "MA Type" input (~25 of
931
+ them in the corpus assessment). New study **`movingAverage({ period, type =
932
+ 'sma', column = 'close', output = 'ma' })`** with fluent
933
+ `.movingAverage()`, new exported type **`MaType`** and its **`MA_TYPES`**
934
+ menu, and the raw-array kernel **`movingAverageValues(values, period,
935
+ type)`** for studies whose input is derived rather than a column (Keltner's
936
+ typical price, Coppock's WMA of two ROCs, the Price Oscillator).
937
+
938
+ Ten types: `sma`, `ema`, `wma` (linear weights), `smma` (Wilder/RMA),
939
+ `dema` (`2·EMA − EMA²`), `tema` (`3·EMA − 3·EMA² + EMA³`), `trima`
940
+ (TA-Lib's SMA-of-SMA, lengths summing to `period + 1`), `hull`
941
+ (`WMA(2·WMA(n/2) − WMA(n), round √n)`), `kama` (Kaufman, TA-Lib's fast 2 /
942
+ slow 30) and `zlema` (`EMA(2x − x[i−lag])`, `lag = ⌊(period−1)/2⌋` — it
943
+ floors, so `zlema(10)` and `zlema(11)` share a lag). Deferred, and named as
944
+ deferred: MAMA/FAMA, T3, VIDYA (needs the K6 recursion) and the
945
+ time-series/regression forecast MA (kernel K7).
946
+
947
+ **`type: 'sma'` and `type: 'ema'` are the calls `sma()` and `ema()` already
948
+ make** — routed back to `rollingValues` / `smooth('ema')`, so they are the
949
+ same doubles rather than a second implementation, and stay on their
950
+ accelerated paths. Pinned bit-for-bit by test.
951
+
952
+ **Oracle**: every type has cases at two periods. `sma` / `wma` / `trima` /
953
+ `kama` match TA-Lib's `MA(matype=…)` **exactly** (≤ 5.7e-13, identical null
954
+ masks; `kama` to `0`). `ema` / `dema` / `tema` keep **pond's first-sample
955
+ EMA seed** rather than TA-Lib's SMA seed — the `macd` precedent, so
956
+ `movingAverage({ type: 'ema' })` cannot disagree with `ema()` inside our own
957
+ package. Masks are still identical; the values are a decaying seed
958
+ transient, measured at 0.210% / 0.529% / 0.059% of scale at the first shared
959
+ bar (period 21) falling to 0.0008% / 0.0155% / 0.0144% by bar 79, and
960
+ asserted as a tail bound. `smma` / `hull` / `zlema` have no TA-Lib function
961
+ and are pandas replications with their analytic warm-up asserted (`smma`
962
+ also pinned bit-for-bit against the `wilderValues` kernel RSI and ATR run
963
+ on).
964
+
965
+ **Interior gaps differ by type, deliberately** (the Wilder asymmetry, now
966
+ stated per type on the kernel): the window types recover once the gap leaves
967
+ the window, the `ema` family skips the missing bar and carries on, and
968
+ `smma` and `kama` propagate to the end — a recursion that consumes every bar
969
+ has no state to carry across a hole. `sma` is also the one type a leading
970
+ gap does **not** shift, because it keeps `sma()`'s row-counting window.
971
+
972
+ ### Changed
973
+
974
+ - `@pond-ts/financial`: **`envelope`'s `maType` widens from `'sma' | 'ema'` to
975
+ the full `MaType` menu** and routes through the shared engine. Strictly
976
+ additive for existing callers — the `sma` and `ema` centre lines are the
977
+ same kernel calls as before, pinned bit-for-bit — and an unknown `maType`
978
+ now throws instead of silently falling back to SMA. (On the raw-array door,
979
+ `movingAverageValues(…, 'sma')` waits for `period` finite values like every
980
+ other type; the column door keeps `sma()`'s rows window.)
981
+ - `pond-ts`: `partitionBy(column, options)` now throws a `TypeError` on an
982
+ unknown option key (e.g. `maxPartitions`, which was never implemented)
983
+ instead of silently ignoring it. Only JS callers passing a key the type
984
+ rejects are affected; the error names the known keys ([PND-LIVFIX]).
985
+
986
+ ### Fixed
987
+
988
+ - `pond-ts` live layer, the [PND-LIVFIX] cluster — the five confirmed
989
+ wrong-answer defects from the 2026-06 audits, each now pinned by a test:
990
+ - **Listener errors are isolated.** A `LiveSeries` / `LiveView` listener
991
+ that throws no longer aborts the push: every row still commits, the
992
+ remaining listeners still run, retention still runs and `'batch'` /
993
+ `'evict'` still fire; the first error is rethrown to the caller once
994
+ the push has completed. Before, a throw skipped retention (the buffer
995
+ sat over `maxEvents` until the next push) and left every later
996
+ subscriber — a derived `filter()` view included — permanently out of
997
+ sync.
998
+ - **Re-entrancy.** A listener may subscribe or unsubscribe during
999
+ dispatch without disturbing the fan-out (a listener added mid-event
1000
+ does not fire for that event), and a `push` made from inside a
1001
+ listener is queued and delivered after the current push, in order.
1002
+ Before, a re-entrant push emitted out of order, was spuriously
1003
+ rejected as out-of-order on the chunked backing, and broke an attached
1004
+ view with an error that read `[object Object]` (keys in that message
1005
+ now print as ISO timestamps).
1006
+ - **Quiet partitions evict by age.** Per-partition `maxAge` was
1007
+ push-driven, so a partition that stopped receiving events kept them
1008
+ forever. `partitionBy` now sweeps every partition against the source
1009
+ watermark as it advances (throttled to `maxAge / 8`), emitting
1010
+ `'evict'` as push-driven retention does.
1011
+ - **Chain-aware `LiveView.dispose()`.** Disposing the last view of
1012
+ `live.filter(p).map(f)` now disposes the unreachable intermediate too
1013
+ (a source view left with no subscribers is torn down; one with another
1014
+ subscriber is not). `dispose()` is idempotent and documented.
1015
+ - **`LiveReduce` windowed reducers on a `reorder` source with
1016
+ retention.** `min` / `max` / `first` / `last` reported stale or
1017
+ `undefined` values because their sliding-window state assumed
1018
+ oldest-arrived-first eviction; over a `reorder` source they now use
1019
+ removal-by-any-index structures (exact in any eviction order, O(n)
1020
+ per update, selected only on that source shape).
1021
+
1022
+ ## [0.65.0] — 2026-09-06
1023
+
1024
+ ### Added
1025
+
1026
+ - `@pond-ts/financial`: **`stochastic(...)`**, **`williamsR(...)`** and
1027
+ **`donchian(...)`** — the three range-position studies, built on one new
1028
+ kernel (`highestLowestValues`: the trailing highest high / lowest low in a
1029
+ single `rolling` scan) plus fluent `.stochastic()` / `.williamsR()` /
1030
+ `.donchian()`. All name their bar inputs per column (`high` / `low` /
1031
+ `close`, defaulting to `DEFAULT_OHLCV`), the `atr` shape.
1032
+
1033
+ **`stochastic`** — `{ kPeriod = 14, slowing = 3, dPeriod = 3, prefix =
1034
+ 'stoch' }` appending `stochK` / `stochD`: the slow stochastic, with
1035
+ `slowing: 1` giving the fast one (TA-Lib's `STOCHF`) from the same
1036
+ function. Values match TA-Lib's `STOCH`/`STOCHF` to `5.7e-14` on every bar
1037
+ TA-Lib emits, `%D` with an identical warm-up. Two deliberate deltas,
1038
+ both documented on the study: **`%K` starts when it can** (bar 15 at the
1039
+ defaults) where TA-Lib masks it back to `%D`'s first bar (17), discarding
1040
+ `dPeriod − 1` real values — the `macd` precedent; and **a flat window
1041
+ (`HH === LL`) is `undefined`** where TA-Lib reports `0`, which is also its
1042
+ value for "close at the very bottom of a real range" — the `rsi` precedent.
1043
+ The smoothing uses a new raw-array kernel, `rollingMeanValues`, that waits
1044
+ for `slowing` _values_ rather than rows: a scratch-column SMA would have
1045
+ put a "3-bar" `%K` on bar 13 with one value in it.
1046
+
1047
+ **`williamsR`** — `{ period = 14, output = 'williamsR' }`, bounded
1048
+ `−100..0`. Matches TA-Lib's `WILLR` bar-for-bar to `1.4e-14` with an
1049
+ identical warm-up; the only delta is the same flat-window `undefined`.
1050
+ It is fast `%K − 100`, computed on the same kernel, and a test pins that
1051
+ identity against `stochastic({ slowing: 1 })`.
1052
+
1053
+ **`donchian`** — `{ period = 20, prefix = 'dc' }` appending `dcUpper` /
1054
+ `dcLower` / `dcMiddle`. `upper` is exactly `rollingMax(high)` and `lower`
1055
+ exactly `rollingMin(low)` (pinned), in one scan plus the midpoint; pandas
1056
+ oracle only (TA-Lib has no Donchian). Scales linearly with price, like
1057
+ `atr`; a missing `high`/`low` is skipped (core's reducer policy), not
1058
+ propagated.
1059
+
1060
+ - `@pond-ts/financial`: **`obv(...)`** and **`vwap(...)`** — the first two
1061
+ **volume** studies, plus fluent `.obv()` / `.vwap()`. Both name their inputs
1062
+ per column (`close` / `volume`, and `high` / `low` for VWAP), each
1063
+ defaulting to its `DEFAULT_OHLCV` name, the rule `atr` established.
1064
+
1065
+ **`obv`** is Granville's On-Balance Volume — the running total of each bar's
1066
+ volume signed by its close change — and is the first study with **no
1067
+ `period`**: it is read for its shape, not its level, so there is nothing to
1068
+ size a window over. It is **TA-Lib's OBV exactly**, including the seed
1069
+ (`OBV[0] = volume[0]`) and an unchanged close adding nothing; the oracle
1070
+ asserts equality, with an identical (empty) warm-up mask. On **interior
1071
+ gaps** it deliberately departs from TA-Lib: a running sum has no local
1072
+ answer for a missing term, so a missing close or volume leaves the level
1073
+ `undefined` from that bar on (the same asymmetry the Wilder recursion has —
1074
+ a leading gap shifts the seed, an interior one propagates). TA-Lib has no
1075
+ gap semantics; measured on a seven-bar series it is 400 out of level at
1076
+ the gap bar and 100 out from then on, presented as a value.
1077
+
1078
+ **`vwap`** is a **rolling** Volume-Weighted Average Price:
1079
+ `Σ typicalPrice · volume / Σ volume` over `period` bars, typical price
1080
+ `(high + low + close) / 3`. There is no TA-Lib VWAP and no single
1081
+ definition, so this is a documented design choice: the rolling form fits
1082
+ the package's count-window shape, and the intraday desk's _anchored_ VWAP
1083
+ is not a special case of it (a count window emits only once it spans
1084
+ `period` rows) — that one needs a session reset and is deferred to the
1085
+ session-anchored phase. `period` is **required** (no conventional length to
1086
+ default to). A window with no volume is `undefined`; a bar missing any
1087
+ input is dropped from **both** sums so the ratio is never biased. Verified
1088
+ against a pandas replication on a fixture whose volume spikes make a
1089
+ dropped weighting visible (≥ 1.24 price units from the plain mean).
1090
+
1091
+ The oracle fixture gains a `volumes` input, and four kernels land under
1092
+ them for the studies that follow: `cumulativeValues` (A/D line, PVT),
1093
+ `signedVolumeValues`, `typicalPriceValues` (CCI, MFI, Keltner) and
1094
+ `rollingWeightedMeanValues` (VWMA).
1095
+
1096
+ - `@pond-ts/financial`: **`atr(...)`** — Wilder's Average True Range, the third
1097
+ [PND-STUDY] named indicator, plus a fluent `.atr()`. `{ period = 14,
1098
+ high = 'high', low = 'low', close = 'close', output = 'atr' }`. Verified
1099
+ against **TA-Lib** bar-for-bar in the oracle — exact agreement at both
1100
+ periods tested, with identical warm-up.
1101
+
1102
+ **The first multi-input study.** Where every study so far took a single
1103
+ `column`, ATR names each of its three inputs, each defaulting to its
1104
+ `DEFAULT_OHLCV` name — the same "never hard-code a column" rule applied per
1105
+ input rather than a new mechanism. Because the inputs are columns of one
1106
+ series they are aligned by construction, so the mismatched-length error
1107
+ array-based libraries must check for at every call is unrepresentable here.
1108
+
1109
+ Warm-up is `period` rows, not `period − 1`: true range needs a previous
1110
+ close, so bar 0 has none.
1111
+
1112
+ - `@pond-ts/financial`: **`macd(...)`** — Moving Average Convergence/Divergence,
1113
+ the second [PND-STUDY] named indicator. `{ fastPeriod = 12, slowPeriod = 26,
1114
+ signalPeriod = 9, column = 'close', prefix = 'macd' }`, appending
1115
+ `macdLine` / `macdSignal` / `macdHist`, plus a fluent `.macd()`.
1116
+
1117
+ Two deliberate deltas from TA-Lib, both documented on the study:
1118
+ **each column warms up when it can** (the line at bar 25, signal and
1119
+ histogram at 33) where TA-Lib masks all three back to 33, keeping eight real
1120
+ values it discards; and the EMAs are **pond's own** (`α = 2/(span+1)` seeded
1121
+ on the first sample — what `ema()` already ships) rather than TA-Lib's
1122
+ SMA-seeded ones. The second is a _seed_ difference, so it decays: measured
1123
+ at 3.8% of the line's magnitude at the first shared bar, 0.089% by bar 79.
1124
+ Seeding TA-Lib's way would make `macd()` disagree with
1125
+ `ema(fast) − ema(slow)` inside this package, which is the worse surprise —
1126
+ and is pinned by a test.
1127
+
1128
+ - `@pond-ts/financial`: **`rsi(...)`** — Wilder's Relative Strength Index, the
1129
+ first of the [PND-STUDY] named indicators. `{ period = 14, column = 'close',
1130
+ output = 'rsi' }`, length-preserving warm-up (`period` rows, not `period − 1`
1131
+ — RSI averages _differences_, so it needs one extra bar), and a fluent
1132
+ `.rsi()`. Verified against **TA-Lib** bar-for-bar in the oracle fixture,
1133
+ agreeing to `1.4e-14`.
1134
+
1135
+ Worth knowing which RSI this is: the gain/loss averages are seeded on the
1136
+ arithmetic mean of the first `period` differences, then carried by Wilder's
1137
+ recursion — TA-Lib's definition and Wilder's original. Implementations that
1138
+ instead smooth with a plain first-sample-seeded EMA of the same `α = 1/period`
1139
+ produce a **materially different** series: on this package's own oracle input
1140
+ the two sit up to **7.03 RSI points** apart, and are still `0.15` apart 65
1141
+ bars later. That is enough to move a reading across the conventional 70/30
1142
+ thresholds, so it is a definition choice rather than a rounding detail.
1143
+
1144
+ - `@pond-ts/financial`: **`momentum(...)`** — the absolute `period`-bar
1145
+ difference, `value − value[i − period]`. `{ period = 10, column = 'close',
1146
+ output = 'momentum' }`, warm-up `period` rows, plus a fluent `.momentum()`.
1147
+ Verified against **TA-Lib's `MOM`** bar-for-bar in the oracle: exact
1148
+ agreement with identical warm-up masks. The additive companion to
1149
+ `percentChange`'s ratio; it is in the price's units and scales with them
1150
+ (pinned by a property test).
1151
+
1152
+ - `@pond-ts/financial`: **`historicalVolatility(...)`** — the standard
1153
+ deviation of log returns over `period` bars, annualised.
1154
+ `{ period = 20, annualize = 252, column = 'close', output = 'hv' }`, plus a
1155
+ fluent `.historicalVolatility()`. Warm-up is `period` rows, not
1156
+ `period − 1` — `period` returns need `period + 1` prices, the same
1157
+ off-by-one RSI and ATR have.
1158
+
1159
+ TA-Lib has no HV to arbitrate the conventions, so they are pinned on the
1160
+ study and in the pandas oracle (`np.log(s).diff().rolling(n).std(ddof=0)
1161
+ · √annualize`): **population σ** (`ddof = 0`, the package convention shared
1162
+ with Bollinger / `rollingStdev` / `zScore`; a sample σ is
1163
+ `√(n/(n−1))` larger — 2.6% at 20); **log returns**, not simple; annualised
1164
+ by `√annualize` with `252` the default for daily bars and an **option**
1165
+ rather than a hidden constant (`1` for per-bar σ, `252·390` for one-minute
1166
+ bars); and a **decimal** (`0.18` = 18%), the form volatility is consumed
1167
+ in. Scale-invariant, pinned by a property test. A non-positive price has
1168
+ no log return: both returns touching it are missing (explicitly, since
1169
+ `ln(−4/−5)` is a finite non-answer). A leading gap shifts the start so the
1170
+ first window still covers `period` real returns, matching pandas'
1171
+ `min_periods` and the Wilder-family studies — the σ is computed by calling
1172
+ the range-exact kernel on the returns array directly, because a scratch
1173
+ column through `rollingValues` counts rows and would emit one bar early
1174
+ over one return too few.
1175
+
1176
+ - `@pond-ts/financial`: **`percentChange` is ROC** — TA-Lib's `ROC` is the same
1177
+ `(price / prevPrice − 1) × 100`, and the oracle now cross-checks the study
1178
+ against it: exact agreement (`0.0`) with identical warm-up masks at both
1179
+ periods. Documented on the study and in API.md; there is deliberately no
1180
+ separate `roc` study.
1181
+
70
1182
  ## [0.64.0] — 2026-08-28
71
1183
 
72
1184
  ### Added