oakscriptjs 0.9.5 → 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 +84 -27
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +235 -78
  4. package/dist/cjs/array/index.js.map +3 -3
  5. package/dist/cjs/callsite/index.d.ts +81 -2
  6. package/dist/cjs/callsite/index.d.ts.map +1 -1
  7. package/dist/cjs/callsite/index.js +62 -1
  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 +76 -21
  23. package/dist/cjs/math/index.d.ts.map +1 -1
  24. package/dist/cjs/math/index.js +155 -41
  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 +86 -43
  53. package/dist/cjs/ta/index.d.ts.map +1 -1
  54. package/dist/cjs/ta/index.js +173 -267
  55. package/dist/cjs/ta/index.js.map +3 -3
  56. package/dist/cjs/ta/running-sum.d.ts +54 -2
  57. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  58. package/dist/cjs/ta/running-sum.js +132 -52
  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 +84 -27
  80. package/dist/esm/array/index.d.ts.map +1 -1
  81. package/dist/esm/array/index.js +373 -111
  82. package/dist/esm/array/index.js.map +1 -1
  83. package/dist/esm/callsite/index.d.ts +81 -2
  84. package/dist/esm/callsite/index.d.ts.map +1 -1
  85. package/dist/esm/callsite/index.js +145 -3
  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 +76 -21
  100. package/dist/esm/math/index.d.ts.map +1 -1
  101. package/dist/esm/math/index.js +227 -61
  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 +86 -43
  130. package/dist/esm/ta/index.d.ts.map +1 -1
  131. package/dist/esm/ta/index.js +296 -348
  132. package/dist/esm/ta/index.js.map +1 -1
  133. package/dist/esm/ta/running-sum.d.ts +54 -2
  134. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  135. package/dist/esm/ta/running-sum.js +166 -71
  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 +1 -1
  157. package/src/array/index.ts +352 -127
  158. package/src/callsite/index.ts +147 -3
  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 +256 -74
  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 +306 -386
  172. package/src/ta/running-sum.ts +187 -74
  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
  *
@@ -227,7 +222,7 @@ export function bb(series, length, mult) {
227
222
  * Standard deviation over the last `length` non-na values.
228
223
  *
229
224
  * @param source - Series of values to process
230
- * @param length - Number of values
225
+ * @param length - Number of values, or one length per bar (a series length, see {@link sma})
231
226
  * @param biased - true (default): biased estimate (divides by `length`); false: unbiased (by `length - 1`)
232
227
  * @returns Standard deviation series (na until `length` non-na values exist)
233
228
  *
@@ -238,7 +233,7 @@ export function bb(series, length, mult) {
238
233
  * compensated running sums of the values and of their squares, bit for bit as PineScript.
239
234
  */
240
235
  export function stdev(source, length, biased = true) {
241
- 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));
242
237
  }
243
238
  /**
244
239
  * The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist.
@@ -605,7 +600,8 @@ export function supertrend(factor, atrPeriod, high, low, close, wicks = false) {
605
600
  * @remarks
606
601
  * - Moving average used in RSI calculation
607
602
  * - Alpha = 1 / length (different from EMA which uses alpha = 2 / (length + 1))
608
- * - 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
609
605
  * - Formula: `RMA = (source + (length - 1) * RMA[1]) / length`, evaluated in this order as in PineScript
610
606
  * (equal to `alpha * source + (1 - alpha) * RMA[1]`, but bit for bit)
611
607
  * - `na` values (also +/-Infinity) in the source series are ignored
@@ -627,21 +623,16 @@ export function rma(source, length) {
627
623
  // An infinite value is na, as in PineScript.
628
624
  let firstValidIndex = -1;
629
625
  let validCount = 0;
630
- let initSum = 0;
631
626
  for (let i = 0; i < source.length; i++) {
632
627
  const val = source[i];
633
- if (val !== undefined && Number.isFinite(val)) {
634
- initSum += val;
635
- validCount++;
636
- if (validCount === len) {
637
- firstValidIndex = i;
638
- break;
639
- }
628
+ if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
629
+ firstValidIndex = i;
630
+ break;
640
631
  }
641
632
  }
642
- // Initialize RMA value with SMA of first `length` non-NaN values
643
- let rmaValue = validCount > 0 ? initSum / validCount : NaN;
644
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;
645
636
  for (let i = 0; i < source.length; i++) {
646
637
  if (!rmaInitialized || i < firstValidIndex) {
647
638
  // Not enough data yet for RMA
@@ -667,6 +658,14 @@ export function rma(source, length) {
667
658
  }
668
659
  return result;
669
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
+ }
670
669
  /**
671
670
  * Weighted Moving Average (WMA) - moving average with linearly decreasing weights.
672
671
  *
@@ -680,6 +679,8 @@ export function rma(source, length) {
680
679
  * - Formula: `sum(source[i]! * (length - i)) / sum(length - i)` for i = 0 to length-1
681
680
  * - As in PineScript: na on a bar whose source is `na` (also +/-Infinity) and until `length` non-`na` values were
682
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)
683
684
  * - More responsive to recent price changes than SMA
684
685
  *
685
686
  * @example
@@ -714,7 +715,7 @@ export function wma(source, length) {
714
715
  let weightSum = 0;
715
716
  for (let j = len - 1; j >= 0; j--) {
716
717
  const weight = len - j;
717
- sum += filled[i - j] * weight;
718
+ sum = wmaAdd(sum, filled[i - j] * weight);
718
719
  weightSum += weight;
719
720
  }
720
721
  result.push(sum / weightSum);
@@ -786,11 +787,12 @@ export function cum(source) {
786
787
  * @returns Boolean series (true at cross points)
787
788
  *
788
789
  * @remarks
789
- * - True when: (source1[i] > source2[i] AND source1[j] <= source2[j]) OR
790
- * (source1[i] < source2[i] AND source1[j] >= source2[j]),
791
- * where j is the last bar before i where both values were not na
792
- * - First value is always false (no previous value to compare)
793
- * - 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
794
796
  * - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
795
797
  *
796
798
  * @example
@@ -800,26 +802,39 @@ export function cum(source) {
800
802
  * ```
801
803
  */
802
804
  export function cross(source1, source2) {
803
- return crossTest(source1, source2, (a, b, pa, pb) => (a > b && pa <= pb) || (a < b && pa >= pb));
805
+ const result = [];
806
+ let side = 0; // -1: last strictly below, 1: last strictly above, 0: none yet
807
+ for (let i = 0; i < source1.length; i++) {
808
+ const a = source1[i];
809
+ const b = source2[i];
810
+ if (Number.isNaN(a) || Number.isNaN(b)) {
811
+ result.push(false);
812
+ continue;
813
+ }
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;
819
+ }
820
+ return result;
804
821
  }
805
822
  /**
806
- * ta.rising / ta.falling: true when each of the `length` steps between the last `length + 1` non-na values (up to
807
- * 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`.
808
826
  *
809
827
  * @internal
810
828
  */
811
829
  function monotonic(source, length, step) {
812
830
  const result = [];
813
- const values = []; // non-na values so far
831
+ let count = 0;
814
832
  for (let i = 0; i < source.length; i++) {
815
833
  const v = source[i];
816
- if (!Number.isNaN(v))
817
- values.push(v);
818
- const n = values.length;
819
- let ok = n > length;
820
- for (let j = 1; ok && j <= length; j++)
821
- ok = step(values[n - j], values[n - j - 1]);
822
- 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);
823
838
  }
824
839
  return result;
825
840
  }
@@ -832,10 +847,11 @@ function monotonic(source, length, step) {
832
847
  *
833
848
  * @remarks
834
849
  * PineScript rules:
835
- * - each of the `length` steps is a rise larger than the 1e-10 tolerance
836
- * - `na` values are skipped: the steps are between the last `length + 1` non-`na` values up to the current bar
837
- * (on an `na` bar, the result of the last non-`na` bar)
838
- * - 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)
839
855
  *
840
856
  * @example
841
857
  * ```typescript
@@ -855,10 +871,11 @@ export function rising(source, length) {
855
871
  *
856
872
  * @remarks
857
873
  * PineScript rules:
858
- * - each of the `length` steps is a fall larger than the 1e-10 tolerance
859
- * - `na` values are skipped: the steps are between the last `length + 1` non-`na` values up to the current bar
860
- * (on an `na` bar, the result of the last non-`na` bar)
861
- * - 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)
862
879
  *
863
880
  * @example
864
881
  * ```typescript
@@ -955,10 +972,11 @@ export function mom(source, length) {
955
972
  *
956
973
  * @remarks
957
974
  * - Measures average absolute distance from the mean
958
- * - Formula: `sum(abs(source[i]! - sma)) / length` for i in 0 to length-1
959
975
  * - Less sensitive to outliers than standard deviation
960
- * - `na` values in the source series are ignored
961
- * - 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`
962
980
  *
963
981
  * @example
964
982
  * ```typescript
@@ -967,24 +985,15 @@ export function mom(source, length) {
967
985
  * ```
968
986
  */
969
987
  export function dev(source, length) {
970
- const result = [];
971
- // PineScript: na when the window of `length` bars holds an na value
972
- const meanValues = strictWindowMean(source, length);
973
- for (let i = 0; i < source.length; i++) {
974
- if (i < length - 1 || isNaN(meanValues[i])) {
975
- result.push(NaN);
976
- }
977
- else {
978
- let sum = 0;
979
- for (let j = 0; j < length; j++) {
980
- if (!isNaN(source[i - j])) {
981
- sum += Math.abs(source[i - j] - meanValues[i]);
982
- }
983
- }
984
- result.push(sum / length);
985
- }
986
- }
987
- 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
+ });
988
997
  }
989
998
  /**
990
999
  * Variance - expectation of squared deviation from mean.
@@ -1013,6 +1022,14 @@ export function dev(source, length) {
1013
1022
  * ```
1014
1023
  */
1015
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
+ }
1016
1033
  const len = Math.floor(length);
1017
1034
  // PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
1018
1035
  if ((biased ? len : len - 1) <= 0)
@@ -1088,7 +1105,7 @@ export function median(source, length) {
1088
1105
  * @remarks
1089
1106
  * - Fixed length of 4 bars
1090
1107
  * - Weights: [1/6, 2/6, 2/6, 1/6] (symmetric)
1091
- * - 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`
1092
1109
  * - More weight given to middle values
1093
1110
  * - `na` values in the source series are included in calculations and will produce an `na` result
1094
1111
  * - Returns NaN for the first 3 bars
@@ -1100,26 +1117,7 @@ export function median(source, length) {
1100
1117
  * ```
1101
1118
  */
1102
1119
  export function swma(source) {
1103
- const result = [];
1104
- for (let i = 0; i < source.length; i++) {
1105
- if (i < 3) {
1106
- result.push(NaN);
1107
- }
1108
- else {
1109
- // Check for any NaN values in the window
1110
- if (isNaN(source[i]) || isNaN(source[i - 1]) || isNaN(source[i - 2]) || isNaN(source[i - 3])) {
1111
- result.push(NaN);
1112
- }
1113
- else {
1114
- const value = source[i - 3] * (1 / 6) +
1115
- source[i - 2] * (2 / 6) +
1116
- source[i - 1] * (2 / 6) +
1117
- source[i] * (1 / 6);
1118
- result.push(value);
1119
- }
1120
- }
1121
- }
1122
- 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);
1123
1121
  }
1124
1122
  /**
1125
1123
  * Volume Weighted Moving Average (VWMA) - moving average weighted by volume.
@@ -1181,6 +1179,9 @@ export function vwma(source, length, volume) {
1181
1179
  * @remarks
1182
1180
  * - Calculates line of best fit using least squares method
1183
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`
1184
1185
  * - offset=0 gives current fitted value, offset<0 gives future projection
1185
1186
  * - `na` values in the source series are included in calculations and will produce an `na` result
1186
1187
  * - Useful for trend detection and prediction
@@ -1212,13 +1213,14 @@ export function linreg(source, length, offset = 0) {
1212
1213
  result.push(NaN);
1213
1214
  }
1214
1215
  else {
1215
- // 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
1216
1218
  let sumX = 0;
1217
1219
  let sumY = 0;
1218
1220
  let sumXY = 0;
1219
1221
  let sumX2 = 0;
1220
1222
  for (let j = 0; j < len; j++) {
1221
- const x = j;
1223
+ const x = j + 1;
1222
1224
  const y = source[i - (len - 1 - j)];
1223
1225
  sumX += x;
1224
1226
  sumY += y;
@@ -1226,10 +1228,8 @@ export function linreg(source, length, offset = 0) {
1226
1228
  sumX2 += x * x;
1227
1229
  }
1228
1230
  const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
1229
- const intercept = (sumY - slope * sumX) / len;
1230
- // Calculate linreg value at offset
1231
- const x = len - 1 - offset;
1232
- result.push(intercept + slope * x);
1231
+ const intercept = sumY / len - (slope * sumX) / len + slope;
1232
+ result.push(intercept + slope * (len - 1 - offset));
1233
1233
  }
1234
1234
  }
1235
1235
  }
@@ -1249,8 +1249,9 @@ export function linreg(source, length, offset = 0) {
1249
1249
  * - -1 = perfect negative correlation
1250
1250
  * - 0 = no correlation
1251
1251
  * - Measures linear relationship between two series
1252
- * - `na` values in the source series are ignored
1253
- * - 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`
1254
1255
  *
1255
1256
  * @example
1256
1257
  * ```typescript
@@ -1259,54 +1260,28 @@ export function linreg(source, length, offset = 0) {
1259
1260
  * ```
1260
1261
  */
1261
1262
  export function correlation(source1, source2, length) {
1262
- const result = [];
1263
- // the non-NaN pairs of the window, newest first (reused on every bar)
1264
- const span = length > 0 ? Math.ceil(length) : 0;
1265
- const a = new Float64Array(span);
1266
- const b = new Float64Array(span);
1267
- for (let i = 0; i < source1.length; i++) {
1268
- if (i < length - 1) {
1269
- result.push(NaN);
1270
- continue;
1271
- }
1272
- let m = 0;
1273
- for (let j = 0; j < length; j++) {
1274
- const v1 = source1[i - j];
1275
- const v2 = source2[i - j];
1276
- if (!isNaN(v1) && !isNaN(v2)) {
1277
- a[m] = v1;
1278
- b[m] = v2;
1279
- m++;
1280
- }
1281
- }
1282
- if (m === 0) {
1283
- result.push(NaN);
1284
- continue;
1285
- }
1286
- // Calculate means
1287
- let sum1 = 0;
1288
- let sum2 = 0;
1289
- for (let k = 0; k < m; k++) {
1290
- sum1 += a[k];
1291
- sum2 += b[k];
1292
- }
1293
- const mean1 = sum1 / m;
1294
- const mean2 = sum2 / m;
1295
- // Calculate correlation components
1296
- let numerator = 0;
1297
- let sum1Sq = 0;
1298
- let sum2Sq = 0;
1299
- for (let k = 0; k < m; k++) {
1300
- const dev1 = a[k] - mean1;
1301
- const dev2 = b[k] - mean2;
1302
- numerator += dev1 * dev2;
1303
- sum1Sq += dev1 * dev1;
1304
- sum2Sq += dev2 * dev2;
1305
- }
1306
- const denominator = Math.sqrt(sum1Sq * sum2Sq);
1307
- result.push(denominator === 0 ? NaN : numerator / denominator);
1308
- }
1309
- 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
+ });
1310
1285
  }
1311
1286
  /**
1312
1287
  * Percent Rank - percentage of how many previous values were less than or equal to current value.
@@ -1365,10 +1340,14 @@ export function percentrank(source, length) {
1365
1340
  * - Typical Price = (High + Low + Close) / 3
1366
1341
  * - Mean Deviation = Average of absolute differences from mean
1367
1342
  * - Scaled by 0.015 to provide more readable numbers
1368
- * - As in PineScript: when the mean deviation is 0 (1e-10 tolerance, a flat window), the CCI of the previous bar
1369
1343
  * - Values above +100 indicate overbought conditions
1370
1344
  * - Values below -100 indicate oversold conditions
1371
- * - \`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)
1372
1351
  *
1373
1352
  * @example
1374
1353
  * \`\`\`typescript
@@ -1378,22 +1357,17 @@ export function percentrank(source, length) {
1378
1357
  * \`\`\`
1379
1358
  */
1380
1359
  export function cci(source, length) {
1381
- const result = [];
1382
1360
  const smaValues = sma(source, length);
1383
1361
  const devValues = dev(source, length);
1384
- for (let i = 0; i < source.length; i++) {
1385
- if (isNaN(smaValues[i]) || isNaN(devValues[i])) {
1386
- result.push(NaN);
1387
- }
1388
- else if (eq(devValues[i], 0)) {
1389
- result.push(i > 0 ? result[i - 1] : NaN);
1390
- }
1391
- else {
1392
- const cci = (source[i] - smaValues[i]) / (0.015 * devValues[i]);
1393
- result.push(cci);
1394
- }
1395
- }
1396
- 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
+ });
1397
1371
  }
1398
1372
  /**
1399
1373
  * Stochastic Oscillator - momentum indicator comparing closing price to price range.
@@ -1409,8 +1383,8 @@ export function cci(source, length) {
1409
1383
  * - Measures where close is relative to the high-low range
1410
1384
  * - Values above 80 typically indicate overbought
1411
1385
  * - Values below 20 typically indicate oversold
1412
- * - Returns NaN when range is zero (high equals low)
1413
- * - \`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)
1414
1388
  *
1415
1389
  * @example
1416
1390
  * \`\`\`typescript
@@ -1423,20 +1397,15 @@ export function stoch(source, high, low, length) {
1423
1397
  const result = [];
1424
1398
  const lowestValues = lowest(low, length);
1425
1399
  const highestValues = highest(high, length);
1400
+ // PineScript: an na input or a zero range keeps the previous result
1401
+ let previous = NaN;
1426
1402
  for (let i = 0; i < source.length; i++) {
1427
- if (isNaN(lowestValues[i]) || isNaN(highestValues[i])) {
1428
- result.push(NaN);
1429
- }
1430
- else {
1431
- const range = highestValues[i] - lowestValues[i];
1432
- if (range === 0) {
1433
- result.push(NaN);
1434
- }
1435
- else {
1436
- const stochValue = 100 * (source[i] - lowestValues[i]) / range;
1437
- result.push(stochValue);
1438
- }
1439
- }
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);
1440
1409
  }
1441
1410
  return result;
1442
1411
  }
@@ -1455,7 +1424,13 @@ export function stoch(source, high, low, length) {
1455
1424
  * - Formula: 100 - (100 / (1 + Positive Money Flow / Negative Money Flow))
1456
1425
  * - Values above 80 indicate overbought
1457
1426
  * - Values below 20 indicate oversold
1458
- * - `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
1459
1434
  *
1460
1435
  * @example
1461
1436
  * ```typescript
@@ -1488,13 +1463,18 @@ export function mfi(source, length, volume) {
1488
1463
  // math.sum: the compensated running sum of the last `length` non-na values (as PineScript, bit for bit)
1489
1464
  const upper = runningSum(upperFlow, length);
1490
1465
  const lower = runningSum(lowerFlow, length);
1491
- return upper.map((u, i) => {
1492
- 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]);
1493
1471
  if (Number.isNaN(u) || Number.isNaN(l))
1494
1472
  return NaN;
1495
1473
  if (l === 0)
1496
1474
  return 100;
1497
- 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;
1498
1478
  });
1499
1479
  }
1500
1480
  /**
@@ -1862,10 +1842,12 @@ export function dmi(diLength, adxSmoothing, high, low, close) {
1862
1842
  * @param source - Series of values to process
1863
1843
  * @param shortLength - Short smoothing length
1864
1844
  * @param longLength - Long smoothing length
1865
- * @returns TSI series
1845
+ * @returns TSI series, a value in [-1, 1]
1866
1846
  *
1867
1847
  * @remarks
1868
- * - 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
1869
1851
  * - Positive values indicate bullish momentum
1870
1852
  * - Negative values indicate bearish momentum
1871
1853
  * - Crossovers of zero line can signal trend changes
@@ -1894,17 +1876,11 @@ export function tsi(source, shortLength, longLength) {
1894
1876
  // Double smooth absolute momentum
1895
1877
  const absMomentum = momentum.map(Math.abs);
1896
1878
  const smoothedAbsMomentum = ema(ema(absMomentum, longLength), shortLength);
1897
- // Calculate TSI
1898
- const result = [];
1899
- for (let i = 0; i < source.length; i++) {
1900
- if (smoothedAbsMomentum[i] === 0) {
1901
- result.push(0);
1902
- }
1903
- else {
1904
- result.push((smoothedMomentum[i] / smoothedAbsMomentum[i]) * 100);
1905
- }
1906
- }
1907
- 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
+ });
1908
1884
  }
1909
1885
  /**
1910
1886
  * Chande Momentum Oscillator - momentum indicator similar to RSI.
@@ -2053,6 +2029,10 @@ export function bbw(source, length, mult) {
2053
2029
  * @param length - Lookback period (default: 14)
2054
2030
  * @returns Williams %R series
2055
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
+ *
2056
2036
  * @example
2057
2037
  * ```typescript
2058
2038
  * const wpr = ta.wpr(high, low, close, 14);
@@ -2061,32 +2041,14 @@ export function bbw(source, length, mult) {
2061
2041
  * ```
2062
2042
  */
2063
2043
  export function wpr(high, low, close, length = 14) {
2064
- const result = [];
2065
- for (let i = 0; i < close.length; i++) {
2066
- if (i < length - 1) {
2067
- result.push(NaN);
2068
- continue;
2069
- }
2070
- // Find highest high and lowest low in the period
2071
- let highestHigh = high[i - length + 1];
2072
- let lowestLow = low[i - length + 1];
2073
- for (let j = i - length + 2; j <= i; j++) {
2074
- if (high[j] > highestHigh)
2075
- highestHigh = high[j];
2076
- if (low[j] < lowestLow)
2077
- lowestLow = low[j];
2078
- }
2079
- const range = highestHigh - lowestLow;
2080
- if (range === 0) {
2081
- result.push(NaN);
2082
- }
2083
- else {
2084
- // Formula: (Highest High - Close) / (Highest High - Lowest Low) * -100
2085
- const wprValue = ((highestHigh - close[i]) / range) * -100;
2086
- result.push(wprValue);
2087
- }
2088
- }
2089
- 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
+ });
2090
2052
  }
2091
2053
  export function vwap(source, volume, anchor, stdev_mult) {
2092
2054
  if (source.length !== volume.length) {
@@ -2095,18 +2057,20 @@ export function vwap(source, volume, anchor, stdev_mult) {
2095
2057
  const mid = [];
2096
2058
  const upper = [];
2097
2059
  const lower = [];
2098
- let sumPV = 0;
2099
- let sumV = 0;
2100
- 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;
2101
2064
  for (let i = 0; i < source.length; i++) {
2102
- if (anchor && anchor[i]) {
2103
- sumPV = 0;
2104
- sumV = 0;
2105
- sumPPV = 0;
2106
- }
2107
2065
  const s = source[i];
2108
2066
  const v = volume[i];
2109
- 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
2110
2074
  sumPV += s * v;
2111
2075
  sumV += v;
2112
2076
  sumPPV += s * s * v;
@@ -2173,7 +2137,8 @@ export function alma(source, length = 9, offset = 0.85, sigma = 6, floor = false
2173
2137
  /**
2174
2138
  * Keltner Channels Width (KCW)
2175
2139
  *
2176
- * 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).
2177
2142
  * Similar to BBW but uses ATR instead of standard deviation.
2178
2143
  *
2179
2144
  * Low KCW suggests consolidation/low volatility.
@@ -2203,8 +2168,8 @@ export function kcw(source, length = 20, mult = 2, useTrueRange = true, high, lo
2203
2168
  result.push(NaN);
2204
2169
  }
2205
2170
  else {
2206
- const width = ((upper[i] - lower[i]) / basis[i]) * 100;
2207
- result.push(width);
2171
+ // PineScript: (upper - lower) / basis, a ratio (not a percentage)
2172
+ result.push((upper[i] - lower[i]) / basis[i]);
2208
2173
  }
2209
2174
  }
2210
2175
  return result;
@@ -2281,46 +2246,32 @@ export function min(source) {
2281
2246
  * The Center of Gravity indicator is an oscillator developed by John Ehlers.
2282
2247
  * It identifies turning points with minimal lag and provides clear signals.
2283
2248
  *
2284
- * The COG calculates a weighted average where more recent prices have higher weights,
2285
- * similar to a moving average but with a focus on momentum shifts.
2286
- *
2287
2249
  * @param source - Source series (typically close)
2288
2250
  * @param length - Lookback period (default: 10)
2289
2251
  * @returns COG series
2290
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
+ *
2291
2260
  * @example
2292
2261
  * ```typescript
2293
2262
  * const cogValue = ta.cog(close, 10);
2294
- * // Use COG crossovers as signals:
2295
- * // - COG crossing above 0: potential buy signal
2296
- * // - COG crossing below 0: potential sell signal
2297
2263
  * ```
2298
2264
  */
2299
2265
  export function cog(source, length = 10) {
2300
- const result = [];
2301
- for (let i = 0; i < source.length; i++) {
2302
- if (i < length - 1) {
2303
- result.push(NaN);
2304
- continue;
2305
- }
2306
- let numerator = 0;
2307
- let denominator = 0;
2308
- for (let j = 0; j < length; j++) {
2309
- const weight = j + 1;
2310
- const price = source[i - length + 1 + j];
2311
- numerator += weight * price;
2312
- denominator += price;
2313
- }
2314
- if (denominator === 0) {
2315
- result.push(NaN);
2316
- }
2317
- else {
2318
- // COG formula: -1 * (sum of (weight * price) / sum of prices) + (length + 1) / 2
2319
- const cog = -1 * (numerator / denominator) + (length + 1) / 2;
2320
- result.push(cog);
2321
- }
2322
- }
2323
- 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
+ });
2324
2275
  }
2325
2276
  /**
2326
2277
  * Mode (Most Frequent Value)
@@ -2339,29 +2290,25 @@ export function cog(source, length = 10) {
2339
2290
  * ```
2340
2291
  *
2341
2292
  * @remarks
2342
- * - `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
2343
2297
  * - If no mode exists, returns the smallest value
2344
- * - Returns NaN for the first (length - 1) values where there's insufficient data
2345
2298
  */
2346
2299
  export function mode(source, length) {
2347
2300
  const result = [];
2301
+ const len = Math.floor(length);
2302
+ const seen = []; // non-na values so far
2348
2303
  for (let i = 0; i < source.length; i++) {
2349
- if (i < length - 1) {
2350
- result.push(NaN);
2351
- continue;
2352
- }
2353
- // Collect non-NaN values in the window
2354
- const values = [];
2355
- for (let j = 0; j < length; j++) {
2356
- const value = source[i - j];
2357
- if (!isNaN(value)) {
2358
- values.push(value);
2359
- }
2360
- }
2361
- if (values.length === 0) {
2304
+ const v = source[i];
2305
+ if (!Number.isNaN(v))
2306
+ seen.push(v);
2307
+ if (seen.length < len) {
2362
2308
  result.push(NaN);
2363
2309
  continue;
2364
2310
  }
2311
+ const values = seen.slice(seen.length - len);
2365
2312
  // Count frequency of each value
2366
2313
  const frequencyMap = new Map();
2367
2314
  for (const value of values) {
@@ -2510,79 +2457,75 @@ export function percentile_nearest_rank(source, length, percentage) {
2510
2457
  * - +100 indicates source consistently increased over the period
2511
2458
  * - -100 indicates source consistently decreased over the period
2512
2459
  * - 0 indicates no directional consistency
2513
- * - 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
2514
2466
  */
2515
2467
  export function rci(source, length) {
2468
+ const len = Math.floor(length);
2516
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);
2517
2474
  for (let i = 0; i < source.length; i++) {
2518
- if (i < length - 1) {
2519
- result.push(NaN);
2475
+ const v = source[i];
2476
+ if (Number.isNaN(v)) {
2477
+ result.push(i > 0 ? result[i - 1] : NaN);
2520
2478
  continue;
2521
2479
  }
2522
- // Collect values in the window
2523
- const values = [];
2524
- for (let j = 0; j < length; j++) {
2525
- values.push(source[i - length + 1 + j]);
2526
- }
2527
- // Check for NaN values
2528
- 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) {
2529
2483
  result.push(NaN);
2530
2484
  continue;
2531
2485
  }
2532
- // Create array of indices with their values for ranking
2533
- const indexed = values.map((value, index) => ({ value, index }));
2534
- // Sort by value to get ranks
2535
- const sorted = [...indexed].sort((a, b) => a.value - b.value);
2536
- // Assign ranks (handling ties by averaging ranks)
2537
- const ranks = new Array(length).fill(0);
2538
- let currentRank = 1;
2539
- for (let j = 0; j < sorted.length; j++) {
2540
- // Count ties
2541
- let tieCount = 1;
2542
- while (j + tieCount < sorted.length && sorted[j].value === sorted[j + tieCount].value) {
2543
- tieCount++;
2544
- }
2545
- // Average rank for ties
2546
- const avgRank = (currentRank + (currentRank + tieCount - 1)) / 2;
2547
- // Assign average rank to all tied values
2548
- for (let k = 0; k < tieCount; k++) {
2549
- ranks[sorted[j + k].index] = avgRank;
2550
- }
2551
- j += tieCount - 1;
2552
- currentRank += tieCount;
2553
- }
2554
- // Calculate D² = sum of (price_rank - time_rank)²
2555
- let sumSquaredDiff = 0;
2556
- for (let j = 0; j < length; j++) {
2557
- const timeRank = j + 1;
2558
- const diff = ranks[j] - timeRank;
2559
- sumSquaredDiff += diff * diff;
2560
- }
2561
- const n = length;
2562
- const base = (n * n * n - n) / 12;
2563
- // Compute tie correction for price ranks
2564
- // Count tied groups from the sorted array
2565
- let tieCorrection = 0;
2566
- for (let j = 0; j < sorted.length;) {
2567
- let tieCount = 1;
2568
- while (j + tieCount < sorted.length && sorted[j].value === sorted[j + tieCount].value) {
2569
- tieCount++;
2570
- }
2571
- if (tieCount > 1) {
2572
- tieCorrection += (tieCount * tieCount * tieCount - tieCount) / 12;
2573
- }
2574
- j += tieCount;
2575
- }
2576
- const A = base - tieCorrection; // Corrected for tied price ranks
2577
- const B = base; // Time ranks have no ties
2578
- let rho;
2579
- if (A === 0 || B === 0) {
2580
- rho = 0;
2581
- }
2582
- else {
2583
- 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;
2584
2523
  }
2585
- 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)));
2586
2529
  }
2587
2530
  return result;
2588
2531
  }
@@ -3127,14 +3070,19 @@ export function pvt(close, volume) {
3127
3070
  }
3128
3071
  /**
3129
3072
  * Accumulation/Distribution index: the running sum of
3130
- * `(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.
3131
3075
  */
3132
3076
  export function accdist(high, low, close, volume) {
3133
- return cum(close.map((c, i) => {
3077
+ let ad = 0;
3078
+ return close.map((c, i) => {
3134
3079
  const h = high[i];
3135
3080
  const l = low[i];
3136
- return h === l ? 0 : ((2 * c - l - h) / (h - l)) * volume[i];
3137
- }));
3081
+ const mfv = (((c - l) - (h - c)) / (h - l)) * volume[i];
3082
+ if (Number.isFinite(mfv))
3083
+ ad += mfv;
3084
+ return ad;
3085
+ });
3138
3086
  }
3139
3087
  /**
3140
3088
  * Intraday Intensity Index: `(2 * close - high - low) / (high - low) * volume`.