oakscriptjs 0.9.4 → 0.9.6

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 (177) hide show
  1. package/dist/cjs/array/index.d.ts +88 -27
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +257 -117
  4. package/dist/cjs/array/index.js.map +3 -3
  5. package/dist/cjs/callsite/index.d.ts +122 -4
  6. package/dist/cjs/callsite/index.d.ts.map +1 -1
  7. package/dist/cjs/callsite/index.js +102 -3
  8. package/dist/cjs/callsite/index.js.map +2 -2
  9. package/dist/cjs/color/index.d.ts +7 -3
  10. package/dist/cjs/color/index.d.ts.map +1 -1
  11. package/dist/cjs/color/index.js +6 -1
  12. package/dist/cjs/color/index.js.map +2 -2
  13. package/dist/cjs/index.d.ts +1 -1
  14. package/dist/cjs/index.js +1 -1
  15. package/dist/cjs/index.js.map +1 -1
  16. package/dist/cjs/label/index.d.ts.map +1 -1
  17. package/dist/cjs/label/index.js +2 -1
  18. package/dist/cjs/label/index.js.map +2 -2
  19. package/dist/cjs/linefill/index.d.ts.map +1 -1
  20. package/dist/cjs/linefill/index.js +1 -0
  21. package/dist/cjs/linefill/index.js.map +2 -2
  22. package/dist/cjs/math/index.d.ts +79 -22
  23. package/dist/cjs/math/index.d.ts.map +1 -1
  24. package/dist/cjs/math/index.js +157 -43
  25. package/dist/cjs/math/index.js.map +3 -3
  26. package/dist/cjs/matrix/index.d.ts.map +1 -1
  27. package/dist/cjs/matrix/index.js +42 -36
  28. package/dist/cjs/matrix/index.js.map +2 -2
  29. package/dist/cjs/runtime/series.d.ts +9 -0
  30. package/dist/cjs/runtime/series.d.ts.map +1 -1
  31. package/dist/cjs/runtime/series.js +16 -2
  32. package/dist/cjs/runtime/series.js.map +2 -2
  33. package/dist/cjs/script/index.d.ts +15 -31
  34. package/dist/cjs/script/index.d.ts.map +1 -1
  35. package/dist/cjs/script/index.js +32 -0
  36. package/dist/cjs/script/index.js.map +2 -2
  37. package/dist/cjs/session/calendar.d.ts.map +1 -1
  38. package/dist/cjs/session/calendar.js +15 -2
  39. package/dist/cjs/session/calendar.js.map +2 -2
  40. package/dist/cjs/str/index.d.ts +50 -38
  41. package/dist/cjs/str/index.d.ts.map +1 -1
  42. package/dist/cjs/str/index.js +43 -15
  43. package/dist/cjs/str/index.js.map +2 -2
  44. package/dist/cjs/str/messageformat.d.ts +2 -1
  45. package/dist/cjs/str/messageformat.d.ts.map +1 -1
  46. package/dist/cjs/str/messageformat.js +4 -1
  47. package/dist/cjs/str/messageformat.js.map +2 -2
  48. package/dist/cjs/str/numberformat.d.ts +4 -0
  49. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  50. package/dist/cjs/str/numberformat.js +26 -4
  51. package/dist/cjs/str/numberformat.js.map +3 -3
  52. package/dist/cjs/ta/index.d.ts +95 -46
  53. package/dist/cjs/ta/index.d.ts.map +1 -1
  54. package/dist/cjs/ta/index.js +193 -318
  55. package/dist/cjs/ta/index.js.map +3 -3
  56. package/dist/cjs/ta/running-sum.d.ts +54 -1
  57. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  58. package/dist/cjs/ta/running-sum.js +132 -47
  59. package/dist/cjs/ta/running-sum.js.map +2 -2
  60. package/dist/cjs/ta-series.d.ts +4 -4
  61. package/dist/cjs/ta-series.d.ts.map +1 -1
  62. package/dist/cjs/ta-series.js +4 -3
  63. package/dist/cjs/ta-series.js.map +2 -2
  64. package/dist/cjs/time/datestring.d.ts +1 -0
  65. package/dist/cjs/time/datestring.d.ts.map +1 -1
  66. package/dist/cjs/time/datestring.js +7 -0
  67. package/dist/cjs/time/datestring.js.map +2 -2
  68. package/dist/cjs/time/index.d.ts.map +1 -1
  69. package/dist/cjs/time/index.js +2 -1
  70. package/dist/cjs/time/index.js.map +2 -2
  71. package/dist/cjs/time/session.d.ts +28 -9
  72. package/dist/cjs/time/session.d.ts.map +1 -1
  73. package/dist/cjs/time/session.js +74 -28
  74. package/dist/cjs/time/session.js.map +2 -2
  75. package/dist/cjs/time/timezone.d.ts +5 -0
  76. package/dist/cjs/time/timezone.d.ts.map +1 -1
  77. package/dist/cjs/time/timezone.js +15 -1
  78. package/dist/cjs/time/timezone.js.map +2 -2
  79. package/dist/esm/array/index.d.ts +88 -27
  80. package/dist/esm/array/index.d.ts.map +1 -1
  81. package/dist/esm/array/index.js +408 -157
  82. package/dist/esm/array/index.js.map +1 -1
  83. package/dist/esm/callsite/index.d.ts +122 -4
  84. package/dist/esm/callsite/index.d.ts.map +1 -1
  85. package/dist/esm/callsite/index.js +228 -8
  86. package/dist/esm/callsite/index.js.map +1 -1
  87. package/dist/esm/color/index.d.ts +7 -3
  88. package/dist/esm/color/index.d.ts.map +1 -1
  89. package/dist/esm/color/index.js +15 -3
  90. package/dist/esm/color/index.js.map +1 -1
  91. package/dist/esm/index.d.ts +1 -1
  92. package/dist/esm/index.js +1 -1
  93. package/dist/esm/label/index.d.ts.map +1 -1
  94. package/dist/esm/label/index.js +2 -1
  95. package/dist/esm/label/index.js.map +1 -1
  96. package/dist/esm/linefill/index.d.ts.map +1 -1
  97. package/dist/esm/linefill/index.js +3 -0
  98. package/dist/esm/linefill/index.js.map +1 -1
  99. package/dist/esm/math/index.d.ts +79 -22
  100. package/dist/esm/math/index.d.ts.map +1 -1
  101. package/dist/esm/math/index.js +229 -63
  102. package/dist/esm/math/index.js.map +1 -1
  103. package/dist/esm/matrix/index.d.ts.map +1 -1
  104. package/dist/esm/matrix/index.js +57 -43
  105. package/dist/esm/matrix/index.js.map +1 -1
  106. package/dist/esm/runtime/series.d.ts +9 -0
  107. package/dist/esm/runtime/series.d.ts.map +1 -1
  108. package/dist/esm/runtime/series.js +18 -1
  109. package/dist/esm/runtime/series.js.map +1 -1
  110. package/dist/esm/script/index.d.ts +15 -31
  111. package/dist/esm/script/index.d.ts.map +1 -1
  112. package/dist/esm/script/index.js +44 -1
  113. package/dist/esm/script/index.js.map +1 -1
  114. package/dist/esm/session/calendar.d.ts.map +1 -1
  115. package/dist/esm/session/calendar.js +22 -2
  116. package/dist/esm/session/calendar.js.map +1 -1
  117. package/dist/esm/str/index.d.ts +50 -38
  118. package/dist/esm/str/index.d.ts.map +1 -1
  119. package/dist/esm/str/index.js +103 -52
  120. package/dist/esm/str/index.js.map +1 -1
  121. package/dist/esm/str/messageformat.d.ts +2 -1
  122. package/dist/esm/str/messageformat.d.ts.map +1 -1
  123. package/dist/esm/str/messageformat.js +7 -2
  124. package/dist/esm/str/messageformat.js.map +1 -1
  125. package/dist/esm/str/numberformat.d.ts +4 -0
  126. package/dist/esm/str/numberformat.d.ts.map +1 -1
  127. package/dist/esm/str/numberformat.js +38 -4
  128. package/dist/esm/str/numberformat.js.map +1 -1
  129. package/dist/esm/ta/index.d.ts +95 -46
  130. package/dist/esm/ta/index.d.ts.map +1 -1
  131. package/dist/esm/ta/index.js +341 -414
  132. package/dist/esm/ta/index.js.map +1 -1
  133. package/dist/esm/ta/running-sum.d.ts +54 -1
  134. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  135. package/dist/esm/ta/running-sum.js +166 -60
  136. package/dist/esm/ta/running-sum.js.map +1 -1
  137. package/dist/esm/ta-series.d.ts +4 -4
  138. package/dist/esm/ta-series.d.ts.map +1 -1
  139. package/dist/esm/ta-series.js +5 -4
  140. package/dist/esm/ta-series.js.map +1 -1
  141. package/dist/esm/time/datestring.d.ts +1 -0
  142. package/dist/esm/time/datestring.d.ts.map +1 -1
  143. package/dist/esm/time/datestring.js +12 -0
  144. package/dist/esm/time/datestring.js.map +1 -1
  145. package/dist/esm/time/index.d.ts.map +1 -1
  146. package/dist/esm/time/index.js +3 -1
  147. package/dist/esm/time/index.js.map +1 -1
  148. package/dist/esm/time/session.d.ts +28 -9
  149. package/dist/esm/time/session.d.ts.map +1 -1
  150. package/dist/esm/time/session.js +113 -42
  151. package/dist/esm/time/session.js.map +1 -1
  152. package/dist/esm/time/timezone.d.ts +5 -0
  153. package/dist/esm/time/timezone.d.ts.map +1 -1
  154. package/dist/esm/time/timezone.js +21 -1
  155. package/dist/esm/time/timezone.js.map +1 -1
  156. package/package.json +2 -2
  157. package/src/array/index.ts +382 -174
  158. package/src/callsite/index.ts +232 -8
  159. package/src/color/index.ts +15 -5
  160. package/src/index.ts +1 -1
  161. package/src/label/index.ts +2 -1
  162. package/src/linefill/index.ts +2 -0
  163. package/src/math/index.ts +261 -77
  164. package/src/matrix/index.ts +52 -52
  165. package/src/runtime/series.ts +22 -1
  166. package/src/script/index.ts +53 -1
  167. package/src/session/calendar.ts +22 -2
  168. package/src/str/index.ts +95 -51
  169. package/src/str/messageformat.ts +7 -2
  170. package/src/str/numberformat.ts +39 -4
  171. package/src/ta/index.ts +356 -458
  172. package/src/ta/running-sum.ts +187 -62
  173. package/src/ta-series.ts +8 -7
  174. package/src/time/datestring.ts +10 -0
  175. package/src/time/index.ts +3 -1
  176. package/src/time/session.ts +97 -47
  177. package/src/time/timezone.ts +21 -1
package/src/ta/index.ts CHANGED
@@ -10,13 +10,13 @@
10
10
 
11
11
  import type { series_float, series_bool, series_int, int, Source, simple_int, simple_float, simple_bool } from '../types/index.js';
12
12
  import { eq, ge, gt, le, lt } from '../compare/index.js';
13
- import { runningSum, runningVariance } from './running-sum.js';
13
+ import { runningSum, runningSumSeries, runningVariance, runningVarianceSeries } from './running-sum.js';
14
14
 
15
15
  /**
16
16
  * Simple Moving Average - returns the moving average (sum of last y values divided by y).
17
17
  *
18
18
  * @param source - Series of values to process
19
- * @param length - Number of bars (length)
19
+ * @param length - Number of bars (length), or one length per bar (a series length)
20
20
  * @returns Simple moving average of source for length bars back
21
21
  *
22
22
  * @remarks
@@ -25,30 +25,26 @@ import { runningSum, runningVariance } from './running-sum.js';
25
25
  * - Returns NaN until `length` non-`na` values are available
26
26
  * - The sum is a compensated running sum, as in PineScript, so the result can differ from the exact window mean in
27
27
  * the last bits (bit for bit with PineScript)
28
+ * - With a series length, the running sum is moved to the new length when the length changes, as PineScript does
29
+ * (see {@link math.sum}); a bar with an `na` source gives the mean of the last `length` earlier values
28
30
  *
29
31
  * @example
30
32
  * ```typescript
31
33
  * const closePrices = [10, 11, 12, 13, 14];
32
34
  * const sma5 = ta.sma(closePrices, 5); // Returns: [NaN, NaN, NaN, NaN, 12]
35
+ * const smaVar = ta.sma(closePrices, [2, 2, 2, 3, 2]); // Returns: [NaN, 10.5, 11.5, 12, 13.5]
33
36
  * ```
34
37
  */
35
- export function sma(source: Source, length: simple_int): series_float {
38
+ export function sma(source: Source, length: simple_int | ArrayLike<number>): series_float {
39
+ if (typeof length !== 'number') {
40
+ const sums = runningSumSeries(source, length, 'ta.sma');
41
+ return sums.map((sum, i) => sum / Math.floor(length[i]!));
42
+ }
36
43
  // Floor the length to match PineScript's auto-truncation of float to int
37
44
  const len = Math.floor(length);
38
45
  return runningSum(source, len).map((sum) => sum / len);
39
46
  }
40
47
 
41
- /** Mean of the last `length` bars; na when the window holds an na value (used by ta.dev). */
42
- function strictWindowMean(source: Source, length: simple_int): series_float {
43
- const len = Math.floor(length);
44
- return Array.from({ length: source.length }, (_, i) => {
45
- if (i < len - 1) return NaN;
46
- let sum = 0;
47
- for (let j = 0; j < len; j++) sum += source[i - j]!;
48
- return sum / len;
49
- });
50
- }
51
-
52
48
  /**
53
49
  * Exponential Moving Average - returns the exponentially weighted moving average.
54
50
  *
@@ -74,26 +70,21 @@ export function ema(source: Source, length: simple_int): series_float {
74
70
  const len = Math.floor(length);
75
71
  const multiplier = 2 / (len + 1);
76
72
 
77
- // SMA seed: slide past leading NaN, then collect `len` valid values.
78
- // An infinite value is na, as in PineScript.
73
+ // SMA seed on the bar of the `len`-th valid value. An infinite value is na, as in PineScript.
79
74
  let firstValidIndex = -1;
80
75
  let validCount = 0;
81
- let initSum = 0;
82
76
 
83
77
  for (let i = 0; i < source.length; i++) {
84
78
  const val = source[i];
85
- if (val !== undefined && Number.isFinite(val)) {
86
- initSum += val;
87
- validCount++;
88
- if (validCount === len) {
89
- firstValidIndex = i;
90
- break;
91
- }
79
+ if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
80
+ firstValidIndex = i;
81
+ break;
92
82
  }
93
83
  }
94
84
 
95
- let emaValue = validCount > 0 ? initSum / validCount : NaN;
96
85
  const emaInitialized = firstValidIndex >= 0;
86
+ // The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
87
+ let emaValue = emaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex]! / len : NaN;
97
88
 
98
89
  for (let i = 0; i < source.length; i++) {
99
90
  if (!emaInitialized || i < firstValidIndex) {
@@ -261,7 +252,7 @@ export function bb(
261
252
  * Standard deviation over the last `length` non-na values.
262
253
  *
263
254
  * @param source - Series of values to process
264
- * @param length - Number of values
255
+ * @param length - Number of values, or one length per bar (a series length, see {@link sma})
265
256
  * @param biased - true (default): biased estimate (divides by `length`); false: unbiased (by `length - 1`)
266
257
  * @returns Standard deviation series (na until `length` non-na values exist)
267
258
  *
@@ -271,8 +262,12 @@ export function bb(
271
262
  * The result is the square root of {@link variance} (0 when the variance is not positive), computed from
272
263
  * compensated running sums of the values and of their squares, bit for bit as PineScript.
273
264
  */
274
- export function stdev(source: Source, length: simple_int, biased: simple_bool = true): series_float {
275
- return variance(source, length, biased).map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
265
+ export function stdev(
266
+ source: Source,
267
+ length: simple_int | ArrayLike<number>,
268
+ biased: simple_bool = true
269
+ ): series_float {
270
+ return varianceOf(source, length, biased, 'ta.stdev').map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
276
271
  }
277
272
 
278
273
  /**
@@ -350,6 +345,32 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
350
345
  });
351
346
  }
352
347
 
348
+ /**
349
+ * Compares each bar with the last earlier bar where both values were not na (PineScript rule for ta.crossover,
350
+ * ta.crossunder and ta.cross): false while there is no such bar, and false on a bar with an na value.
351
+ *
352
+ * @internal
353
+ */
354
+ function crossTest(
355
+ series1: Source,
356
+ series2: Source,
357
+ test: (a: number, b: number, prevA: number, prevB: number) => boolean,
358
+ ): series_bool {
359
+ const result: series_bool = [];
360
+ let prevA = NaN;
361
+ let prevB = NaN;
362
+ for (let i = 0; i < series1.length; i++) {
363
+ const a = series1[i]!;
364
+ const b = series2[i]!;
365
+ result.push(test(a, b, prevA, prevB));
366
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
367
+ prevA = a;
368
+ prevB = b;
369
+ }
370
+ }
371
+ return result;
372
+ }
373
+
353
374
  /**
354
375
  * Crossover - returns true when series1 crosses over series2 (moves from below to above).
355
376
  *
@@ -358,8 +379,10 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
358
379
  * @returns Boolean series (true at crossover points)
359
380
  *
360
381
  * @remarks
361
- * - True when: series1[i]! > series2[i]! AND series1[i-1] <= series2[i-1]
382
+ * - True when: series1[i] > series2[i] AND series1[j] <= series2[j], where j is the last bar before i where both
383
+ * values were not na (PineScript rule: a crossing right after an na bar is not missed)
362
384
  * - First value is always false (no previous value to compare)
385
+ * - Comparisons are exact (no tolerance)
363
386
  * - Useful for detecting bullish signals (e.g., fast MA crossing over slow MA)
364
387
  *
365
388
  * @example
@@ -368,17 +391,7 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
368
391
  * ```
369
392
  */
370
393
  export function crossover(series1: Source, series2: Source): series_bool {
371
- const result: series_bool = [];
372
-
373
- for (let i = 0; i < series1.length; i++) {
374
- if (i === 0) {
375
- result.push(false);
376
- } else {
377
- result.push(series1[i]! > series2[i]! && series1[i - 1]! <= series2[i - 1]!);
378
- }
379
- }
380
-
381
- return result;
394
+ return crossTest(series1, series2, (a, b, pa, pb) => a > b && pa <= pb);
382
395
  }
383
396
 
384
397
  /**
@@ -389,8 +402,10 @@ export function crossover(series1: Source, series2: Source): series_bool {
389
402
  * @returns Boolean series (true at crossunder points)
390
403
  *
391
404
  * @remarks
392
- * - True when: series1[i]! < series2[i]! AND series1[i-1] >= series2[i-1]
405
+ * - True when: series1[i] < series2[i] AND series1[j] >= series2[j], where j is the last bar before i where both
406
+ * values were not na
393
407
  * - First value is always false (no previous value to compare)
408
+ * - Comparisons are exact (no tolerance)
394
409
  * - Useful for detecting bearish signals (e.g., fast MA crossing under slow MA)
395
410
  *
396
411
  * @example
@@ -399,17 +414,7 @@ export function crossover(series1: Source, series2: Source): series_bool {
399
414
  * ```
400
415
  */
401
416
  export function crossunder(series1: Source, series2: Source): series_bool {
402
- const result: series_bool = [];
403
-
404
- for (let i = 0; i < series1.length; i++) {
405
- if (i === 0) {
406
- result.push(false);
407
- } else {
408
- result.push(series1[i]! < series2[i]! && series1[i - 1]! >= series2[i - 1]!);
409
- }
410
- }
411
-
412
- return result;
417
+ return crossTest(series1, series2, (a, b, pa, pb) => a < b && pa >= pb);
413
418
  }
414
419
 
415
420
  /**
@@ -672,7 +677,8 @@ export function supertrend(
672
677
  * @remarks
673
678
  * - Moving average used in RSI calculation
674
679
  * - Alpha = 1 / length (different from EMA which uses alpha = 2 / (length + 1))
675
- * - First value is initialized with SMA, then uses exponential smoothing
680
+ * - First value is initialized with SMA, then uses exponential smoothing. As in PineScript, the seed is the
681
+ * `ta.sma` value (its compensated running sum), so it can differ from the exact mean in the last bits
676
682
  * - Formula: `RMA = (source + (length - 1) * RMA[1]) / length`, evaluated in this order as in PineScript
677
683
  * (equal to `alpha * source + (1 - alpha) * RMA[1]`, but bit for bit)
678
684
  * - `na` values (also +/-Infinity) in the source series are ignored
@@ -695,23 +701,18 @@ export function rma(source: Source, length: simple_int): series_float {
695
701
  // An infinite value is na, as in PineScript.
696
702
  let firstValidIndex = -1;
697
703
  let validCount = 0;
698
- let initSum = 0;
699
704
 
700
705
  for (let i = 0; i < source.length; i++) {
701
706
  const val = source[i];
702
- if (val !== undefined && Number.isFinite(val)) {
703
- initSum += val;
704
- validCount++;
705
- if (validCount === len) {
706
- firstValidIndex = i;
707
- break;
708
- }
707
+ if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
708
+ firstValidIndex = i;
709
+ break;
709
710
  }
710
711
  }
711
712
 
712
- // Initialize RMA value with SMA of first `length` non-NaN values
713
- let rmaValue = validCount > 0 ? initSum / validCount : NaN;
714
713
  const rmaInitialized = firstValidIndex >= 0;
714
+ // The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
715
+ let rmaValue = rmaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex]! / len : NaN;
715
716
 
716
717
  for (let i = 0; i < source.length; i++) {
717
718
  if (!rmaInitialized || i < firstValidIndex) {
@@ -737,6 +738,15 @@ export function rma(source: Source, length: simple_int): series_float {
737
738
  return result;
738
739
  }
739
740
 
741
+ /**
742
+ * One step of the PineScript ta.wma sum: `a + b`, or 0 when `|a + b| <= 1e-10 * max(1, |a| + |b|)`.
743
+ * Measured bit for bit: a sum that cancels to a tiny residue (relative to the operands, or below 1e-10) is 0.
744
+ */
745
+ function wmaAdd(a: number, b: number): number {
746
+ const sum = a + b;
747
+ return Math.abs(sum) <= 1e-10 * Math.max(1, Math.abs(a) + Math.abs(b)) ? 0 : sum;
748
+ }
749
+
740
750
  /**
741
751
  * Weighted Moving Average (WMA) - moving average with linearly decreasing weights.
742
752
  *
@@ -750,6 +760,8 @@ export function rma(source: Source, length: simple_int): series_float {
750
760
  * - Formula: `sum(source[i]! * (length - i)) / sum(length - i)` for i = 0 to length-1
751
761
  * - As in PineScript: na on a bar whose source is `na` (also +/-Infinity) and until `length` non-`na` values were
752
762
  * received; an `na` value inside the window is replaced by the previous non-`na` value
763
+ * - Summed from the oldest value to the newest; as PineScript, a partial sum `s + t` with
764
+ * `|s + t| <= 1e-10 * max(1, |s| + |t|)` is 0 (a cancellation residue, or values below 1e-10)
753
765
  * - More responsive to recent price changes than SMA
754
766
  *
755
767
  * @example
@@ -784,7 +796,7 @@ export function wma(source: Source, length: simple_int): series_float {
784
796
  let weightSum = 0;
785
797
  for (let j = len - 1; j >= 0; j--) {
786
798
  const weight = len - j;
787
- sum += filled[i - j]! * weight;
799
+ sum = wmaAdd(sum, filled[i - j]! * weight);
788
800
  weightSum += weight;
789
801
  }
790
802
  result.push(sum / weightSum);
@@ -859,10 +871,12 @@ export function cum(source: Source): series_float {
859
871
  * @returns Boolean series (true at cross points)
860
872
  *
861
873
  * @remarks
862
- * - True when: (source1[i]! > source2[i]! AND source1[i-1] <= source2[i-1]) OR
863
- * (source1[i]! < source2[i]! AND source1[i-1] >= source2[i-1])
864
- * - First value is always false (no previous value to compare)
865
- * - Detects any crossing (either over or under)
874
+ * PineScript rule (measured bit for bit; not `crossover or crossunder`):
875
+ * - The call keeps the side of the last bar where `source1` was strictly above or strictly below `source2` (no
876
+ * side before such a bar). Equal bars and bars with an `na` value do not change it.
877
+ * - True when the values are strictly on the other side of the kept side: so a run of equal values is passed, and
878
+ * a series that starts equal and then moves away is not a cross
879
+ * - False on a bar with an `na` value
866
880
  * - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
867
881
  *
868
882
  * @example
@@ -873,36 +887,36 @@ export function cum(source: Source): series_float {
873
887
  */
874
888
  export function cross(source1: Source, source2: Source): series_bool {
875
889
  const result: series_bool = [];
876
-
890
+ let side = 0; // -1: last strictly below, 1: last strictly above, 0: none yet
877
891
  for (let i = 0; i < source1.length; i++) {
878
- if (i === 0) {
892
+ const a = source1[i]!;
893
+ const b = source2[i]!;
894
+ if (Number.isNaN(a) || Number.isNaN(b)) {
879
895
  result.push(false);
880
- } else {
881
- const crossedUp = source1[i]! > source2[i]! && source1[i - 1]! <= source2[i - 1]!;
882
- const crossedDown = source1[i]! < source2[i]! && source1[i - 1]! >= source2[i - 1]!;
883
- result.push(crossedUp || crossedDown);
896
+ continue;
884
897
  }
898
+ result.push((side < 0 && a > b) || (side > 0 && a < b));
899
+ if (a < b) side = -1;
900
+ else if (a > b) side = 1;
885
901
  }
886
-
887
902
  return result;
888
903
  }
889
904
 
890
905
  /**
891
- * ta.rising / ta.falling: true when each of the `length` steps between the last `length + 1` non-na values (up to
892
- * the current bar) passes `step(newer, older)`; false when there are fewer non-na values.
906
+ * ta.rising / ta.falling: a counter of the steps from one bar to the next that pass `step(newer, older)`, reset to 0
907
+ * by a step that does not pass; a step with an na value (on either bar) leaves the counter unchanged. True when the
908
+ * counter is at least `length`.
893
909
  *
894
910
  * @internal
895
911
  */
896
912
  function monotonic(source: Source, length: simple_int, step: (newer: number, older: number) => boolean): series_bool {
897
913
  const result: series_bool = [];
898
- const values: number[] = []; // non-na values so far
914
+ let count = 0;
899
915
  for (let i = 0; i < source.length; i++) {
900
916
  const v = source[i]!;
901
- if (!Number.isNaN(v)) values.push(v);
902
- const n = values.length;
903
- let ok = n > length;
904
- for (let j = 1; ok && j <= length; j++) ok = step(values[n - j]!, values[n - j - 1]!);
905
- result.push(ok);
917
+ const prev = i > 0 ? source[i - 1]! : NaN;
918
+ if (!Number.isNaN(v) && !Number.isNaN(prev)) count = step(v, prev) ? count + 1 : 0;
919
+ result.push(count >= length);
906
920
  }
907
921
  return result;
908
922
  }
@@ -916,10 +930,11 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
916
930
  *
917
931
  * @remarks
918
932
  * PineScript rules:
919
- * - each of the `length` steps is a rise larger than the 1e-10 tolerance
920
- * - `na` values are skipped: the steps are between the last `length + 1` non-`na` values up to the current bar
921
- * (on an `na` bar, the result of the last non-`na` bar)
922
- * - false while there are fewer than `length + 1` non-`na` values
933
+ * - a counter of rises from the previous bar (a rise is larger than the 1e-10 tolerance); a step that is not a rise
934
+ * resets it to 0
935
+ * - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
936
+ * result of the bar before the `na`)
937
+ * - true when the counter is at least `length` (false at the start)
923
938
  *
924
939
  * @example
925
940
  * ```typescript
@@ -940,10 +955,11 @@ export function rising(source: Source, length: simple_int): series_bool {
940
955
  *
941
956
  * @remarks
942
957
  * PineScript rules:
943
- * - each of the `length` steps is a fall larger than the 1e-10 tolerance
944
- * - `na` values are skipped: the steps are between the last `length + 1` non-`na` values up to the current bar
945
- * (on an `na` bar, the result of the last non-`na` bar)
946
- * - false while there are fewer than `length + 1` non-`na` values
958
+ * - a counter of falls from the previous bar (a fall is larger than the 1e-10 tolerance); a step that is not a fall
959
+ * resets it to 0
960
+ * - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
961
+ * result of the bar before the `na`)
962
+ * - true when the counter is at least `length` (false at the start)
947
963
  *
948
964
  * @example
949
965
  * ```typescript
@@ -1043,10 +1059,11 @@ export function mom(source: Source, length: simple_int): series_float {
1043
1059
  *
1044
1060
  * @remarks
1045
1061
  * - Measures average absolute distance from the mean
1046
- * - Formula: `sum(abs(source[i]! - sma)) / length` for i in 0 to length-1
1047
1062
  * - Less sensitive to outliers than standard deviation
1048
- * - `na` values in the source series are ignored
1049
- * - The function calculates on the `length` quantity of non-`na` values
1063
+ * - PineScript rule (bit for bit): `sum(abs(source[i] - mean)) / length` for `i = 0 .. length - 1`, added newest
1064
+ * first, with `mean = ta.sma(source, length)` (its compensated running sum, so a flat window can give a tiny
1065
+ * non-zero deviation)
1066
+ * - The loop reads the last `length` bars: an `na` among them gives `na`
1050
1067
  *
1051
1068
  * @example
1052
1069
  * ```typescript
@@ -1055,25 +1072,13 @@ export function mom(source: Source, length: simple_int): series_float {
1055
1072
  * ```
1056
1073
  */
1057
1074
  export function dev(source: Source, length: simple_int): series_float {
1058
- const result: series_float = [];
1059
- // PineScript: na when the window of `length` bars holds an na value
1060
- const meanValues = strictWindowMean(source, length);
1061
-
1062
- for (let i = 0; i < source.length; i++) {
1063
- if (i < length - 1 || isNaN(meanValues[i]!)) {
1064
- result.push(NaN);
1065
- } else {
1066
- let sum = 0;
1067
- for (let j = 0; j < length; j++) {
1068
- if (!isNaN(source[i - j]!)) {
1069
- sum += Math.abs(source[i - j]! - meanValues[i]!);
1070
- }
1071
- }
1072
- result.push(sum / length);
1073
- }
1074
- }
1075
-
1076
- return result;
1075
+ const len = Math.floor(length);
1076
+ return sma(source, len).map((mean, i) => {
1077
+ if (i < len - 1 || Number.isNaN(mean)) return NaN;
1078
+ let sum = 0;
1079
+ for (let k = 0; k < len; k++) sum += Math.abs(source[i - k]! - mean);
1080
+ return sum / len;
1081
+ });
1077
1082
  }
1078
1083
 
1079
1084
  /**
@@ -1102,7 +1107,25 @@ export function dev(source: Source, length: simple_int): series_float {
1102
1107
  * const sampleVariance = ta.variance(closePrices, 20, false);
1103
1108
  * ```
1104
1109
  */
1105
- export function variance(source: Source, length: simple_int, biased: simple_bool = true): series_float {
1110
+ export function variance(
1111
+ source: Source,
1112
+ length: simple_int | ArrayLike<number>,
1113
+ biased: simple_bool = true
1114
+ ): series_float {
1115
+ return varianceOf(source, length, biased, 'ta.variance');
1116
+ }
1117
+
1118
+ /** {@link variance} with a fixed or a series length; `name` is used in the error messages. */
1119
+ function varianceOf(
1120
+ source: Source,
1121
+ length: simple_int | ArrayLike<number>,
1122
+ biased: boolean,
1123
+ name: string
1124
+ ): series_float {
1125
+ if (typeof length !== 'number') {
1126
+ const result = runningVarianceSeries(source, length, biased, name);
1127
+ return biased ? result : result.map((v, i) => (Math.floor(length[i]!) > 1 ? v : NaN));
1128
+ }
1106
1129
  const len = Math.floor(length);
1107
1130
  // PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
1108
1131
  if ((biased ? len : len - 1) <= 0) return source.map(() => NaN);
@@ -1174,7 +1197,7 @@ export function median(source: Source, length: simple_int): series_float {
1174
1197
  * @remarks
1175
1198
  * - Fixed length of 4 bars
1176
1199
  * - Weights: [1/6, 2/6, 2/6, 1/6] (symmetric)
1177
- * - Formula: `source[3] * 1/6 + source[2] * 2/6 + source[1] * 2/6 + source[0] * 1/6`
1200
+ * - Formula, in this order as in PineScript (bit for bit): `(source + 2 * source[1] + 2 * source[2] + source[3]) / 6`
1178
1201
  * - More weight given to middle values
1179
1202
  * - `na` values in the source series are included in calculations and will produce an `na` result
1180
1203
  * - Returns NaN for the first 3 bars
@@ -1186,27 +1209,9 @@ export function median(source: Source, length: simple_int): series_float {
1186
1209
  * ```
1187
1210
  */
1188
1211
  export function swma(source: Source): series_float {
1189
- const result: series_float = [];
1190
-
1191
- for (let i = 0; i < source.length; i++) {
1192
- if (i < 3) {
1193
- result.push(NaN);
1194
- } else {
1195
- // Check for any NaN values in the window
1196
- if (isNaN(source[i]!) || isNaN(source[i - 1]!) || isNaN(source[i - 2]!) || isNaN(source[i - 3]!)) {
1197
- result.push(NaN);
1198
- } else {
1199
- const value =
1200
- source[i - 3]! * (1 / 6) +
1201
- source[i - 2]! * (2 / 6) +
1202
- source[i - 1]! * (2 / 6) +
1203
- source[i]! * (1 / 6);
1204
- result.push(value);
1205
- }
1206
- }
1207
- }
1208
-
1209
- return result;
1212
+ return Array.from({ length: source.length }, (_, i) =>
1213
+ i < 3 ? NaN : (source[i]! + 2 * source[i - 1]! + 2 * source[i - 2]! + source[i - 3]!) / 6
1214
+ );
1210
1215
  }
1211
1216
 
1212
1217
  /**
@@ -1275,6 +1280,9 @@ export function vwma(source: Source, length: simple_int, volume?: Source): serie
1275
1280
  * @remarks
1276
1281
  * - Calculates line of best fit using least squares method
1277
1282
  * - Formula: `linreg = intercept + slope * (length - 1 - offset)`
1283
+ * - As PineScript (bit for bit): x = 1 (oldest) .. length (newest), sums from the oldest value,
1284
+ * `slope = (length * sumXY - sumX * sumY) / (length * sumX2 - sumX * sumX)`,
1285
+ * `intercept = sumY / length - slope * sumX / length + slope`
1278
1286
  * - offset=0 gives current fitted value, offset<0 gives future projection
1279
1287
  * - `na` values in the source series are included in calculations and will produce an `na` result
1280
1288
  * - Useful for trend detection and prediction
@@ -1306,27 +1314,23 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
1306
1314
  if (hasNaN) {
1307
1315
  result.push(NaN);
1308
1316
  } else {
1309
- // Calculate least squares regression
1317
+ // Least squares as PineScript: x = 1 (oldest) .. len (newest), summed from the oldest value; the intercept
1318
+ // is the fitted value at x = 1
1310
1319
  let sumX = 0;
1311
1320
  let sumY = 0;
1312
1321
  let sumXY = 0;
1313
1322
  let sumX2 = 0;
1314
-
1315
- for (let j = 0; j < len; j++) {
1316
- const x = j;
1317
- const y = source[i - (len - 1 - j)]!;
1323
+ for (let j = 0; j < len; j++) {
1324
+ const x = j + 1;
1325
+ const y = source[i - (len - 1 - j)]!;
1318
1326
  sumX += x;
1319
1327
  sumY += y;
1320
1328
  sumXY += x * y;
1321
1329
  sumX2 += x * x;
1322
1330
  }
1323
-
1324
- const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
1325
- const intercept = (sumY - slope * sumX) / len;
1326
-
1327
- // Calculate linreg value at offset
1328
- const x = len - 1 - offset;
1329
- result.push(intercept + slope * x);
1331
+ const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
1332
+ const intercept = sumY / len - (slope * sumX) / len + slope;
1333
+ result.push(intercept + slope * (len - 1 - offset));
1330
1334
  }
1331
1335
  }
1332
1336
  }
@@ -1348,8 +1352,9 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
1348
1352
  * - -1 = perfect negative correlation
1349
1353
  * - 0 = no correlation
1350
1354
  * - Measures linear relationship between two series
1351
- * - `na` values in the source series are ignored
1352
- * - The function calculates on the `length` quantity of non-`na` values
1355
+ * - As PineScript: `cov = sma(x * y) - sma(x) * sma(y)`, `r = cov / sqrt(variance(x) * variance(y))` with the
1356
+ * compensated running sums of `ta.sma` / `ta.variance` (each skips its own `na` values); `|cov| <= 1e-10` gives 0, a
1357
+ * zero variance gives `na`
1353
1358
  *
1354
1359
  * @example
1355
1360
  * ```typescript
@@ -1358,59 +1363,26 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
1358
1363
  * ```
1359
1364
  */
1360
1365
  export function correlation(source1: Source, source2: Source, length: simple_int): series_float {
1361
- const result: series_float = [];
1362
- // the non-NaN pairs of the window, newest first (reused on every bar)
1363
- const span = length > 0 ? Math.ceil(length) : 0;
1364
- const a = new Float64Array(span);
1365
- const b = new Float64Array(span);
1366
-
1367
- for (let i = 0; i < source1.length; i++) {
1368
- if (i < length - 1) {
1369
- result.push(NaN);
1370
- continue;
1371
- }
1372
- let m = 0;
1373
- for (let j = 0; j < length; j++) {
1374
- const v1 = source1[i - j]!;
1375
- const v2 = source2[i - j]!;
1376
- if (!isNaN(v1) && !isNaN(v2)) {
1377
- a[m] = v1;
1378
- b[m] = v2;
1379
- m++;
1380
- }
1381
- }
1382
- if (m === 0) {
1383
- result.push(NaN);
1384
- continue;
1385
- }
1386
-
1387
- // Calculate means
1388
- let sum1 = 0;
1389
- let sum2 = 0;
1390
- for (let k = 0; k < m; k++) {
1391
- sum1 += a[k]!;
1392
- sum2 += b[k]!;
1393
- }
1394
- const mean1 = sum1 / m;
1395
- const mean2 = sum2 / m;
1396
-
1397
- // Calculate correlation components
1398
- let numerator = 0;
1399
- let sum1Sq = 0;
1400
- let sum2Sq = 0;
1401
- for (let k = 0; k < m; k++) {
1402
- const dev1 = a[k]! - mean1;
1403
- const dev2 = b[k]! - mean2;
1404
- numerator += dev1 * dev2;
1405
- sum1Sq += dev1 * dev1;
1406
- sum2Sq += dev2 * dev2;
1407
- }
1408
-
1409
- const denominator = Math.sqrt(sum1Sq * sum2Sq);
1410
- result.push(denominator === 0 ? NaN : numerator / denominator);
1411
- }
1412
-
1413
- return result;
1366
+ // PineScript: from compensated running sums, cov = sma(x * y) - sma(x) * sma(y), r = cov / sqrt(var(x) * var(y))
1367
+ // (biased variances); each running sum skips its own na values
1368
+ const product = Array.from({ length: source1.length }, (_, i) => {
1369
+ const v1 = source1[i];
1370
+ const v2 = source2[i];
1371
+ return v1 === null || v1 === undefined || v2 === null || v2 === undefined ? NaN : v1 * v2;
1372
+ });
1373
+ const meanXY = sma(product, length);
1374
+ const meanX = sma(source1, length);
1375
+ const meanY = sma(source2, length);
1376
+ const varX = variance(source1, length);
1377
+ const varY = variance(source2, length);
1378
+ return meanXY.map((mxy, i) => {
1379
+ const cov = mxy - meanX[i]! * meanY[i]!;
1380
+ if (Number.isNaN(cov)) return NaN;
1381
+ // a covariance within 1e-10 of 0 gives 0; a zero denominator gives na (a division by zero)
1382
+ if (Math.abs(cov) <= 1e-10) return 0;
1383
+ const denominator = Math.sqrt(varX[i]! * varY[i]!);
1384
+ return denominator === 0 ? NaN : cov / denominator;
1385
+ });
1414
1386
  }
1415
1387
 
1416
1388
  /**
@@ -1428,7 +1400,7 @@ export function correlation(source1: Source, source2: Source, length: simple_int
1428
1400
  * - Useful for identifying relative strength within a period
1429
1401
  *
1430
1402
  * PineScript rules:
1431
- * - percentrank = count(source[k] <= source, k = 1..length) / length * 100
1403
+ * - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
1432
1404
  * - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
1433
1405
  * `<=` the current value
1434
1406
  * - the comparison uses the 1e-10 tolerance
@@ -1454,7 +1426,7 @@ export function percentrank(source: Source, length: simple_int): series_float {
1454
1426
  for (let j = 1; j <= length; j++) {
1455
1427
  if (le(source[i - j]!, currentValue)) countLessOrEqual++;
1456
1428
  }
1457
- result.push((countLessOrEqual / length) * 100);
1429
+ result.push((countLessOrEqual * 100) / length);
1458
1430
  }
1459
1431
 
1460
1432
  return result;
@@ -1472,10 +1444,14 @@ export function percentrank(source: Source, length: simple_int): series_float {
1472
1444
  * - Typical Price = (High + Low + Close) / 3
1473
1445
  * - Mean Deviation = Average of absolute differences from mean
1474
1446
  * - Scaled by 0.015 to provide more readable numbers
1475
- * - As in PineScript: when the mean deviation is 0 (1e-10 tolerance, a flat window), the CCI of the previous bar
1476
1447
  * - Values above +100 indicate overbought conditions
1477
1448
  * - Values below -100 indicate oversold conditions
1478
- * - \`na\` values in the source series are ignored
1449
+ *
1450
+ * PineScript rules (bit for bit):
1451
+ * - `(source - ta.sma(source, length)) / (0.015 * ta.dev(source, length))`
1452
+ * - when the mean deviation is 0 (1e-10 tolerance, a flat window), the last CCI value; 0 when there is none yet
1453
+ * (a flat first window)
1454
+ * - \`na\` when the mean or the deviation is \`na\` (an \`na\` in the last \`length\` bars)
1479
1455
  *
1480
1456
  * @example
1481
1457
  * \`\`\`typescript
@@ -1485,22 +1461,15 @@ export function percentrank(source: Source, length: simple_int): series_float {
1485
1461
  * \`\`\`
1486
1462
  */
1487
1463
  export function cci(source: Source, length: simple_int): series_float {
1488
- const result: series_float = [];
1489
1464
  const smaValues = sma(source, length);
1490
1465
  const devValues = dev(source, length);
1491
-
1492
- for (let i = 0; i < source.length; i++) {
1493
- if (isNaN(smaValues[i]!) || isNaN(devValues[i]!)) {
1494
- result.push(NaN);
1495
- } else if (eq(devValues[i]!, 0)) {
1496
- result.push(i > 0 ? result[i - 1]! : NaN);
1497
- } else {
1498
- const cci = (source[i]! - smaValues[i]!) / (0.015 * devValues[i]!);
1499
- result.push(cci);
1500
- }
1501
- }
1502
-
1503
- return result;
1466
+ let last = 0;
1467
+ return smaValues.map((mean, i) => {
1468
+ const d = devValues[i]!;
1469
+ if (Number.isNaN(mean) || Number.isNaN(d)) return NaN;
1470
+ if (!eq(d, 0)) last = (source[i]! - mean) / (0.015 * d);
1471
+ return last;
1472
+ });
1504
1473
  }
1505
1474
 
1506
1475
  /**
@@ -1517,8 +1486,8 @@ export function cci(source: Source, length: simple_int): series_float {
1517
1486
  * - Measures where close is relative to the high-low range
1518
1487
  * - Values above 80 typically indicate overbought
1519
1488
  * - Values below 20 typically indicate oversold
1520
- * - Returns NaN when range is zero (high equals low)
1521
- * - \`na\` values in the source series are ignored
1489
+ * - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range is 0, the
1490
+ * result is the previous result of the call (\`na\` until a first value)
1522
1491
  *
1523
1492
  * @example
1524
1493
  * \`\`\`typescript
@@ -1532,18 +1501,14 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
1532
1501
  const lowestValues = lowest(low, length);
1533
1502
  const highestValues = highest(high, length);
1534
1503
 
1504
+ // PineScript: an na input or a zero range keeps the previous result
1505
+ let previous = NaN;
1535
1506
  for (let i = 0; i < source.length; i++) {
1536
- if (isNaN(lowestValues[i]!) || isNaN(highestValues[i]!)) {
1537
- result.push(NaN);
1538
- } else {
1539
- const range = highestValues[i]! - lowestValues[i]!;
1540
- if (range === 0) {
1541
- result.push(NaN);
1542
- } else {
1543
- const stochValue = 100 * (source[i]! - lowestValues[i]!) / range;
1544
- result.push(stochValue);
1545
- }
1546
- }
1507
+ const value = source[i]!;
1508
+ const lo = lowestValues[i]!;
1509
+ const range = highestValues[i]! - lo;
1510
+ if (!Number.isNaN(value) && !Number.isNaN(range) && range !== 0) previous = (100 * (value - lo)) / range;
1511
+ result.push(previous);
1547
1512
  }
1548
1513
 
1549
1514
  return result;
@@ -1565,7 +1530,13 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
1565
1530
  * - Formula: 100 - (100 / (1 + Positive Money Flow / Negative Money Flow))
1566
1531
  * - Values above 80 indicate overbought
1567
1532
  * - Values below 20 indicate oversold
1568
- * - `na` values in the source series are ignored
1533
+ *
1534
+ * PineScript rules (bit for bit):
1535
+ * - the two flows are `math.sum` running sums; a sum within 1e-10 of 0 counts as 0 (the residue a running sum
1536
+ * keeps over a window of zeros)
1537
+ * - 100 when the negative flow is 0; `na` when `1 + positive / negative` is 0 (division by zero)
1538
+ * - a bar with an `na` source adds nothing to the sums and gives the previous result; the bar after it (an `na`
1539
+ * change) adds its money flow to both sums
1569
1540
  *
1570
1541
  * @example
1571
1542
  * ```typescript
@@ -1602,11 +1573,16 @@ export function mfi(source: Source, length: simple_int, volume?: Source): series
1602
1573
  const upper = runningSum(upperFlow, length);
1603
1574
  const lower = runningSum(lowerFlow, length);
1604
1575
 
1605
- return upper.map((u, i) => {
1606
- const l = lower[i]!;
1576
+ // A sum within 1e-10 of 0 (the residue of a running sum over a window of zeros) is 0
1577
+ const zero = (x: number) => (Math.abs(x) <= 1e-10 ? 0 : x);
1578
+ return upper.map((sumUp, i) => {
1579
+ const u = zero(sumUp);
1580
+ const l = zero(lower[i]!);
1607
1581
  if (Number.isNaN(u) || Number.isNaN(l)) return NaN;
1608
1582
  if (l === 0) return 100;
1609
- return 100 - 100 / (1 + u / l);
1583
+ const ratio = 1 + u / l;
1584
+ // x / 0 is na in PineScript
1585
+ return ratio === 0 ? NaN : 100 - 100 / ratio;
1610
1586
  });
1611
1587
  }
1612
1588
 
@@ -2018,10 +1994,12 @@ export function dmi(
2018
1994
  * @param source - Series of values to process
2019
1995
  * @param shortLength - Short smoothing length
2020
1996
  * @param longLength - Long smoothing length
2021
- * @returns TSI series
1997
+ * @returns TSI series, a value in [-1, 1]
2022
1998
  *
2023
1999
  * @remarks
2024
- * - TSI oscillates between +100 and -100
2000
+ * - As PineScript: `ta.ema(ta.ema(m, longLength), shortLength) / ta.ema(ta.ema(math.abs(m), longLength), shortLength)`
2001
+ * with `m = source - source[1]` (not scaled by 100)
2002
+ * - na when the denominator is 0 (a constant source), as a PineScript division by zero
2025
2003
  * - Positive values indicate bullish momentum
2026
2004
  * - Negative values indicate bearish momentum
2027
2005
  * - Crossovers of zero line can signal trend changes
@@ -2053,17 +2031,11 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
2053
2031
  const absMomentum = momentum.map(Math.abs);
2054
2032
  const smoothedAbsMomentum = ema(ema(absMomentum, longLength), shortLength);
2055
2033
 
2056
- // Calculate TSI
2057
- const result: series_float = [];
2058
- for (let i = 0; i < source.length; i++) {
2059
- if (smoothedAbsMomentum[i]! === 0) {
2060
- result.push(0);
2061
- } else {
2062
- result.push((smoothedMomentum[i]! / smoothedAbsMomentum[i]!) * 100);
2063
- }
2064
- }
2065
-
2066
- return result;
2034
+ // PineScript: a value in [-1, 1]; a division by zero is na
2035
+ return smoothedMomentum.map((m, i) => {
2036
+ const d = smoothedAbsMomentum[i]!;
2037
+ return d === 0 ? NaN : m / d;
2038
+ });
2067
2039
  }
2068
2040
 
2069
2041
  /**
@@ -2078,7 +2050,8 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
2078
2050
  * - CMO > +50: overbought conditions
2079
2051
  * - CMO < -50: oversold conditions
2080
2052
  * - Unlike RSI, CMO uses sum of gains/losses instead of averages
2081
- * - na on a flat window (every change 0), as in PineScript
2053
+ * - As in PineScript: the sums are the running sums of `math.sum` (bit for bit), and a flat window (every change 0)
2054
+ * is `0 / 0` = na
2082
2055
  * - More volatile than RSI
2083
2056
  *
2084
2057
  * @example
@@ -2089,37 +2062,20 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
2089
2062
  * ```
2090
2063
  */
2091
2064
  export function cmo(source: Source, length: simple_int): series_float {
2092
- const result: series_float = [];
2093
-
2065
+ // PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
2066
+ // sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
2067
+ // math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
2068
+ const up: number[] = [];
2069
+ const down: number[] = [];
2094
2070
  for (let i = 0; i < source.length; i++) {
2095
- if (i < length) {
2096
- result.push(NaN);
2097
- continue;
2098
- }
2099
-
2100
- let sumGains = 0;
2101
- let sumLosses = 0;
2102
-
2103
- for (let j = 0; j < length; j++) {
2104
- const change = source[i - j]! - source[i - j - 1]!;
2105
- if (change > 0) {
2106
- sumGains += change;
2107
- } else {
2108
- sumLosses += Math.abs(change);
2109
- }
2110
- }
2111
-
2112
- // PineScript: 100 * (sm1 - sm2) / (sm1 + sm2), so a flat window (0 / 0) is na
2113
- const totalMovement = sumGains + sumLosses;
2114
- if (totalMovement === 0) {
2115
- result.push(NaN);
2116
- } else {
2117
- const cmoValue = ((sumGains - sumLosses) / totalMovement) * 100;
2118
- result.push(cmoValue);
2119
- }
2071
+ const mom = i > 0 ? source[i]! - source[i - 1]! : NaN;
2072
+ up.push(mom >= 0 ? mom : 0);
2073
+ down.push(mom >= 0 ? 0 : -mom);
2120
2074
  }
2121
-
2122
- return result;
2075
+ const sm1 = runningSum(up, length);
2076
+ const sm2 = runningSum(down, length);
2077
+ // a flat window is 0 / 0 = na
2078
+ return sm1.map((s1, i) => (100 * (s1 - sm2[i]!)) / (s1 + sm2[i]!));
2123
2079
  }
2124
2080
 
2125
2081
  /**
@@ -2245,6 +2201,10 @@ export function bbw(source: Source, length: simple_int, mult: simple_float): ser
2245
2201
  * @param length - Lookback period (default: 14)
2246
2202
  * @returns Williams %R series
2247
2203
  *
2204
+ * @remarks
2205
+ * PineScript rule (bit for bit): `100 * (close - max) / (max - min)` with `max = ta.highest(high, length)` and
2206
+ * `min = ta.lowest(low, length)` (their `na` rules); `na` when the range is 0.
2207
+ *
2248
2208
  * @example
2249
2209
  * ```typescript
2250
2210
  * const wpr = ta.wpr(high, low, close, 14);
@@ -2253,34 +2213,14 @@ export function bbw(source: Source, length: simple_int, mult: simple_float): ser
2253
2213
  * ```
2254
2214
  */
2255
2215
  export function wpr(high: Source, low: Source, close: Source, length: simple_int = 14): series_float {
2256
- const result: series_float = [];
2257
-
2258
- for (let i = 0; i < close.length; i++) {
2259
- if (i < length - 1) {
2260
- result.push(NaN);
2261
- continue;
2262
- }
2263
-
2264
- // Find highest high and lowest low in the period
2265
- let highestHigh = high[i - length + 1]!;
2266
- let lowestLow = low[i - length + 1]!;
2267
-
2268
- for (let j = i - length + 2; j <= i; j++) {
2269
- if (high[j]! > highestHigh) highestHigh = high[j]!;
2270
- if (low[j]! < lowestLow) lowestLow = low[j]!;
2271
- }
2272
-
2273
- const range = highestHigh - lowestLow;
2274
- if (range === 0) {
2275
- result.push(NaN);
2276
- } else {
2277
- // Formula: (Highest High - Close) / (Highest High - Lowest Low) * -100
2278
- const wprValue = ((highestHigh! - close[i]!) / range) * -100;
2279
- result.push(wprValue);
2280
- }
2281
- }
2282
-
2283
- return result;
2216
+ const highs = highest(high, length);
2217
+ const lows = lowest(low, length);
2218
+ return Array.from({ length: close.length }, (_, i) => {
2219
+ const hmax = highs[i]!;
2220
+ const range = hmax - lows[i]!;
2221
+ // PineScript, in this order (bit for bit): 100 * (close - max) / (max - min); x / 0 is na
2222
+ return range === 0 ? NaN : (100 * (close[i]! - hmax)) / range;
2223
+ });
2284
2224
  }
2285
2225
 
2286
2226
  /**
@@ -2296,6 +2236,9 @@ export function wpr(high: Source, low: Source, close: Source, length: simple_int
2296
2236
  * @remarks
2297
2237
  * As in PineScript: the sums restart on anchor bars; the standard deviation is
2298
2238
  * `sqrt(Σ(volume × source²) / Σvolume − vwap²)`.
2239
+ * - With an anchor series: `na` before the first anchor bar
2240
+ * - A bar with an `na` source (or volume) makes the sums `na` until the next anchor bar (`na` values are not
2241
+ * skipped)
2299
2242
  */
2300
2243
  export function vwap(source: Source, volume: Source, anchor?: ArrayLike<boolean | number>): series_float;
2301
2244
  export function vwap(
@@ -2316,18 +2259,19 @@ export function vwap(
2316
2259
  const mid: series_float = [];
2317
2260
  const upper: series_float = [];
2318
2261
  const lower: series_float = [];
2319
- let sumPV = 0;
2320
- let sumV = 0;
2321
- let sumPPV = 0;
2262
+ // na until the first anchor (without an anchor series, bar 0 starts the sums)
2263
+ let sumPV = NaN;
2264
+ let sumV = NaN;
2265
+ let sumPPV = NaN;
2322
2266
  for (let i = 0; i < source.length; i++) {
2323
- if (anchor && anchor[i]) {
2324
- sumPV = 0;
2325
- sumV = 0;
2326
- sumPPV = 0;
2327
- }
2328
2267
  const s = source[i]!;
2329
2268
  const v = volume[i]!;
2330
- if (!Number.isNaN(s) && !Number.isNaN(v)) {
2269
+ if (anchor ? anchor[i] : i === 0) {
2270
+ sumPV = s * v;
2271
+ sumV = v;
2272
+ sumPPV = s * s * v;
2273
+ } else {
2274
+ // An na source or volume makes the sums na until the next anchor
2331
2275
  sumPV += s * v;
2332
2276
  sumV += v;
2333
2277
  sumPPV += s * s * v;
@@ -2409,7 +2353,8 @@ export function alma(
2409
2353
  /**
2410
2354
  * Keltner Channels Width (KCW)
2411
2355
  *
2412
- * Measures the width of Keltner Channels as a percentage of the middle line.
2356
+ * Measures the width of Keltner Channels relative to the middle line: `(upper - lower) / middle`, a ratio as in
2357
+ * PineScript (0.05 is a width of 5% of the middle line; not multiplied by 100).
2413
2358
  * Similar to BBW but uses ATR instead of standard deviation.
2414
2359
  *
2415
2360
  * Low KCW suggests consolidation/low volatility.
@@ -2447,8 +2392,8 @@ export function kcw(
2447
2392
  if (isNaN(basis[i]!) || basis[i]! === 0) {
2448
2393
  result.push(NaN);
2449
2394
  } else {
2450
- const width = ((upper[i]! - lower[i]!) / basis[i]!) * 100;
2451
- result.push(width);
2395
+ // PineScript: (upper - lower) / basis, a ratio (not a percentage)
2396
+ result.push((upper[i]! - lower[i]!) / basis[i]!);
2452
2397
  }
2453
2398
  }
2454
2399
 
@@ -2530,50 +2475,30 @@ export function min(source: Source): series_float {
2530
2475
  * The Center of Gravity indicator is an oscillator developed by John Ehlers.
2531
2476
  * It identifies turning points with minimal lag and provides clear signals.
2532
2477
  *
2533
- * The COG calculates a weighted average where more recent prices have higher weights,
2534
- * similar to a moving average but with a focus on momentum shifts.
2535
- *
2536
2478
  * @param source - Source series (typically close)
2537
2479
  * @param length - Lookback period (default: 10)
2538
2480
  * @returns COG series
2539
2481
  *
2482
+ * @remarks
2483
+ * PineScript rule (bit for bit): `-num / math.sum(source, length)`, with `num` the sum of `source[i] * (i + 1)`
2484
+ * for `i = 0 .. length - 1`, added newest first.
2485
+ * - The denominator is the compensated running sum of `math.sum` (the last `length` non-`na` values)
2486
+ * - The numerator reads the last `length` bars, so an `na` among them gives `na`
2487
+ * - A zero denominator gives `na` (PineScript division by zero)
2488
+ *
2540
2489
  * @example
2541
2490
  * ```typescript
2542
2491
  * const cogValue = ta.cog(close, 10);
2543
- * // Use COG crossovers as signals:
2544
- * // - COG crossing above 0: potential buy signal
2545
- * // - COG crossing below 0: potential sell signal
2546
2492
  * ```
2547
2493
  */
2548
2494
  export function cog(source: Source, length: simple_int = 10): series_float {
2549
- const result: series_float = [];
2550
-
2551
- for (let i = 0; i < source.length; i++) {
2552
- if (i < length - 1) {
2553
- result.push(NaN);
2554
- continue;
2555
- }
2556
-
2557
- let numerator = 0;
2558
- let denominator = 0;
2559
-
2560
- for (let j = 0; j < length; j++) {
2561
- const weight = j + 1;
2562
- const price = source[i - length + 1 + j]!;
2563
- numerator += weight * price;
2564
- denominator += price;
2565
- }
2566
-
2567
- if (denominator === 0) {
2568
- result.push(NaN);
2569
- } else {
2570
- // COG formula: -1 * (sum of (weight * price) / sum of prices) + (length + 1) / 2
2571
- const cog = -1 * (numerator / denominator) + (length + 1) / 2;
2572
- result.push(cog);
2573
- }
2574
- }
2575
-
2576
- return result;
2495
+ const len = Math.floor(length);
2496
+ return runningSum(source, len).map((sum, i) => {
2497
+ if (i < len - 1 || Number.isNaN(sum) || sum === 0) return NaN;
2498
+ let num = 0;
2499
+ for (let k = 0; k < len; k++) num += source[i - k]! * (k + 1);
2500
+ return -num / sum;
2501
+ });
2577
2502
  }
2578
2503
 
2579
2504
  /**
@@ -2593,32 +2518,25 @@ export function cog(source: Source, length: simple_int = 10): series_float {
2593
2518
  * ```
2594
2519
  *
2595
2520
  * @remarks
2596
- * - `na` values in the source series are ignored
2521
+ * PineScript rules:
2522
+ * - the window is the last `length` non-`na` values (an `na` bar does not enter it, and gets the mode of the
2523
+ * window it keeps)
2524
+ * - NaN until `length` non-`na` values exist
2597
2525
  * - If no mode exists, returns the smallest value
2598
- * - Returns NaN for the first (length - 1) values where there's insufficient data
2599
2526
  */
2600
2527
  export function mode(source: Source, length: simple_int): series_float {
2601
2528
  const result: series_float = [];
2529
+ const len = Math.floor(length);
2530
+ const seen: number[] = []; // non-na values so far
2602
2531
 
2603
2532
  for (let i = 0; i < source.length; i++) {
2604
- if (i < length - 1) {
2605
- result.push(NaN);
2606
- continue;
2607
- }
2608
-
2609
- // Collect non-NaN values in the window
2610
- const values: number[] = [];
2611
- for (let j = 0; j < length; j++) {
2612
- const value = source[i - j]!;
2613
- if (!isNaN(value)) {
2614
- values.push(value);
2615
- }
2616
- }
2617
-
2618
- if (values.length === 0) {
2533
+ const v = source[i]!;
2534
+ if (!Number.isNaN(v)) seen.push(v);
2535
+ if (seen.length < len) {
2619
2536
  result.push(NaN);
2620
2537
  continue;
2621
2538
  }
2539
+ const values = seen.slice(seen.length - len);
2622
2540
 
2623
2541
  // Count frequency of each value
2624
2542
  const frequencyMap = new Map<number, number>();
@@ -2781,95 +2699,73 @@ export function percentile_nearest_rank(
2781
2699
  * - +100 indicates source consistently increased over the period
2782
2700
  * - -100 indicates source consistently decreased over the period
2783
2701
  * - 0 indicates no directional consistency
2784
- * - Returns NaN for the first (length - 1) values where there's insufficient data
2702
+ * - As PineScript (bit for bit): the first value is on bar `length`; values less than 1e-10 above the smallest value
2703
+ * of a group (ascending sweep) are ties with their average rank; a window where all values are ties gives `na`;
2704
+ * moment-form variances of the bar positions and the ranks, `(100 * cov) / (sd_x * sd_y)`
2705
+ * - A bar with an `na` source keeps the previous result; the values are kept in a ring of `length + 1` slots by bar
2706
+ * index (0 at the start, not written on `na` bars), so a later window sees the value of `length + 1` bars earlier
2707
+ * where the `na` was
2785
2708
  */
2786
2709
  export function rci(source: Source, length: simple_int): series_float {
2710
+ const len = Math.floor(length);
2787
2711
  const result: series_float = [];
2788
-
2712
+ // PineScript keeps the values in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start
2713
+ // and written only on bars with a value: a bar with an na source keeps the previous result, and later windows see
2714
+ // the value of `length + 1` bars earlier in that slot
2715
+ const ring = new Array<number>(len + 1).fill(0);
2789
2716
  for (let i = 0; i < source.length; i++) {
2790
- if (i < length - 1) {
2791
- result.push(NaN);
2717
+ const v = source[i]!;
2718
+ if (Number.isNaN(v)) {
2719
+ result.push(i > 0 ? result[i - 1]! : NaN);
2792
2720
  continue;
2793
2721
  }
2794
-
2795
- // Collect values in the window
2796
- const values: number[] = [];
2797
- for (let j = 0; j < length; j++) {
2798
- values.push(source[i - length + 1 + j]!);
2799
- }
2800
-
2801
- // Check for NaN values
2802
- if (values.some(v => isNaN(v))) {
2722
+ ring[i % (len + 1)] = v;
2723
+ // the first value is on bar `length` (one bar after the window is full)
2724
+ if (i < len) {
2803
2725
  result.push(NaN);
2804
2726
  continue;
2805
2727
  }
2806
-
2807
- // Create array of indices with their values for ranking
2808
- const indexed = values.map((value, index) => ({ value, index }));
2809
-
2810
- // Sort by value to get ranks
2811
- const sorted = [...indexed].sort((a, b) => a.value - b.value);
2812
-
2813
- // Assign ranks (handling ties by averaging ranks)
2814
- const ranks = new Array(length).fill(0);
2815
- let currentRank = 1;
2816
- for (let j = 0; j < sorted.length; j++) {
2817
- // Count ties
2818
- let tieCount = 1;
2819
- while (j + tieCount < sorted.length && sorted[j]!.value === sorted[j + tieCount]!.value) {
2820
- tieCount++;
2821
- }
2822
-
2823
- // Average rank for ties
2824
- const avgRank = (currentRank + (currentRank + tieCount - 1)) / 2;
2825
-
2826
- // Assign average rank to all tied values
2827
- for (let k = 0; k < tieCount; k++) {
2828
- ranks[sorted[j + k]!.index] = avgRank;
2829
- }
2830
-
2831
- j += tieCount - 1;
2832
- currentRank += tieCount;
2833
- }
2834
-
2835
- // Calculate D² = sum of (price_rank - time_rank)²
2836
- let sumSquaredDiff = 0;
2837
- for (let j = 0; j < length; j++) {
2838
- const timeRank = j + 1;
2839
- const diff = ranks[j] - timeRank;
2840
- sumSquaredDiff += diff * diff;
2841
- }
2842
-
2843
- const n = length;
2844
- const base = (n * n * n - n) / 12;
2845
-
2846
- // Compute tie correction for price ranks
2847
- // Count tied groups from the sorted array
2848
- let tieCorrection = 0;
2849
- for (let j = 0; j < sorted.length; ) {
2850
- let tieCount = 1;
2851
- while (j + tieCount < sorted.length && sorted[j]!.value === sorted[j + tieCount]!.value) {
2852
- tieCount++;
2853
- }
2854
- if (tieCount > 1) {
2855
- tieCorrection += (tieCount * tieCount * tieCount - tieCount) / 12;
2856
- }
2857
- j += tieCount;
2858
- }
2859
-
2860
- const A = base - tieCorrection; // Corrected for tied price ranks
2861
- const B = base; // Time ranks have no ties
2862
-
2863
- let rho: number;
2864
- if (A === 0 || B === 0) {
2865
- rho = 0;
2866
- } else {
2867
- rho = (A + B - sumSquaredDiff) / (2 * Math.sqrt(A * B));
2728
+ // window newest first (position 0 = this bar)
2729
+ const values = Array.from({ length: len }, (_, j) => ring[(i - j) % (len + 1)]!);
2730
+ // Ranks: ascending sweep; a group collects the values less than 1e-10 above its smallest value; each group gets
2731
+ // its average 0-based rank counted from the largest value
2732
+ const order = values.map((_, j) => j).sort((a, b) => values[a]! - values[b]!);
2733
+ const ranks = new Array<number>(len);
2734
+ for (let g = 0; g < len; ) {
2735
+ const anchor = values[order[g]!]!;
2736
+ let end = g + 1;
2737
+ while (end < len && values[order[end]!]! - anchor < 1e-10) end++;
2738
+ const avgRank = len - 1 - (g + g + (end - g) - 1) / 2;
2739
+ for (let k = g; k < end; k++) ranks[order[k]!] = avgRank;
2740
+ g = end;
2741
+ }
2742
+ // moment-form variances, covariance from deviation products, (100 * cov) / (sd_x * sd_y): this operation order
2743
+ // gives PineScript's values bit for bit
2744
+ let sumX = 0;
2745
+ let sumY = 0;
2746
+ let sumX2 = 0;
2747
+ let sumY2 = 0;
2748
+ for (let k = 0; k < len; k++) {
2749
+ const y = ranks[k]!;
2750
+ sumX += k;
2751
+ sumY += y;
2752
+ sumX2 += k * k;
2753
+ sumY2 += y * y;
2754
+ }
2755
+ const meanX = sumX / len;
2756
+ const meanY = sumY / len;
2757
+ const varX = sumX2 / len - meanX * meanX;
2758
+ const varY = sumY2 / len - meanY * meanY;
2759
+ // a window where all values are ties has no correlation
2760
+ if (!(varX > 0) || !(varY > 0)) {
2761
+ result.push(NaN);
2762
+ continue;
2868
2763
  }
2869
-
2870
- result.push(rho * 100);
2764
+ let cov = 0;
2765
+ for (let k = 0; k < len; k++) cov += (k - meanX) * (ranks[k]! - meanY);
2766
+ cov /= len;
2767
+ result.push((100 * cov) / (Math.sqrt(varX) * Math.sqrt(varY)));
2871
2768
  }
2872
-
2873
2769
  return result;
2874
2770
  }
2875
2771
 
@@ -3490,16 +3386,18 @@ export function pvt(close: Source, volume: Source): series_float {
3490
3386
 
3491
3387
  /**
3492
3388
  * Accumulation/Distribution index: the running sum of
3493
- * `(2 * close - low - high) / (high - low) * volume`, with 0 on bars where `high == low`.
3389
+ * `((close - low) - (high - close)) / (high - low) * volume`, in this order as in PineScript (bit for bit).
3390
+ * A bar where this is `na` (`high == low`, an `na` volume) adds nothing and keeps the previous value.
3494
3391
  */
3495
3392
  export function accdist(high: Source, low: Source, close: Source, volume: Source): series_float {
3496
- return cum(
3497
- close.map((c, i) => {
3498
- const h = high[i]!;
3499
- const l = low[i]!;
3500
- return h === l ? 0 : ((2 * c - l - h) / (h - l)) * volume[i]!;
3501
- })
3502
- );
3393
+ let ad = 0;
3394
+ return close.map((c, i) => {
3395
+ const h = high[i]!;
3396
+ const l = low[i]!;
3397
+ const mfv = (((c - l) - (h - c)) / (h - l)) * volume[i]!;
3398
+ if (Number.isFinite(mfv)) ad += mfv;
3399
+ return ad;
3400
+ });
3503
3401
  }
3504
3402
 
3505
3403
  /**