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
@@ -8,12 +8,12 @@
8
8
  * @version 6
9
9
  */
10
10
  import { eq, ge, gt, le, lt } from '../compare/index.js';
11
- import { runningSum, runningVariance } from './running-sum.js';
11
+ import { runningSum, runningSumSeries, runningVariance, runningVarianceSeries } from './running-sum.js';
12
12
  /**
13
13
  * Simple Moving Average - returns the moving average (sum of last y values divided by y).
14
14
  *
15
15
  * @param source - Series of values to process
16
- * @param length - Number of bars (length)
16
+ * @param length - Number of bars (length), or one length per bar (a series length)
17
17
  * @returns Simple moving average of source for length bars back
18
18
  *
19
19
  * @remarks
@@ -22,30 +22,25 @@ import { runningSum, runningVariance } from './running-sum.js';
22
22
  * - Returns NaN until `length` non-`na` values are available
23
23
  * - The sum is a compensated running sum, as in PineScript, so the result can differ from the exact window mean in
24
24
  * the last bits (bit for bit with PineScript)
25
+ * - With a series length, the running sum is moved to the new length when the length changes, as PineScript does
26
+ * (see {@link math.sum}); a bar with an `na` source gives the mean of the last `length` earlier values
25
27
  *
26
28
  * @example
27
29
  * ```typescript
28
30
  * const closePrices = [10, 11, 12, 13, 14];
29
31
  * const sma5 = ta.sma(closePrices, 5); // Returns: [NaN, NaN, NaN, NaN, 12]
32
+ * const smaVar = ta.sma(closePrices, [2, 2, 2, 3, 2]); // Returns: [NaN, 10.5, 11.5, 12, 13.5]
30
33
  * ```
31
34
  */
32
35
  export function sma(source, length) {
36
+ if (typeof length !== 'number') {
37
+ const sums = runningSumSeries(source, length, 'ta.sma');
38
+ return sums.map((sum, i) => sum / Math.floor(length[i]));
39
+ }
33
40
  // Floor the length to match PineScript's auto-truncation of float to int
34
41
  const len = Math.floor(length);
35
42
  return runningSum(source, len).map((sum) => sum / len);
36
43
  }
37
- /** Mean of the last `length` bars; na when the window holds an na value (used by ta.dev). */
38
- function strictWindowMean(source, length) {
39
- const len = Math.floor(length);
40
- return Array.from({ length: source.length }, (_, i) => {
41
- if (i < len - 1)
42
- return NaN;
43
- let sum = 0;
44
- for (let j = 0; j < len; j++)
45
- sum += source[i - j];
46
- return sum / len;
47
- });
48
- }
49
44
  /**
50
45
  * Exponential Moving Average - returns the exponentially weighted moving average.
51
46
  *
@@ -70,24 +65,19 @@ export function ema(source, length) {
70
65
  const result = [];
71
66
  const len = Math.floor(length);
72
67
  const multiplier = 2 / (len + 1);
73
- // SMA seed: slide past leading NaN, then collect `len` valid values.
74
- // An infinite value is na, as in PineScript.
68
+ // SMA seed on the bar of the `len`-th valid value. An infinite value is na, as in PineScript.
75
69
  let firstValidIndex = -1;
76
70
  let validCount = 0;
77
- let initSum = 0;
78
71
  for (let i = 0; i < source.length; i++) {
79
72
  const val = source[i];
80
- if (val !== undefined && Number.isFinite(val)) {
81
- initSum += val;
82
- validCount++;
83
- if (validCount === len) {
84
- firstValidIndex = i;
85
- break;
86
- }
73
+ if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
74
+ firstValidIndex = i;
75
+ break;
87
76
  }
88
77
  }
89
- let emaValue = validCount > 0 ? initSum / validCount : NaN;
90
78
  const emaInitialized = firstValidIndex >= 0;
79
+ // The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
80
+ let emaValue = emaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex] / len : NaN;
91
81
  for (let i = 0; i < source.length; i++) {
92
82
  if (!emaInitialized || i < firstValidIndex) {
93
83
  result.push(NaN);
@@ -232,7 +222,7 @@ export function bb(series, length, mult) {
232
222
  * Standard deviation over the last `length` non-na values.
233
223
  *
234
224
  * @param source - Series of values to process
235
- * @param length - Number of values
225
+ * @param length - Number of values, or one length per bar (a series length, see {@link sma})
236
226
  * @param biased - true (default): biased estimate (divides by `length`); false: unbiased (by `length - 1`)
237
227
  * @returns Standard deviation series (na until `length` non-na values exist)
238
228
  *
@@ -243,7 +233,7 @@ export function bb(series, length, mult) {
243
233
  * compensated running sums of the values and of their squares, bit for bit as PineScript.
244
234
  */
245
235
  export function stdev(source, length, biased = true) {
246
- return variance(source, length, biased).map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
236
+ return varianceOf(source, length, biased, 'ta.stdev').map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
247
237
  }
248
238
  /**
249
239
  * The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist.
@@ -321,6 +311,27 @@ function extremeValue(source, length, lowest) {
321
311
  return v === 0 && zeros[i] ? (lowest ? -0 : 0) : v;
322
312
  });
323
313
  }
314
+ /**
315
+ * Compares each bar with the last earlier bar where both values were not na (PineScript rule for ta.crossover,
316
+ * ta.crossunder and ta.cross): false while there is no such bar, and false on a bar with an na value.
317
+ *
318
+ * @internal
319
+ */
320
+ function crossTest(series1, series2, test) {
321
+ const result = [];
322
+ let prevA = NaN;
323
+ let prevB = NaN;
324
+ for (let i = 0; i < series1.length; i++) {
325
+ const a = series1[i];
326
+ const b = series2[i];
327
+ result.push(test(a, b, prevA, prevB));
328
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
329
+ prevA = a;
330
+ prevB = b;
331
+ }
332
+ }
333
+ return result;
334
+ }
324
335
  /**
325
336
  * Crossover - returns true when series1 crosses over series2 (moves from below to above).
326
337
  *
@@ -329,8 +340,10 @@ function extremeValue(source, length, lowest) {
329
340
  * @returns Boolean series (true at crossover points)
330
341
  *
331
342
  * @remarks
332
- * - True when: series1[i]! > series2[i]! AND series1[i-1] <= series2[i-1]
343
+ * - True when: series1[i] > series2[i] AND series1[j] <= series2[j], where j is the last bar before i where both
344
+ * values were not na (PineScript rule: a crossing right after an na bar is not missed)
333
345
  * - First value is always false (no previous value to compare)
346
+ * - Comparisons are exact (no tolerance)
334
347
  * - Useful for detecting bullish signals (e.g., fast MA crossing over slow MA)
335
348
  *
336
349
  * @example
@@ -339,16 +352,7 @@ function extremeValue(source, length, lowest) {
339
352
  * ```
340
353
  */
341
354
  export function crossover(series1, series2) {
342
- const result = [];
343
- for (let i = 0; i < series1.length; i++) {
344
- if (i === 0) {
345
- result.push(false);
346
- }
347
- else {
348
- result.push(series1[i] > series2[i] && series1[i - 1] <= series2[i - 1]);
349
- }
350
- }
351
- return result;
355
+ return crossTest(series1, series2, (a, b, pa, pb) => a > b && pa <= pb);
352
356
  }
353
357
  /**
354
358
  * Crossunder - returns true when series1 crosses under series2 (moves from above to below).
@@ -358,8 +362,10 @@ export function crossover(series1, series2) {
358
362
  * @returns Boolean series (true at crossunder points)
359
363
  *
360
364
  * @remarks
361
- * - True when: series1[i]! < series2[i]! AND series1[i-1] >= series2[i-1]
365
+ * - True when: series1[i] < series2[i] AND series1[j] >= series2[j], where j is the last bar before i where both
366
+ * values were not na
362
367
  * - First value is always false (no previous value to compare)
368
+ * - Comparisons are exact (no tolerance)
363
369
  * - Useful for detecting bearish signals (e.g., fast MA crossing under slow MA)
364
370
  *
365
371
  * @example
@@ -368,16 +374,7 @@ export function crossover(series1, series2) {
368
374
  * ```
369
375
  */
370
376
  export function crossunder(series1, series2) {
371
- const result = [];
372
- for (let i = 0; i < series1.length; i++) {
373
- if (i === 0) {
374
- result.push(false);
375
- }
376
- else {
377
- result.push(series1[i] < series2[i] && series1[i - 1] >= series2[i - 1]);
378
- }
379
- }
380
- return result;
377
+ return crossTest(series1, series2, (a, b, pa, pb) => a < b && pa >= pb);
381
378
  }
382
379
  /**
383
380
  * Change - calculates the difference between the current value and its value length bars ago.
@@ -603,7 +600,8 @@ export function supertrend(factor, atrPeriod, high, low, close, wicks = false) {
603
600
  * @remarks
604
601
  * - Moving average used in RSI calculation
605
602
  * - Alpha = 1 / length (different from EMA which uses alpha = 2 / (length + 1))
606
- * - First value is initialized with SMA, then uses exponential smoothing
603
+ * - First value is initialized with SMA, then uses exponential smoothing. As in PineScript, the seed is the
604
+ * `ta.sma` value (its compensated running sum), so it can differ from the exact mean in the last bits
607
605
  * - Formula: `RMA = (source + (length - 1) * RMA[1]) / length`, evaluated in this order as in PineScript
608
606
  * (equal to `alpha * source + (1 - alpha) * RMA[1]`, but bit for bit)
609
607
  * - `na` values (also +/-Infinity) in the source series are ignored
@@ -625,21 +623,16 @@ export function rma(source, length) {
625
623
  // An infinite value is na, as in PineScript.
626
624
  let firstValidIndex = -1;
627
625
  let validCount = 0;
628
- let initSum = 0;
629
626
  for (let i = 0; i < source.length; i++) {
630
627
  const val = source[i];
631
- if (val !== undefined && Number.isFinite(val)) {
632
- initSum += val;
633
- validCount++;
634
- if (validCount === len) {
635
- firstValidIndex = i;
636
- break;
637
- }
628
+ if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
629
+ firstValidIndex = i;
630
+ break;
638
631
  }
639
632
  }
640
- // Initialize RMA value with SMA of first `length` non-NaN values
641
- let rmaValue = validCount > 0 ? initSum / validCount : NaN;
642
633
  const rmaInitialized = firstValidIndex >= 0;
634
+ // The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
635
+ let rmaValue = rmaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex] / len : NaN;
643
636
  for (let i = 0; i < source.length; i++) {
644
637
  if (!rmaInitialized || i < firstValidIndex) {
645
638
  // Not enough data yet for RMA
@@ -665,6 +658,14 @@ export function rma(source, length) {
665
658
  }
666
659
  return result;
667
660
  }
661
+ /**
662
+ * One step of the PineScript ta.wma sum: `a + b`, or 0 when `|a + b| <= 1e-10 * max(1, |a| + |b|)`.
663
+ * Measured bit for bit: a sum that cancels to a tiny residue (relative to the operands, or below 1e-10) is 0.
664
+ */
665
+ function wmaAdd(a, b) {
666
+ const sum = a + b;
667
+ return Math.abs(sum) <= 1e-10 * Math.max(1, Math.abs(a) + Math.abs(b)) ? 0 : sum;
668
+ }
668
669
  /**
669
670
  * Weighted Moving Average (WMA) - moving average with linearly decreasing weights.
670
671
  *
@@ -678,6 +679,8 @@ export function rma(source, length) {
678
679
  * - Formula: `sum(source[i]! * (length - i)) / sum(length - i)` for i = 0 to length-1
679
680
  * - As in PineScript: na on a bar whose source is `na` (also +/-Infinity) and until `length` non-`na` values were
680
681
  * received; an `na` value inside the window is replaced by the previous non-`na` value
682
+ * - Summed from the oldest value to the newest; as PineScript, a partial sum `s + t` with
683
+ * `|s + t| <= 1e-10 * max(1, |s| + |t|)` is 0 (a cancellation residue, or values below 1e-10)
681
684
  * - More responsive to recent price changes than SMA
682
685
  *
683
686
  * @example
@@ -712,7 +715,7 @@ export function wma(source, length) {
712
715
  let weightSum = 0;
713
716
  for (let j = len - 1; j >= 0; j--) {
714
717
  const weight = len - j;
715
- sum += filled[i - j] * weight;
718
+ sum = wmaAdd(sum, filled[i - j] * weight);
716
719
  weightSum += weight;
717
720
  }
718
721
  result.push(sum / weightSum);
@@ -784,10 +787,12 @@ export function cum(source) {
784
787
  * @returns Boolean series (true at cross points)
785
788
  *
786
789
  * @remarks
787
- * - True when: (source1[i]! > source2[i]! AND source1[i-1] <= source2[i-1]) OR
788
- * (source1[i]! < source2[i]! AND source1[i-1] >= source2[i-1])
789
- * - First value is always false (no previous value to compare)
790
- * - Detects any crossing (either over or under)
790
+ * PineScript rule (measured bit for bit; not `crossover or crossunder`):
791
+ * - The call keeps the side of the last bar where `source1` was strictly above or strictly below `source2` (no
792
+ * side before such a bar). Equal bars and bars with an `na` value do not change it.
793
+ * - True when the values are strictly on the other side of the kept side: so a run of equal values is passed, and
794
+ * a series that starts equal and then moves away is not a cross
795
+ * - False on a bar with an `na` value
791
796
  * - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
792
797
  *
793
798
  * @example
@@ -798,36 +803,38 @@ export function cum(source) {
798
803
  */
799
804
  export function cross(source1, source2) {
800
805
  const result = [];
806
+ let side = 0; // -1: last strictly below, 1: last strictly above, 0: none yet
801
807
  for (let i = 0; i < source1.length; i++) {
802
- if (i === 0) {
808
+ const a = source1[i];
809
+ const b = source2[i];
810
+ if (Number.isNaN(a) || Number.isNaN(b)) {
803
811
  result.push(false);
812
+ continue;
804
813
  }
805
- else {
806
- const crossedUp = source1[i] > source2[i] && source1[i - 1] <= source2[i - 1];
807
- const crossedDown = source1[i] < source2[i] && source1[i - 1] >= source2[i - 1];
808
- result.push(crossedUp || crossedDown);
809
- }
814
+ result.push((side < 0 && a > b) || (side > 0 && a < b));
815
+ if (a < b)
816
+ side = -1;
817
+ else if (a > b)
818
+ side = 1;
810
819
  }
811
820
  return result;
812
821
  }
813
822
  /**
814
- * ta.rising / ta.falling: true when each of the `length` steps between the last `length + 1` non-na values (up to
815
- * the current bar) passes `step(newer, older)`; false when there are fewer non-na values.
823
+ * ta.rising / ta.falling: a counter of the steps from one bar to the next that pass `step(newer, older)`, reset to 0
824
+ * by a step that does not pass; a step with an na value (on either bar) leaves the counter unchanged. True when the
825
+ * counter is at least `length`.
816
826
  *
817
827
  * @internal
818
828
  */
819
829
  function monotonic(source, length, step) {
820
830
  const result = [];
821
- const values = []; // non-na values so far
831
+ let count = 0;
822
832
  for (let i = 0; i < source.length; i++) {
823
833
  const v = source[i];
824
- if (!Number.isNaN(v))
825
- values.push(v);
826
- const n = values.length;
827
- let ok = n > length;
828
- for (let j = 1; ok && j <= length; j++)
829
- ok = step(values[n - j], values[n - j - 1]);
830
- result.push(ok);
834
+ const prev = i > 0 ? source[i - 1] : NaN;
835
+ if (!Number.isNaN(v) && !Number.isNaN(prev))
836
+ count = step(v, prev) ? count + 1 : 0;
837
+ result.push(count >= length);
831
838
  }
832
839
  return result;
833
840
  }
@@ -840,10 +847,11 @@ function monotonic(source, length, step) {
840
847
  *
841
848
  * @remarks
842
849
  * PineScript rules:
843
- * - each of the `length` steps is a rise larger than the 1e-10 tolerance
844
- * - `na` values are skipped: the steps are between the last `length + 1` non-`na` values up to the current bar
845
- * (on an `na` bar, the result of the last non-`na` bar)
846
- * - false while there are fewer than `length + 1` non-`na` values
850
+ * - a counter of rises from the previous bar (a rise is larger than the 1e-10 tolerance); a step that is not a rise
851
+ * resets it to 0
852
+ * - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
853
+ * result of the bar before the `na`)
854
+ * - true when the counter is at least `length` (false at the start)
847
855
  *
848
856
  * @example
849
857
  * ```typescript
@@ -863,10 +871,11 @@ export function rising(source, length) {
863
871
  *
864
872
  * @remarks
865
873
  * PineScript rules:
866
- * - each of the `length` steps is a fall larger than the 1e-10 tolerance
867
- * - `na` values are skipped: the steps are between the last `length + 1` non-`na` values up to the current bar
868
- * (on an `na` bar, the result of the last non-`na` bar)
869
- * - false while there are fewer than `length + 1` non-`na` values
874
+ * - a counter of falls from the previous bar (a fall is larger than the 1e-10 tolerance); a step that is not a fall
875
+ * resets it to 0
876
+ * - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
877
+ * result of the bar before the `na`)
878
+ * - true when the counter is at least `length` (false at the start)
870
879
  *
871
880
  * @example
872
881
  * ```typescript
@@ -963,10 +972,11 @@ export function mom(source, length) {
963
972
  *
964
973
  * @remarks
965
974
  * - Measures average absolute distance from the mean
966
- * - Formula: `sum(abs(source[i]! - sma)) / length` for i in 0 to length-1
967
975
  * - Less sensitive to outliers than standard deviation
968
- * - `na` values in the source series are ignored
969
- * - The function calculates on the `length` quantity of non-`na` values
976
+ * - PineScript rule (bit for bit): `sum(abs(source[i] - mean)) / length` for `i = 0 .. length - 1`, added newest
977
+ * first, with `mean = ta.sma(source, length)` (its compensated running sum, so a flat window can give a tiny
978
+ * non-zero deviation)
979
+ * - The loop reads the last `length` bars: an `na` among them gives `na`
970
980
  *
971
981
  * @example
972
982
  * ```typescript
@@ -975,24 +985,15 @@ export function mom(source, length) {
975
985
  * ```
976
986
  */
977
987
  export function dev(source, length) {
978
- const result = [];
979
- // PineScript: na when the window of `length` bars holds an na value
980
- const meanValues = strictWindowMean(source, length);
981
- for (let i = 0; i < source.length; i++) {
982
- if (i < length - 1 || isNaN(meanValues[i])) {
983
- result.push(NaN);
984
- }
985
- else {
986
- let sum = 0;
987
- for (let j = 0; j < length; j++) {
988
- if (!isNaN(source[i - j])) {
989
- sum += Math.abs(source[i - j] - meanValues[i]);
990
- }
991
- }
992
- result.push(sum / length);
993
- }
994
- }
995
- return result;
988
+ const len = Math.floor(length);
989
+ return sma(source, len).map((mean, i) => {
990
+ if (i < len - 1 || Number.isNaN(mean))
991
+ return NaN;
992
+ let sum = 0;
993
+ for (let k = 0; k < len; k++)
994
+ sum += Math.abs(source[i - k] - mean);
995
+ return sum / len;
996
+ });
996
997
  }
997
998
  /**
998
999
  * Variance - expectation of squared deviation from mean.
@@ -1021,6 +1022,14 @@ export function dev(source, length) {
1021
1022
  * ```
1022
1023
  */
1023
1024
  export function variance(source, length, biased = true) {
1025
+ return varianceOf(source, length, biased, 'ta.variance');
1026
+ }
1027
+ /** {@link variance} with a fixed or a series length; `name` is used in the error messages. */
1028
+ function varianceOf(source, length, biased, name) {
1029
+ if (typeof length !== 'number') {
1030
+ const result = runningVarianceSeries(source, length, biased, name);
1031
+ return biased ? result : result.map((v, i) => (Math.floor(length[i]) > 1 ? v : NaN));
1032
+ }
1024
1033
  const len = Math.floor(length);
1025
1034
  // PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
1026
1035
  if ((biased ? len : len - 1) <= 0)
@@ -1096,7 +1105,7 @@ export function median(source, length) {
1096
1105
  * @remarks
1097
1106
  * - Fixed length of 4 bars
1098
1107
  * - Weights: [1/6, 2/6, 2/6, 1/6] (symmetric)
1099
- * - Formula: `source[3] * 1/6 + source[2] * 2/6 + source[1] * 2/6 + source[0] * 1/6`
1108
+ * - Formula, in this order as in PineScript (bit for bit): `(source + 2 * source[1] + 2 * source[2] + source[3]) / 6`
1100
1109
  * - More weight given to middle values
1101
1110
  * - `na` values in the source series are included in calculations and will produce an `na` result
1102
1111
  * - Returns NaN for the first 3 bars
@@ -1108,26 +1117,7 @@ export function median(source, length) {
1108
1117
  * ```
1109
1118
  */
1110
1119
  export function swma(source) {
1111
- const result = [];
1112
- for (let i = 0; i < source.length; i++) {
1113
- if (i < 3) {
1114
- result.push(NaN);
1115
- }
1116
- else {
1117
- // Check for any NaN values in the window
1118
- if (isNaN(source[i]) || isNaN(source[i - 1]) || isNaN(source[i - 2]) || isNaN(source[i - 3])) {
1119
- result.push(NaN);
1120
- }
1121
- else {
1122
- const value = source[i - 3] * (1 / 6) +
1123
- source[i - 2] * (2 / 6) +
1124
- source[i - 1] * (2 / 6) +
1125
- source[i] * (1 / 6);
1126
- result.push(value);
1127
- }
1128
- }
1129
- }
1130
- return result;
1120
+ return Array.from({ length: source.length }, (_, i) => i < 3 ? NaN : (source[i] + 2 * source[i - 1] + 2 * source[i - 2] + source[i - 3]) / 6);
1131
1121
  }
1132
1122
  /**
1133
1123
  * Volume Weighted Moving Average (VWMA) - moving average weighted by volume.
@@ -1189,6 +1179,9 @@ export function vwma(source, length, volume) {
1189
1179
  * @remarks
1190
1180
  * - Calculates line of best fit using least squares method
1191
1181
  * - Formula: `linreg = intercept + slope * (length - 1 - offset)`
1182
+ * - As PineScript (bit for bit): x = 1 (oldest) .. length (newest), sums from the oldest value,
1183
+ * `slope = (length * sumXY - sumX * sumY) / (length * sumX2 - sumX * sumX)`,
1184
+ * `intercept = sumY / length - slope * sumX / length + slope`
1192
1185
  * - offset=0 gives current fitted value, offset<0 gives future projection
1193
1186
  * - `na` values in the source series are included in calculations and will produce an `na` result
1194
1187
  * - Useful for trend detection and prediction
@@ -1220,13 +1213,14 @@ export function linreg(source, length, offset = 0) {
1220
1213
  result.push(NaN);
1221
1214
  }
1222
1215
  else {
1223
- // Calculate least squares regression
1216
+ // Least squares as PineScript: x = 1 (oldest) .. len (newest), summed from the oldest value; the intercept
1217
+ // is the fitted value at x = 1
1224
1218
  let sumX = 0;
1225
1219
  let sumY = 0;
1226
1220
  let sumXY = 0;
1227
1221
  let sumX2 = 0;
1228
1222
  for (let j = 0; j < len; j++) {
1229
- const x = j;
1223
+ const x = j + 1;
1230
1224
  const y = source[i - (len - 1 - j)];
1231
1225
  sumX += x;
1232
1226
  sumY += y;
@@ -1234,10 +1228,8 @@ export function linreg(source, length, offset = 0) {
1234
1228
  sumX2 += x * x;
1235
1229
  }
1236
1230
  const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
1237
- const intercept = (sumY - slope * sumX) / len;
1238
- // Calculate linreg value at offset
1239
- const x = len - 1 - offset;
1240
- result.push(intercept + slope * x);
1231
+ const intercept = sumY / len - (slope * sumX) / len + slope;
1232
+ result.push(intercept + slope * (len - 1 - offset));
1241
1233
  }
1242
1234
  }
1243
1235
  }
@@ -1257,8 +1249,9 @@ export function linreg(source, length, offset = 0) {
1257
1249
  * - -1 = perfect negative correlation
1258
1250
  * - 0 = no correlation
1259
1251
  * - Measures linear relationship between two series
1260
- * - `na` values in the source series are ignored
1261
- * - The function calculates on the `length` quantity of non-`na` values
1252
+ * - As PineScript: `cov = sma(x * y) - sma(x) * sma(y)`, `r = cov / sqrt(variance(x) * variance(y))` with the
1253
+ * compensated running sums of `ta.sma` / `ta.variance` (each skips its own `na` values); `|cov| <= 1e-10` gives 0, a
1254
+ * zero variance gives `na`
1262
1255
  *
1263
1256
  * @example
1264
1257
  * ```typescript
@@ -1267,54 +1260,28 @@ export function linreg(source, length, offset = 0) {
1267
1260
  * ```
1268
1261
  */
1269
1262
  export function correlation(source1, source2, length) {
1270
- const result = [];
1271
- // the non-NaN pairs of the window, newest first (reused on every bar)
1272
- const span = length > 0 ? Math.ceil(length) : 0;
1273
- const a = new Float64Array(span);
1274
- const b = new Float64Array(span);
1275
- for (let i = 0; i < source1.length; i++) {
1276
- if (i < length - 1) {
1277
- result.push(NaN);
1278
- continue;
1279
- }
1280
- let m = 0;
1281
- for (let j = 0; j < length; j++) {
1282
- const v1 = source1[i - j];
1283
- const v2 = source2[i - j];
1284
- if (!isNaN(v1) && !isNaN(v2)) {
1285
- a[m] = v1;
1286
- b[m] = v2;
1287
- m++;
1288
- }
1289
- }
1290
- if (m === 0) {
1291
- result.push(NaN);
1292
- continue;
1293
- }
1294
- // Calculate means
1295
- let sum1 = 0;
1296
- let sum2 = 0;
1297
- for (let k = 0; k < m; k++) {
1298
- sum1 += a[k];
1299
- sum2 += b[k];
1300
- }
1301
- const mean1 = sum1 / m;
1302
- const mean2 = sum2 / m;
1303
- // Calculate correlation components
1304
- let numerator = 0;
1305
- let sum1Sq = 0;
1306
- let sum2Sq = 0;
1307
- for (let k = 0; k < m; k++) {
1308
- const dev1 = a[k] - mean1;
1309
- const dev2 = b[k] - mean2;
1310
- numerator += dev1 * dev2;
1311
- sum1Sq += dev1 * dev1;
1312
- sum2Sq += dev2 * dev2;
1313
- }
1314
- const denominator = Math.sqrt(sum1Sq * sum2Sq);
1315
- result.push(denominator === 0 ? NaN : numerator / denominator);
1316
- }
1317
- return result;
1263
+ // PineScript: from compensated running sums, cov = sma(x * y) - sma(x) * sma(y), r = cov / sqrt(var(x) * var(y))
1264
+ // (biased variances); each running sum skips its own na values
1265
+ const product = Array.from({ length: source1.length }, (_, i) => {
1266
+ const v1 = source1[i];
1267
+ const v2 = source2[i];
1268
+ return v1 === null || v1 === undefined || v2 === null || v2 === undefined ? NaN : v1 * v2;
1269
+ });
1270
+ const meanXY = sma(product, length);
1271
+ const meanX = sma(source1, length);
1272
+ const meanY = sma(source2, length);
1273
+ const varX = variance(source1, length);
1274
+ const varY = variance(source2, length);
1275
+ return meanXY.map((mxy, i) => {
1276
+ const cov = mxy - meanX[i] * meanY[i];
1277
+ if (Number.isNaN(cov))
1278
+ return NaN;
1279
+ // a covariance within 1e-10 of 0 gives 0; a zero denominator gives na (a division by zero)
1280
+ if (Math.abs(cov) <= 1e-10)
1281
+ return 0;
1282
+ const denominator = Math.sqrt(varX[i] * varY[i]);
1283
+ return denominator === 0 ? NaN : cov / denominator;
1284
+ });
1318
1285
  }
1319
1286
  /**
1320
1287
  * Percent Rank - percentage of how many previous values were less than or equal to current value.
@@ -1331,7 +1298,7 @@ export function correlation(source1, source2, length) {
1331
1298
  * - Useful for identifying relative strength within a period
1332
1299
  *
1333
1300
  * PineScript rules:
1334
- * - percentrank = count(source[k] <= source, k = 1..length) / length * 100
1301
+ * - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
1335
1302
  * - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
1336
1303
  * `<=` the current value
1337
1304
  * - the comparison uses the 1e-10 tolerance
@@ -1357,7 +1324,7 @@ export function percentrank(source, length) {
1357
1324
  if (le(source[i - j], currentValue))
1358
1325
  countLessOrEqual++;
1359
1326
  }
1360
- result.push((countLessOrEqual / length) * 100);
1327
+ result.push((countLessOrEqual * 100) / length);
1361
1328
  }
1362
1329
  return result;
1363
1330
  }
@@ -1373,10 +1340,14 @@ export function percentrank(source, length) {
1373
1340
  * - Typical Price = (High + Low + Close) / 3
1374
1341
  * - Mean Deviation = Average of absolute differences from mean
1375
1342
  * - Scaled by 0.015 to provide more readable numbers
1376
- * - As in PineScript: when the mean deviation is 0 (1e-10 tolerance, a flat window), the CCI of the previous bar
1377
1343
  * - Values above +100 indicate overbought conditions
1378
1344
  * - Values below -100 indicate oversold conditions
1379
- * - \`na\` values in the source series are ignored
1345
+ *
1346
+ * PineScript rules (bit for bit):
1347
+ * - `(source - ta.sma(source, length)) / (0.015 * ta.dev(source, length))`
1348
+ * - when the mean deviation is 0 (1e-10 tolerance, a flat window), the last CCI value; 0 when there is none yet
1349
+ * (a flat first window)
1350
+ * - \`na\` when the mean or the deviation is \`na\` (an \`na\` in the last \`length\` bars)
1380
1351
  *
1381
1352
  * @example
1382
1353
  * \`\`\`typescript
@@ -1386,22 +1357,17 @@ export function percentrank(source, length) {
1386
1357
  * \`\`\`
1387
1358
  */
1388
1359
  export function cci(source, length) {
1389
- const result = [];
1390
1360
  const smaValues = sma(source, length);
1391
1361
  const devValues = dev(source, length);
1392
- for (let i = 0; i < source.length; i++) {
1393
- if (isNaN(smaValues[i]) || isNaN(devValues[i])) {
1394
- result.push(NaN);
1395
- }
1396
- else if (eq(devValues[i], 0)) {
1397
- result.push(i > 0 ? result[i - 1] : NaN);
1398
- }
1399
- else {
1400
- const cci = (source[i] - smaValues[i]) / (0.015 * devValues[i]);
1401
- result.push(cci);
1402
- }
1403
- }
1404
- return result;
1362
+ let last = 0;
1363
+ return smaValues.map((mean, i) => {
1364
+ const d = devValues[i];
1365
+ if (Number.isNaN(mean) || Number.isNaN(d))
1366
+ return NaN;
1367
+ if (!eq(d, 0))
1368
+ last = (source[i] - mean) / (0.015 * d);
1369
+ return last;
1370
+ });
1405
1371
  }
1406
1372
  /**
1407
1373
  * Stochastic Oscillator - momentum indicator comparing closing price to price range.
@@ -1417,8 +1383,8 @@ export function cci(source, length) {
1417
1383
  * - Measures where close is relative to the high-low range
1418
1384
  * - Values above 80 typically indicate overbought
1419
1385
  * - Values below 20 typically indicate oversold
1420
- * - Returns NaN when range is zero (high equals low)
1421
- * - \`na\` values in the source series are ignored
1386
+ * - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range is 0, the
1387
+ * result is the previous result of the call (\`na\` until a first value)
1422
1388
  *
1423
1389
  * @example
1424
1390
  * \`\`\`typescript
@@ -1431,20 +1397,15 @@ export function stoch(source, high, low, length) {
1431
1397
  const result = [];
1432
1398
  const lowestValues = lowest(low, length);
1433
1399
  const highestValues = highest(high, length);
1400
+ // PineScript: an na input or a zero range keeps the previous result
1401
+ let previous = NaN;
1434
1402
  for (let i = 0; i < source.length; i++) {
1435
- if (isNaN(lowestValues[i]) || isNaN(highestValues[i])) {
1436
- result.push(NaN);
1437
- }
1438
- else {
1439
- const range = highestValues[i] - lowestValues[i];
1440
- if (range === 0) {
1441
- result.push(NaN);
1442
- }
1443
- else {
1444
- const stochValue = 100 * (source[i] - lowestValues[i]) / range;
1445
- result.push(stochValue);
1446
- }
1447
- }
1403
+ const value = source[i];
1404
+ const lo = lowestValues[i];
1405
+ const range = highestValues[i] - lo;
1406
+ if (!Number.isNaN(value) && !Number.isNaN(range) && range !== 0)
1407
+ previous = (100 * (value - lo)) / range;
1408
+ result.push(previous);
1448
1409
  }
1449
1410
  return result;
1450
1411
  }
@@ -1463,7 +1424,13 @@ export function stoch(source, high, low, length) {
1463
1424
  * - Formula: 100 - (100 / (1 + Positive Money Flow / Negative Money Flow))
1464
1425
  * - Values above 80 indicate overbought
1465
1426
  * - Values below 20 indicate oversold
1466
- * - `na` values in the source series are ignored
1427
+ *
1428
+ * PineScript rules (bit for bit):
1429
+ * - the two flows are `math.sum` running sums; a sum within 1e-10 of 0 counts as 0 (the residue a running sum
1430
+ * keeps over a window of zeros)
1431
+ * - 100 when the negative flow is 0; `na` when `1 + positive / negative` is 0 (division by zero)
1432
+ * - a bar with an `na` source adds nothing to the sums and gives the previous result; the bar after it (an `na`
1433
+ * change) adds its money flow to both sums
1467
1434
  *
1468
1435
  * @example
1469
1436
  * ```typescript
@@ -1496,13 +1463,18 @@ export function mfi(source, length, volume) {
1496
1463
  // math.sum: the compensated running sum of the last `length` non-na values (as PineScript, bit for bit)
1497
1464
  const upper = runningSum(upperFlow, length);
1498
1465
  const lower = runningSum(lowerFlow, length);
1499
- return upper.map((u, i) => {
1500
- const l = lower[i];
1466
+ // A sum within 1e-10 of 0 (the residue of a running sum over a window of zeros) is 0
1467
+ const zero = (x) => (Math.abs(x) <= 1e-10 ? 0 : x);
1468
+ return upper.map((sumUp, i) => {
1469
+ const u = zero(sumUp);
1470
+ const l = zero(lower[i]);
1501
1471
  if (Number.isNaN(u) || Number.isNaN(l))
1502
1472
  return NaN;
1503
1473
  if (l === 0)
1504
1474
  return 100;
1505
- return 100 - 100 / (1 + u / l);
1475
+ const ratio = 1 + u / l;
1476
+ // x / 0 is na in PineScript
1477
+ return ratio === 0 ? NaN : 100 - 100 / ratio;
1506
1478
  });
1507
1479
  }
1508
1480
  /**
@@ -1870,10 +1842,12 @@ export function dmi(diLength, adxSmoothing, high, low, close) {
1870
1842
  * @param source - Series of values to process
1871
1843
  * @param shortLength - Short smoothing length
1872
1844
  * @param longLength - Long smoothing length
1873
- * @returns TSI series
1845
+ * @returns TSI series, a value in [-1, 1]
1874
1846
  *
1875
1847
  * @remarks
1876
- * - TSI oscillates between +100 and -100
1848
+ * - As PineScript: `ta.ema(ta.ema(m, longLength), shortLength) / ta.ema(ta.ema(math.abs(m), longLength), shortLength)`
1849
+ * with `m = source - source[1]` (not scaled by 100)
1850
+ * - na when the denominator is 0 (a constant source), as a PineScript division by zero
1877
1851
  * - Positive values indicate bullish momentum
1878
1852
  * - Negative values indicate bearish momentum
1879
1853
  * - Crossovers of zero line can signal trend changes
@@ -1902,17 +1876,11 @@ export function tsi(source, shortLength, longLength) {
1902
1876
  // Double smooth absolute momentum
1903
1877
  const absMomentum = momentum.map(Math.abs);
1904
1878
  const smoothedAbsMomentum = ema(ema(absMomentum, longLength), shortLength);
1905
- // Calculate TSI
1906
- const result = [];
1907
- for (let i = 0; i < source.length; i++) {
1908
- if (smoothedAbsMomentum[i] === 0) {
1909
- result.push(0);
1910
- }
1911
- else {
1912
- result.push((smoothedMomentum[i] / smoothedAbsMomentum[i]) * 100);
1913
- }
1914
- }
1915
- return result;
1879
+ // PineScript: a value in [-1, 1]; a division by zero is na
1880
+ return smoothedMomentum.map((m, i) => {
1881
+ const d = smoothedAbsMomentum[i];
1882
+ return d === 0 ? NaN : m / d;
1883
+ });
1916
1884
  }
1917
1885
  /**
1918
1886
  * Chande Momentum Oscillator - momentum indicator similar to RSI.
@@ -1926,7 +1894,8 @@ export function tsi(source, shortLength, longLength) {
1926
1894
  * - CMO > +50: overbought conditions
1927
1895
  * - CMO < -50: oversold conditions
1928
1896
  * - Unlike RSI, CMO uses sum of gains/losses instead of averages
1929
- * - na on a flat window (every change 0), as in PineScript
1897
+ * - As in PineScript: the sums are the running sums of `math.sum` (bit for bit), and a flat window (every change 0)
1898
+ * is `0 / 0` = na
1930
1899
  * - More volatile than RSI
1931
1900
  *
1932
1901
  * @example
@@ -1937,34 +1906,20 @@ export function tsi(source, shortLength, longLength) {
1937
1906
  * ```
1938
1907
  */
1939
1908
  export function cmo(source, length) {
1940
- const result = [];
1909
+ // PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
1910
+ // sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
1911
+ // math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
1912
+ const up = [];
1913
+ const down = [];
1941
1914
  for (let i = 0; i < source.length; i++) {
1942
- if (i < length) {
1943
- result.push(NaN);
1944
- continue;
1945
- }
1946
- let sumGains = 0;
1947
- let sumLosses = 0;
1948
- for (let j = 0; j < length; j++) {
1949
- const change = source[i - j] - source[i - j - 1];
1950
- if (change > 0) {
1951
- sumGains += change;
1952
- }
1953
- else {
1954
- sumLosses += Math.abs(change);
1955
- }
1956
- }
1957
- // PineScript: 100 * (sm1 - sm2) / (sm1 + sm2), so a flat window (0 / 0) is na
1958
- const totalMovement = sumGains + sumLosses;
1959
- if (totalMovement === 0) {
1960
- result.push(NaN);
1961
- }
1962
- else {
1963
- const cmoValue = ((sumGains - sumLosses) / totalMovement) * 100;
1964
- result.push(cmoValue);
1965
- }
1915
+ const mom = i > 0 ? source[i] - source[i - 1] : NaN;
1916
+ up.push(mom >= 0 ? mom : 0);
1917
+ down.push(mom >= 0 ? 0 : -mom);
1966
1918
  }
1967
- return result;
1919
+ const sm1 = runningSum(up, length);
1920
+ const sm2 = runningSum(down, length);
1921
+ // a flat window is 0 / 0 = na
1922
+ return sm1.map((s1, i) => (100 * (s1 - sm2[i])) / (s1 + sm2[i]));
1968
1923
  }
1969
1924
  /**
1970
1925
  * Keltner Channels - volatility-based envelope indicator.
@@ -2074,6 +2029,10 @@ export function bbw(source, length, mult) {
2074
2029
  * @param length - Lookback period (default: 14)
2075
2030
  * @returns Williams %R series
2076
2031
  *
2032
+ * @remarks
2033
+ * PineScript rule (bit for bit): `100 * (close - max) / (max - min)` with `max = ta.highest(high, length)` and
2034
+ * `min = ta.lowest(low, length)` (their `na` rules); `na` when the range is 0.
2035
+ *
2077
2036
  * @example
2078
2037
  * ```typescript
2079
2038
  * const wpr = ta.wpr(high, low, close, 14);
@@ -2082,32 +2041,14 @@ export function bbw(source, length, mult) {
2082
2041
  * ```
2083
2042
  */
2084
2043
  export function wpr(high, low, close, length = 14) {
2085
- const result = [];
2086
- for (let i = 0; i < close.length; i++) {
2087
- if (i < length - 1) {
2088
- result.push(NaN);
2089
- continue;
2090
- }
2091
- // Find highest high and lowest low in the period
2092
- let highestHigh = high[i - length + 1];
2093
- let lowestLow = low[i - length + 1];
2094
- for (let j = i - length + 2; j <= i; j++) {
2095
- if (high[j] > highestHigh)
2096
- highestHigh = high[j];
2097
- if (low[j] < lowestLow)
2098
- lowestLow = low[j];
2099
- }
2100
- const range = highestHigh - lowestLow;
2101
- if (range === 0) {
2102
- result.push(NaN);
2103
- }
2104
- else {
2105
- // Formula: (Highest High - Close) / (Highest High - Lowest Low) * -100
2106
- const wprValue = ((highestHigh - close[i]) / range) * -100;
2107
- result.push(wprValue);
2108
- }
2109
- }
2110
- return result;
2044
+ const highs = highest(high, length);
2045
+ const lows = lowest(low, length);
2046
+ return Array.from({ length: close.length }, (_, i) => {
2047
+ const hmax = highs[i];
2048
+ const range = hmax - lows[i];
2049
+ // PineScript, in this order (bit for bit): 100 * (close - max) / (max - min); x / 0 is na
2050
+ return range === 0 ? NaN : (100 * (close[i] - hmax)) / range;
2051
+ });
2111
2052
  }
2112
2053
  export function vwap(source, volume, anchor, stdev_mult) {
2113
2054
  if (source.length !== volume.length) {
@@ -2116,18 +2057,20 @@ export function vwap(source, volume, anchor, stdev_mult) {
2116
2057
  const mid = [];
2117
2058
  const upper = [];
2118
2059
  const lower = [];
2119
- let sumPV = 0;
2120
- let sumV = 0;
2121
- let sumPPV = 0;
2060
+ // na until the first anchor (without an anchor series, bar 0 starts the sums)
2061
+ let sumPV = NaN;
2062
+ let sumV = NaN;
2063
+ let sumPPV = NaN;
2122
2064
  for (let i = 0; i < source.length; i++) {
2123
- if (anchor && anchor[i]) {
2124
- sumPV = 0;
2125
- sumV = 0;
2126
- sumPPV = 0;
2127
- }
2128
2065
  const s = source[i];
2129
2066
  const v = volume[i];
2130
- if (!Number.isNaN(s) && !Number.isNaN(v)) {
2067
+ if (anchor ? anchor[i] : i === 0) {
2068
+ sumPV = s * v;
2069
+ sumV = v;
2070
+ sumPPV = s * s * v;
2071
+ }
2072
+ else {
2073
+ // An na source or volume makes the sums na until the next anchor
2131
2074
  sumPV += s * v;
2132
2075
  sumV += v;
2133
2076
  sumPPV += s * s * v;
@@ -2194,7 +2137,8 @@ export function alma(source, length = 9, offset = 0.85, sigma = 6, floor = false
2194
2137
  /**
2195
2138
  * Keltner Channels Width (KCW)
2196
2139
  *
2197
- * Measures the width of Keltner Channels as a percentage of the middle line.
2140
+ * Measures the width of Keltner Channels relative to the middle line: `(upper - lower) / middle`, a ratio as in
2141
+ * PineScript (0.05 is a width of 5% of the middle line; not multiplied by 100).
2198
2142
  * Similar to BBW but uses ATR instead of standard deviation.
2199
2143
  *
2200
2144
  * Low KCW suggests consolidation/low volatility.
@@ -2224,8 +2168,8 @@ export function kcw(source, length = 20, mult = 2, useTrueRange = true, high, lo
2224
2168
  result.push(NaN);
2225
2169
  }
2226
2170
  else {
2227
- const width = ((upper[i] - lower[i]) / basis[i]) * 100;
2228
- result.push(width);
2171
+ // PineScript: (upper - lower) / basis, a ratio (not a percentage)
2172
+ result.push((upper[i] - lower[i]) / basis[i]);
2229
2173
  }
2230
2174
  }
2231
2175
  return result;
@@ -2302,46 +2246,32 @@ export function min(source) {
2302
2246
  * The Center of Gravity indicator is an oscillator developed by John Ehlers.
2303
2247
  * It identifies turning points with minimal lag and provides clear signals.
2304
2248
  *
2305
- * The COG calculates a weighted average where more recent prices have higher weights,
2306
- * similar to a moving average but with a focus on momentum shifts.
2307
- *
2308
2249
  * @param source - Source series (typically close)
2309
2250
  * @param length - Lookback period (default: 10)
2310
2251
  * @returns COG series
2311
2252
  *
2253
+ * @remarks
2254
+ * PineScript rule (bit for bit): `-num / math.sum(source, length)`, with `num` the sum of `source[i] * (i + 1)`
2255
+ * for `i = 0 .. length - 1`, added newest first.
2256
+ * - The denominator is the compensated running sum of `math.sum` (the last `length` non-`na` values)
2257
+ * - The numerator reads the last `length` bars, so an `na` among them gives `na`
2258
+ * - A zero denominator gives `na` (PineScript division by zero)
2259
+ *
2312
2260
  * @example
2313
2261
  * ```typescript
2314
2262
  * const cogValue = ta.cog(close, 10);
2315
- * // Use COG crossovers as signals:
2316
- * // - COG crossing above 0: potential buy signal
2317
- * // - COG crossing below 0: potential sell signal
2318
2263
  * ```
2319
2264
  */
2320
2265
  export function cog(source, length = 10) {
2321
- const result = [];
2322
- for (let i = 0; i < source.length; i++) {
2323
- if (i < length - 1) {
2324
- result.push(NaN);
2325
- continue;
2326
- }
2327
- let numerator = 0;
2328
- let denominator = 0;
2329
- for (let j = 0; j < length; j++) {
2330
- const weight = j + 1;
2331
- const price = source[i - length + 1 + j];
2332
- numerator += weight * price;
2333
- denominator += price;
2334
- }
2335
- if (denominator === 0) {
2336
- result.push(NaN);
2337
- }
2338
- else {
2339
- // COG formula: -1 * (sum of (weight * price) / sum of prices) + (length + 1) / 2
2340
- const cog = -1 * (numerator / denominator) + (length + 1) / 2;
2341
- result.push(cog);
2342
- }
2343
- }
2344
- return result;
2266
+ const len = Math.floor(length);
2267
+ return runningSum(source, len).map((sum, i) => {
2268
+ if (i < len - 1 || Number.isNaN(sum) || sum === 0)
2269
+ return NaN;
2270
+ let num = 0;
2271
+ for (let k = 0; k < len; k++)
2272
+ num += source[i - k] * (k + 1);
2273
+ return -num / sum;
2274
+ });
2345
2275
  }
2346
2276
  /**
2347
2277
  * Mode (Most Frequent Value)
@@ -2360,29 +2290,25 @@ export function cog(source, length = 10) {
2360
2290
  * ```
2361
2291
  *
2362
2292
  * @remarks
2363
- * - `na` values in the source series are ignored
2293
+ * PineScript rules:
2294
+ * - the window is the last `length` non-`na` values (an `na` bar does not enter it, and gets the mode of the
2295
+ * window it keeps)
2296
+ * - NaN until `length` non-`na` values exist
2364
2297
  * - If no mode exists, returns the smallest value
2365
- * - Returns NaN for the first (length - 1) values where there's insufficient data
2366
2298
  */
2367
2299
  export function mode(source, length) {
2368
2300
  const result = [];
2301
+ const len = Math.floor(length);
2302
+ const seen = []; // non-na values so far
2369
2303
  for (let i = 0; i < source.length; i++) {
2370
- if (i < length - 1) {
2371
- result.push(NaN);
2372
- continue;
2373
- }
2374
- // Collect non-NaN values in the window
2375
- const values = [];
2376
- for (let j = 0; j < length; j++) {
2377
- const value = source[i - j];
2378
- if (!isNaN(value)) {
2379
- values.push(value);
2380
- }
2381
- }
2382
- if (values.length === 0) {
2304
+ const v = source[i];
2305
+ if (!Number.isNaN(v))
2306
+ seen.push(v);
2307
+ if (seen.length < len) {
2383
2308
  result.push(NaN);
2384
2309
  continue;
2385
2310
  }
2311
+ const values = seen.slice(seen.length - len);
2386
2312
  // Count frequency of each value
2387
2313
  const frequencyMap = new Map();
2388
2314
  for (const value of values) {
@@ -2531,79 +2457,75 @@ export function percentile_nearest_rank(source, length, percentage) {
2531
2457
  * - +100 indicates source consistently increased over the period
2532
2458
  * - -100 indicates source consistently decreased over the period
2533
2459
  * - 0 indicates no directional consistency
2534
- * - Returns NaN for the first (length - 1) values where there's insufficient data
2460
+ * - As PineScript (bit for bit): the first value is on bar `length`; values less than 1e-10 above the smallest value
2461
+ * of a group (ascending sweep) are ties with their average rank; a window where all values are ties gives `na`;
2462
+ * moment-form variances of the bar positions and the ranks, `(100 * cov) / (sd_x * sd_y)`
2463
+ * - A bar with an `na` source keeps the previous result; the values are kept in a ring of `length + 1` slots by bar
2464
+ * index (0 at the start, not written on `na` bars), so a later window sees the value of `length + 1` bars earlier
2465
+ * where the `na` was
2535
2466
  */
2536
2467
  export function rci(source, length) {
2468
+ const len = Math.floor(length);
2537
2469
  const result = [];
2470
+ // PineScript keeps the values in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start
2471
+ // and written only on bars with a value: a bar with an na source keeps the previous result, and later windows see
2472
+ // the value of `length + 1` bars earlier in that slot
2473
+ const ring = new Array(len + 1).fill(0);
2538
2474
  for (let i = 0; i < source.length; i++) {
2539
- if (i < length - 1) {
2540
- result.push(NaN);
2475
+ const v = source[i];
2476
+ if (Number.isNaN(v)) {
2477
+ result.push(i > 0 ? result[i - 1] : NaN);
2541
2478
  continue;
2542
2479
  }
2543
- // Collect values in the window
2544
- const values = [];
2545
- for (let j = 0; j < length; j++) {
2546
- values.push(source[i - length + 1 + j]);
2547
- }
2548
- // Check for NaN values
2549
- if (values.some(v => isNaN(v))) {
2480
+ ring[i % (len + 1)] = v;
2481
+ // the first value is on bar `length` (one bar after the window is full)
2482
+ if (i < len) {
2550
2483
  result.push(NaN);
2551
2484
  continue;
2552
2485
  }
2553
- // Create array of indices with their values for ranking
2554
- const indexed = values.map((value, index) => ({ value, index }));
2555
- // Sort by value to get ranks
2556
- const sorted = [...indexed].sort((a, b) => a.value - b.value);
2557
- // Assign ranks (handling ties by averaging ranks)
2558
- const ranks = new Array(length).fill(0);
2559
- let currentRank = 1;
2560
- for (let j = 0; j < sorted.length; j++) {
2561
- // Count ties
2562
- let tieCount = 1;
2563
- while (j + tieCount < sorted.length && sorted[j].value === sorted[j + tieCount].value) {
2564
- tieCount++;
2565
- }
2566
- // Average rank for ties
2567
- const avgRank = (currentRank + (currentRank + tieCount - 1)) / 2;
2568
- // Assign average rank to all tied values
2569
- for (let k = 0; k < tieCount; k++) {
2570
- ranks[sorted[j + k].index] = avgRank;
2571
- }
2572
- j += tieCount - 1;
2573
- currentRank += tieCount;
2574
- }
2575
- // Calculate D² = sum of (price_rank - time_rank)²
2576
- let sumSquaredDiff = 0;
2577
- for (let j = 0; j < length; j++) {
2578
- const timeRank = j + 1;
2579
- const diff = ranks[j] - timeRank;
2580
- sumSquaredDiff += diff * diff;
2581
- }
2582
- const n = length;
2583
- const base = (n * n * n - n) / 12;
2584
- // Compute tie correction for price ranks
2585
- // Count tied groups from the sorted array
2586
- let tieCorrection = 0;
2587
- for (let j = 0; j < sorted.length;) {
2588
- let tieCount = 1;
2589
- while (j + tieCount < sorted.length && sorted[j].value === sorted[j + tieCount].value) {
2590
- tieCount++;
2591
- }
2592
- if (tieCount > 1) {
2593
- tieCorrection += (tieCount * tieCount * tieCount - tieCount) / 12;
2594
- }
2595
- j += tieCount;
2596
- }
2597
- const A = base - tieCorrection; // Corrected for tied price ranks
2598
- const B = base; // Time ranks have no ties
2599
- let rho;
2600
- if (A === 0 || B === 0) {
2601
- rho = 0;
2602
- }
2603
- else {
2604
- rho = (A + B - sumSquaredDiff) / (2 * Math.sqrt(A * B));
2486
+ // window newest first (position 0 = this bar)
2487
+ const values = Array.from({ length: len }, (_, j) => ring[(i - j) % (len + 1)]);
2488
+ // Ranks: ascending sweep; a group collects the values less than 1e-10 above its smallest value; each group gets
2489
+ // its average 0-based rank counted from the largest value
2490
+ const order = values.map((_, j) => j).sort((a, b) => values[a] - values[b]);
2491
+ const ranks = new Array(len);
2492
+ for (let g = 0; g < len;) {
2493
+ const anchor = values[order[g]];
2494
+ let end = g + 1;
2495
+ while (end < len && values[order[end]] - anchor < 1e-10)
2496
+ end++;
2497
+ const avgRank = len - 1 - (g + g + (end - g) - 1) / 2;
2498
+ for (let k = g; k < end; k++)
2499
+ ranks[order[k]] = avgRank;
2500
+ g = end;
2501
+ }
2502
+ // moment-form variances, covariance from deviation products, (100 * cov) / (sd_x * sd_y): this operation order
2503
+ // gives PineScript's values bit for bit
2504
+ let sumX = 0;
2505
+ let sumY = 0;
2506
+ let sumX2 = 0;
2507
+ let sumY2 = 0;
2508
+ for (let k = 0; k < len; k++) {
2509
+ const y = ranks[k];
2510
+ sumX += k;
2511
+ sumY += y;
2512
+ sumX2 += k * k;
2513
+ sumY2 += y * y;
2514
+ }
2515
+ const meanX = sumX / len;
2516
+ const meanY = sumY / len;
2517
+ const varX = sumX2 / len - meanX * meanX;
2518
+ const varY = sumY2 / len - meanY * meanY;
2519
+ // a window where all values are ties has no correlation
2520
+ if (!(varX > 0) || !(varY > 0)) {
2521
+ result.push(NaN);
2522
+ continue;
2605
2523
  }
2606
- result.push(rho * 100);
2524
+ let cov = 0;
2525
+ for (let k = 0; k < len; k++)
2526
+ cov += (k - meanX) * (ranks[k] - meanY);
2527
+ cov /= len;
2528
+ result.push((100 * cov) / (Math.sqrt(varX) * Math.sqrt(varY)));
2607
2529
  }
2608
2530
  return result;
2609
2531
  }
@@ -3148,14 +3070,19 @@ export function pvt(close, volume) {
3148
3070
  }
3149
3071
  /**
3150
3072
  * Accumulation/Distribution index: the running sum of
3151
- * `(2 * close - low - high) / (high - low) * volume`, with 0 on bars where `high == low`.
3073
+ * `((close - low) - (high - close)) / (high - low) * volume`, in this order as in PineScript (bit for bit).
3074
+ * A bar where this is `na` (`high == low`, an `na` volume) adds nothing and keeps the previous value.
3152
3075
  */
3153
3076
  export function accdist(high, low, close, volume) {
3154
- return cum(close.map((c, i) => {
3077
+ let ad = 0;
3078
+ return close.map((c, i) => {
3155
3079
  const h = high[i];
3156
3080
  const l = low[i];
3157
- return h === l ? 0 : ((2 * c - l - h) / (h - l)) * volume[i];
3158
- }));
3081
+ const mfv = (((c - l) - (h - c)) / (h - l)) * volume[i];
3082
+ if (Number.isFinite(mfv))
3083
+ ad += mfv;
3084
+ return ad;
3085
+ });
3159
3086
  }
3160
3087
  /**
3161
3088
  * Intraday Intensity Index: `(2 * close - high - low) / (high - low) * volume`.