oakscriptjs 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/dist/cjs/array/index.d.ts +88 -27
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +257 -117
  4. package/dist/cjs/array/index.js.map +3 -3
  5. package/dist/cjs/callsite/index.d.ts +122 -4
  6. package/dist/cjs/callsite/index.d.ts.map +1 -1
  7. package/dist/cjs/callsite/index.js +102 -3
  8. package/dist/cjs/callsite/index.js.map +2 -2
  9. package/dist/cjs/color/index.d.ts +7 -3
  10. package/dist/cjs/color/index.d.ts.map +1 -1
  11. package/dist/cjs/color/index.js +6 -1
  12. package/dist/cjs/color/index.js.map +2 -2
  13. package/dist/cjs/index.d.ts +1 -1
  14. package/dist/cjs/index.js +1 -1
  15. package/dist/cjs/index.js.map +1 -1
  16. package/dist/cjs/label/index.d.ts.map +1 -1
  17. package/dist/cjs/label/index.js +2 -1
  18. package/dist/cjs/label/index.js.map +2 -2
  19. package/dist/cjs/linefill/index.d.ts.map +1 -1
  20. package/dist/cjs/linefill/index.js +1 -0
  21. package/dist/cjs/linefill/index.js.map +2 -2
  22. package/dist/cjs/math/index.d.ts +79 -22
  23. package/dist/cjs/math/index.d.ts.map +1 -1
  24. package/dist/cjs/math/index.js +157 -43
  25. package/dist/cjs/math/index.js.map +3 -3
  26. package/dist/cjs/matrix/index.d.ts.map +1 -1
  27. package/dist/cjs/matrix/index.js +42 -36
  28. package/dist/cjs/matrix/index.js.map +2 -2
  29. package/dist/cjs/runtime/series.d.ts +9 -0
  30. package/dist/cjs/runtime/series.d.ts.map +1 -1
  31. package/dist/cjs/runtime/series.js +16 -2
  32. package/dist/cjs/runtime/series.js.map +2 -2
  33. package/dist/cjs/script/index.d.ts +15 -31
  34. package/dist/cjs/script/index.d.ts.map +1 -1
  35. package/dist/cjs/script/index.js +32 -0
  36. package/dist/cjs/script/index.js.map +2 -2
  37. package/dist/cjs/session/calendar.d.ts.map +1 -1
  38. package/dist/cjs/session/calendar.js +15 -2
  39. package/dist/cjs/session/calendar.js.map +2 -2
  40. package/dist/cjs/str/index.d.ts +50 -38
  41. package/dist/cjs/str/index.d.ts.map +1 -1
  42. package/dist/cjs/str/index.js +43 -15
  43. package/dist/cjs/str/index.js.map +2 -2
  44. package/dist/cjs/str/messageformat.d.ts +2 -1
  45. package/dist/cjs/str/messageformat.d.ts.map +1 -1
  46. package/dist/cjs/str/messageformat.js +4 -1
  47. package/dist/cjs/str/messageformat.js.map +2 -2
  48. package/dist/cjs/str/numberformat.d.ts +4 -0
  49. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  50. package/dist/cjs/str/numberformat.js +26 -4
  51. package/dist/cjs/str/numberformat.js.map +3 -3
  52. package/dist/cjs/ta/index.d.ts +95 -46
  53. package/dist/cjs/ta/index.d.ts.map +1 -1
  54. package/dist/cjs/ta/index.js +193 -318
  55. package/dist/cjs/ta/index.js.map +3 -3
  56. package/dist/cjs/ta/running-sum.d.ts +54 -1
  57. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  58. package/dist/cjs/ta/running-sum.js +132 -47
  59. package/dist/cjs/ta/running-sum.js.map +2 -2
  60. package/dist/cjs/ta-series.d.ts +4 -4
  61. package/dist/cjs/ta-series.d.ts.map +1 -1
  62. package/dist/cjs/ta-series.js +4 -3
  63. package/dist/cjs/ta-series.js.map +2 -2
  64. package/dist/cjs/time/datestring.d.ts +1 -0
  65. package/dist/cjs/time/datestring.d.ts.map +1 -1
  66. package/dist/cjs/time/datestring.js +7 -0
  67. package/dist/cjs/time/datestring.js.map +2 -2
  68. package/dist/cjs/time/index.d.ts.map +1 -1
  69. package/dist/cjs/time/index.js +2 -1
  70. package/dist/cjs/time/index.js.map +2 -2
  71. package/dist/cjs/time/session.d.ts +28 -9
  72. package/dist/cjs/time/session.d.ts.map +1 -1
  73. package/dist/cjs/time/session.js +74 -28
  74. package/dist/cjs/time/session.js.map +2 -2
  75. package/dist/cjs/time/timezone.d.ts +5 -0
  76. package/dist/cjs/time/timezone.d.ts.map +1 -1
  77. package/dist/cjs/time/timezone.js +15 -1
  78. package/dist/cjs/time/timezone.js.map +2 -2
  79. package/dist/esm/array/index.d.ts +88 -27
  80. package/dist/esm/array/index.d.ts.map +1 -1
  81. package/dist/esm/array/index.js +408 -157
  82. package/dist/esm/array/index.js.map +1 -1
  83. package/dist/esm/callsite/index.d.ts +122 -4
  84. package/dist/esm/callsite/index.d.ts.map +1 -1
  85. package/dist/esm/callsite/index.js +228 -8
  86. package/dist/esm/callsite/index.js.map +1 -1
  87. package/dist/esm/color/index.d.ts +7 -3
  88. package/dist/esm/color/index.d.ts.map +1 -1
  89. package/dist/esm/color/index.js +15 -3
  90. package/dist/esm/color/index.js.map +1 -1
  91. package/dist/esm/index.d.ts +1 -1
  92. package/dist/esm/index.js +1 -1
  93. package/dist/esm/label/index.d.ts.map +1 -1
  94. package/dist/esm/label/index.js +2 -1
  95. package/dist/esm/label/index.js.map +1 -1
  96. package/dist/esm/linefill/index.d.ts.map +1 -1
  97. package/dist/esm/linefill/index.js +3 -0
  98. package/dist/esm/linefill/index.js.map +1 -1
  99. package/dist/esm/math/index.d.ts +79 -22
  100. package/dist/esm/math/index.d.ts.map +1 -1
  101. package/dist/esm/math/index.js +229 -63
  102. package/dist/esm/math/index.js.map +1 -1
  103. package/dist/esm/matrix/index.d.ts.map +1 -1
  104. package/dist/esm/matrix/index.js +57 -43
  105. package/dist/esm/matrix/index.js.map +1 -1
  106. package/dist/esm/runtime/series.d.ts +9 -0
  107. package/dist/esm/runtime/series.d.ts.map +1 -1
  108. package/dist/esm/runtime/series.js +18 -1
  109. package/dist/esm/runtime/series.js.map +1 -1
  110. package/dist/esm/script/index.d.ts +15 -31
  111. package/dist/esm/script/index.d.ts.map +1 -1
  112. package/dist/esm/script/index.js +44 -1
  113. package/dist/esm/script/index.js.map +1 -1
  114. package/dist/esm/session/calendar.d.ts.map +1 -1
  115. package/dist/esm/session/calendar.js +22 -2
  116. package/dist/esm/session/calendar.js.map +1 -1
  117. package/dist/esm/str/index.d.ts +50 -38
  118. package/dist/esm/str/index.d.ts.map +1 -1
  119. package/dist/esm/str/index.js +103 -52
  120. package/dist/esm/str/index.js.map +1 -1
  121. package/dist/esm/str/messageformat.d.ts +2 -1
  122. package/dist/esm/str/messageformat.d.ts.map +1 -1
  123. package/dist/esm/str/messageformat.js +7 -2
  124. package/dist/esm/str/messageformat.js.map +1 -1
  125. package/dist/esm/str/numberformat.d.ts +4 -0
  126. package/dist/esm/str/numberformat.d.ts.map +1 -1
  127. package/dist/esm/str/numberformat.js +38 -4
  128. package/dist/esm/str/numberformat.js.map +1 -1
  129. package/dist/esm/ta/index.d.ts +95 -46
  130. package/dist/esm/ta/index.d.ts.map +1 -1
  131. package/dist/esm/ta/index.js +341 -414
  132. package/dist/esm/ta/index.js.map +1 -1
  133. package/dist/esm/ta/running-sum.d.ts +54 -1
  134. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  135. package/dist/esm/ta/running-sum.js +166 -60
  136. package/dist/esm/ta/running-sum.js.map +1 -1
  137. package/dist/esm/ta-series.d.ts +4 -4
  138. package/dist/esm/ta-series.d.ts.map +1 -1
  139. package/dist/esm/ta-series.js +5 -4
  140. package/dist/esm/ta-series.js.map +1 -1
  141. package/dist/esm/time/datestring.d.ts +1 -0
  142. package/dist/esm/time/datestring.d.ts.map +1 -1
  143. package/dist/esm/time/datestring.js +12 -0
  144. package/dist/esm/time/datestring.js.map +1 -1
  145. package/dist/esm/time/index.d.ts.map +1 -1
  146. package/dist/esm/time/index.js +3 -1
  147. package/dist/esm/time/index.js.map +1 -1
  148. package/dist/esm/time/session.d.ts +28 -9
  149. package/dist/esm/time/session.d.ts.map +1 -1
  150. package/dist/esm/time/session.js +113 -42
  151. package/dist/esm/time/session.js.map +1 -1
  152. package/dist/esm/time/timezone.d.ts +5 -0
  153. package/dist/esm/time/timezone.d.ts.map +1 -1
  154. package/dist/esm/time/timezone.js +21 -1
  155. package/dist/esm/time/timezone.js.map +1 -1
  156. package/package.json +2 -2
  157. package/src/array/index.ts +382 -174
  158. package/src/callsite/index.ts +232 -8
  159. package/src/color/index.ts +15 -5
  160. package/src/index.ts +1 -1
  161. package/src/label/index.ts +2 -1
  162. package/src/linefill/index.ts +2 -0
  163. package/src/math/index.ts +261 -77
  164. package/src/matrix/index.ts +52 -52
  165. package/src/runtime/series.ts +22 -1
  166. package/src/script/index.ts +53 -1
  167. package/src/session/calendar.ts +22 -2
  168. package/src/str/index.ts +95 -51
  169. package/src/str/messageformat.ts +7 -2
  170. package/src/str/numberformat.ts +39 -4
  171. package/src/ta/index.ts +356 -458
  172. package/src/ta/running-sum.ts +187 -62
  173. package/src/ta-series.ts +8 -7
  174. package/src/time/datestring.ts +10 -0
  175. package/src/time/index.ts +3 -1
  176. package/src/time/session.ts +97 -47
  177. package/src/time/timezone.ts +21 -1
package/src/math/index.ts CHANGED
@@ -8,16 +8,17 @@
8
8
  *
9
9
  * Known difference: `log10`, `exp`, `log`, `pow`, `sin`, `cos`, `tan` use JavaScript's `Math`. PineScript uses
10
10
  * other algorithms, whose results differ by 1 or 2 units in the last place on part of the inputs (`log10` about 4 %,
11
- * `exp` about 9 %, `sin` / `cos` / `tan` about 3 %, `log` 0.4 %, `pow` 0.05 % of random inputs). A constant argument
12
- * (`math.log10(2)`) can differ by up to 3 units in the last place. This is visible only when a script amplifies
13
- * these tiny differences (e.g. `ma > ma[1]` on values of 1e195); `sqrt`, `atan`, `asin`, `acos` are equal.
11
+ * `exp` about 9 %, `sin` / `cos` / `tan` about 3 %, `log` 0.4 %, `pow` 0.05 % of random inputs). This is visible
12
+ * only when a script amplifies these tiny differences (e.g. `ma > ma[1]` on values of 1e195); `sqrt`, `atan`,
13
+ * `asin`, `acos` are equal. A constant argument (`math.log10(2)`) is computed before the script runs, with other
14
+ * rules: see `constant`.
14
15
  *
15
16
  * @version 6
16
17
  */
17
18
 
18
19
  import type { float, int, simple_int, series_float } from '../types/index.js';
19
20
  import { Series } from '../runtime/series.js';
20
- import { runningSum } from '../ta/running-sum.js';
21
+ import { runningSum, runningSumSeries } from '../ta/running-sum.js';
21
22
 
22
23
  /**
23
24
  * Returns the absolute value of a number.
@@ -147,16 +148,23 @@ export function floor(value: float | Series): int | Series {
147
148
  * @remarks
148
149
  * - Rounds to nearest integer by default
149
150
  * - With precision, rounds to that many decimal places
150
- * - Uses "round half up" strategy (0.5 rounds to 1)
151
151
  * - If value is a Series, returns a Series; if value is a number, returns a number
152
152
  *
153
+ * PineScript rules:
154
+ * - ties go away from zero (2.5 -> 3, -2.5 -> -3, -0.5 -> -1)
155
+ * - without a precision, or with a precision <= 0, the result is the integer rounding (1234.5678 @ -2 -> 1235)
156
+ * - a positive precision rounds only the fraction: `int + round(frac * scale) / scale` with
157
+ * `scale = 1 / 10^-precision` (precision above 16 acts as 16); the tie is decided on the exact value of
158
+ * `frac * scale` with a 1e-10 tolerance (1.005 @ 2 -> 1.01, 0.285 @ 2 -> 0.29, -1.45 @ 1 -> -1.5)
159
+ * - an infinite value gives na
160
+ *
153
161
  * @example
154
162
  * ```typescript
155
163
  * math.round(4.2) // Returns: 4
156
164
  * math.round(4.5) // Returns: 5
157
- * math.round(4.8) // Returns: 5
165
+ * math.round(-2.5) // Returns: -3
158
166
  * math.round(4.567, 2) // Returns: 4.57
159
- * math.round(4.567, 1) // Returns: 4.6
167
+ * math.round(1.005, 2) // Returns: 1.01
160
168
  * ```
161
169
  */
162
170
  export function round(value: Series, precision?: int): Series;
@@ -165,30 +173,118 @@ export function round(value: float | Series, precision?: int): float | Series {
165
173
  if (value instanceof Series) {
166
174
  const bars = value.bars;
167
175
  const valueArray = value.toArray();
168
- const length = bars.length;
169
-
170
176
  const result: number[] = [];
171
- if (precision === undefined) {
172
- for (let i = 0; i < length; i++) {
173
- const v = valueArray[i] ?? NaN;
174
- result.push(Math.round(v));
175
- }
176
- } else {
177
- const multiplier = Math.pow(10, precision);
178
- for (let i = 0; i < length; i++) {
179
- const v = valueArray[i] ?? NaN;
180
- result.push(Math.round(v * multiplier) / multiplier);
181
- }
177
+ for (let i = 0; i < bars.length; i++) {
178
+ result.push(roundValue(valueArray[i] ?? NaN, precision));
182
179
  }
183
-
184
180
  return Series.fromArray(bars, result);
185
181
  }
186
-
187
- if (precision === undefined) {
188
- return Math.round(value);
182
+ return roundValue(value, precision);
183
+ }
184
+
185
+ /** `0.5 - 1e-10` as the exact fraction TIE_HALF / TIE_SCALE: the tie threshold of round / round_to_mintick. */
186
+ const TIE_SCALE = 10n ** 10n;
187
+ const TIE_HALF = 5n * 10n ** 9n - 1n;
188
+ /** From 2^52 up every double is an integer. */
189
+ const INT_LIMIT = 2 ** 52;
190
+
191
+ /** Exact value of a finite double >= 0 as `[numerator, denominator]`. */
192
+ function exactRatio(x: number): [bigint, bigint] {
193
+ const view = new DataView(new ArrayBuffer(8));
194
+ view.setFloat64(0, x);
195
+ const hi = view.getUint32(0);
196
+ const lo = view.getUint32(4);
197
+ const expField = (hi >>> 20) & 0x7ff;
198
+ let mantissa = (BigInt(hi & 0xfffff) << 32n) | BigInt(lo);
199
+ let exp = -1074;
200
+ if (expField !== 0) {
201
+ mantissa |= 1n << 52n;
202
+ exp = expField - 1075;
203
+ }
204
+ return exp >= 0 ? [mantissa << BigInt(exp), 1n] : [mantissa, 1n << BigInt(-exp)];
205
+ }
206
+
207
+ /** True when `num / den - units` (exact) is >= 0.5 - 1e-10. */
208
+ function reachesTie(num: bigint, den: bigint, units: bigint): boolean {
209
+ return (num - units * den) * TIE_SCALE >= TIE_HALF * den;
210
+ }
211
+
212
+ /** `10^-p`: the exact literal for an integer p (correctly rounded), Math.pow otherwise. */
213
+ function tenToMinus(p: number): number {
214
+ return Number.isInteger(p) ? Number(`1e-${p}`) : Math.pow(10, -p);
215
+ }
216
+
217
+ /** PineScript math.round of one value (see `round`). */
218
+ function roundValue(value: number, precision?: number): number {
219
+ if (!Number.isFinite(value)) return NaN;
220
+ const negative = value < 0;
221
+ const magnitude = negative ? -value : value;
222
+ if (precision === undefined || Number.isNaN(precision) || precision <= 0) {
223
+ if (magnitude >= INT_LIMIT) return value;
224
+ let units = Math.floor(magnitude);
225
+ if (magnitude - units >= 0.5) units++;
226
+ return negative ? 0 - units : units;
227
+ }
228
+ const scale = 1 / tenToMinus(Math.min(precision, 16));
229
+ const fraction = magnitude % 1;
230
+ const scaled = fraction * scale;
231
+ let units = Math.floor(scaled);
232
+ const offset = scaled - units - 0.5;
233
+ // Only a near-tie needs the exact product; the window covers the double's own error.
234
+ const slack = 1e-9 + scaled * 1.8e-15;
235
+ if (-slack < offset && offset < slack) {
236
+ const [fn, fd] = exactRatio(fraction);
237
+ const [sn, sd] = exactRatio(scale);
238
+ if (reachesTie(fn * sn, fd * sd, BigInt(units))) units++;
239
+ } else if (offset > 0) {
240
+ units++;
189
241
  }
190
- const multiplier = Math.pow(10, precision);
191
- return Math.round(value * multiplier) / multiplier;
242
+ const result = magnitude - fraction + units / scale;
243
+ return negative ? 0 - result : result;
244
+ }
245
+
246
+ /** 10^16: a constant keeps 16 decimals. */
247
+ const CONSTANT_SCALE = 10n ** 16n;
248
+
249
+ /**
250
+ * Returns the value PineScript gives to a constant float expression (not a PineScript function).
251
+ *
252
+ * @param value - The result of the constant expression, computed in JavaScript (e.g. `1 / 255`)
253
+ * @returns The value of the same expression in a PineScript script
254
+ *
255
+ * @remarks
256
+ * PineScript computes a constant expression (only literals and constants: `1 / 255`, `0.1 * 3`,
257
+ * `math.sqrt(0.1)`) before the script runs, and the value is then rounded. JavaScript cannot tell a constant from a
258
+ * series value, so wrap the constant expressions of a script with this function.
259
+ * - |value| >= 0.001: the exact value of the double is rounded to 16 decimals, ties to even
260
+ * (`1 / 255` -> 0.003921568627451, `1 / 7` -> 0.1428571428571428)
261
+ * - |value| < 0.001, na and infinities: unchanged
262
+ * - the whole expression is computed first, then rounded once (`(1 / 7) * 7` is 1)
263
+ * - math functions of constants use JavaScript's `Math` there: `math.constant(math.sqrt(0.1))` is the PineScript
264
+ * value; `math.log10` of a constant is `math.log(x) / math.log(10)` there:
265
+ * `math.constant(math.log(2) / math.log(10))` -> 0.3010299956639811
266
+ * - the same expression with a series operand (`close / 255`) is not rounded: do not wrap it
267
+ *
268
+ * @example
269
+ * ```typescript
270
+ * // Pine: var const float i_255 = 1 / 255
271
+ * const i_255 = math.constant(1 / 255) // 0.003921568627451 (JavaScript: 0.00392156862745098)
272
+ * math.constant(1 / 7) // 0.1428571428571428 (JavaScript: 0.14285714285714285)
273
+ * math.constant(1 / 3) // 0.3333333333333333 (unchanged)
274
+ * ```
275
+ */
276
+ export function constant(value: float): float {
277
+ const magnitude = Math.abs(value);
278
+ // from 0.5 up, 16 decimals move a double by less than half its spacing: it stays the same
279
+ if (!(magnitude >= 1e-3 && magnitude < 0.5)) return value;
280
+ const [num, den] = exactRatio(magnitude);
281
+ const scaled = num * CONSTANT_SCALE;
282
+ let units = scaled / den;
283
+ const twiceRest = (scaled - units * den) * 2n;
284
+ if (twiceRest > den || (twiceRest === den && (units & 1n) === 1n)) units++;
285
+ const digits = units.toString().padStart(17, '0');
286
+ const result = Number(`${digits.slice(0, -16)}.${digits.slice(-16)}`);
287
+ return value < 0 ? -result : result;
192
288
  }
193
289
 
194
290
  /**
@@ -301,6 +397,8 @@ export function min(...values: (float | Series)[]): float | Series {
301
397
  * - Calculates sum of all values divided by count
302
398
  * - Returns NaN if no arguments provided (division by zero)
303
399
  * - If any argument is a Series, returns a Series; otherwise returns a number
400
+ * - PineScript rule: with 1 or 2 values the sum is plain; with 3 values or more it is a Kahan compensated sum whose
401
+ * pending correction is subtracted before the division (math.avg(0.1, 0.2, 0.3) = 0.19999999999999998)
304
402
  *
305
403
  * @example
306
404
  * ```typescript
@@ -313,10 +411,9 @@ export function min(...values: (float | Series)[]): float | Series {
313
411
  export function avg(...values: (float | Series)[]): float | Series {
314
412
  // Check if any value is a Series
315
413
  const hasSeries = values.some(v => v instanceof Series);
316
-
414
+
317
415
  if (!hasSeries) {
318
- // All scalars - calculate simple average
319
- return (values as number[]).reduce((a, b) => a + b, 0) / values.length;
416
+ return avgOf(values as number[]);
320
417
  }
321
418
 
322
419
  // At least one Series - return a Series
@@ -334,18 +431,35 @@ export function avg(...values: (float | Series)[]): float | Series {
334
431
  const nums = values.map((v, idx) =>
335
432
  valueArrays[idx] ? valueArrays[idx]![i] ?? NaN : (v as number)
336
433
  );
337
- const sum = nums.reduce((a, b) => a + b, 0);
338
- result.push(sum / nums.length);
434
+ result.push(avgOf(nums));
339
435
  }
340
-
436
+
341
437
  return Series.fromArray(bars, result);
342
438
  }
343
439
 
440
+ /** PineScript math.avg of numbers: plain sum up to 2 values, Kahan sum with the correction flushed from 3. */
441
+ function avgOf(nums: number[]): number {
442
+ if (nums.length <= 2) {
443
+ let sum = 0;
444
+ for (const n of nums) sum = sum + n;
445
+ return sum / nums.length;
446
+ }
447
+ let sum = 0;
448
+ let c = 0;
449
+ for (const n of nums) {
450
+ const y = n - c;
451
+ const t = sum + y;
452
+ c = t - sum - y;
453
+ sum = t;
454
+ }
455
+ return (sum - c) / nums.length;
456
+ }
457
+
344
458
  /**
345
459
  * Returns the sum of values over a sliding window.
346
460
  *
347
461
  * @param source - Series of values or array
348
- * @param length - Window length for summation
462
+ * @param length - Window length for summation, or one length per bar (a series length: array or Series)
349
463
  * @returns Series or array with rolling sum values
350
464
  *
351
465
  * @remarks
@@ -355,6 +469,11 @@ export function avg(...values: (float | Series)[]): float | Series {
355
469
  * - A compensated running sum, as in PineScript, so the result can differ from the exact window sum in the last
356
470
  * bits (bit for bit with PineScript)
357
471
  * - When passed a Series, returns a Series; when passed an array, returns an array
472
+ * - With a series length, PineScript moves the running sum to the new length when the length changes, with Kahan
473
+ * steps of the raw values: a smaller length removes the values that leave the window (oldest first); a larger
474
+ * length adds back the values from the one before the old window down to the one `length` values back (newest
475
+ * first), then the usual step removes the stored value of the one `length` values back. A bar with an `na` value
476
+ * gives the sum of the last `length` earlier values
358
477
  *
359
478
  * @example
360
479
  * ```typescript
@@ -362,14 +481,20 @@ export function avg(...values: (float | Series)[]): float | Series {
362
481
  * // Explanation: [NaN, NaN, 1+2+3, 2+3+4, 3+4+5]
363
482
  * ```
364
483
  */
365
- export function sum(source: Series, length: simple_int): Series;
366
- export function sum(source: series_float, length: simple_int): series_float;
367
- export function sum(source: series_float | Series, length: simple_int): series_float | Series {
484
+ export function sum(source: Series, length: simple_int | ArrayLike<number> | Series): Series;
485
+ export function sum(source: series_float, length: simple_int | ArrayLike<number> | Series): series_float;
486
+ export function sum(
487
+ source: series_float | Series,
488
+ length: simple_int | ArrayLike<number> | Series
489
+ ): series_float | Series {
368
490
  // Handle Series objects - extract to array first
369
491
  const sourceArray = source instanceof Series ? source.toArray() : source;
370
492
  // PineScript: the sum of the last `length` non-na values (also +/-Infinity skipped); an na bar keeps the previous
371
493
  // result. A compensated running sum, bit for bit as PineScript.
372
- const result: series_float = runningSum(sourceArray, length);
494
+ const result: series_float =
495
+ typeof length === 'number'
496
+ ? runningSum(sourceArray, length)
497
+ : runningSumSeries(sourceArray, length instanceof Series ? length.toArray() : length, 'math.sum');
373
498
 
374
499
  // If input was a Series, return a Series
375
500
  if (source instanceof Series) {
@@ -916,31 +1041,63 @@ export function todegrees(radians: float | Series): float | Series {
916
1041
  return radians * (180 / Math.PI);
917
1042
  }
918
1043
 
1044
+ /**
1045
+ * PineScript's seeded generator: Java's linear congruential generator (`java.util.Random`), `nextDouble()`.
1046
+ * Each generator draws one number per call.
1047
+ */
1048
+ export function seededRandom(seed: int): (min?: float, max?: float) => float {
1049
+ const mask = (1n << 48n) - 1n;
1050
+ let state = (BigInt(Math.trunc(seed)) ^ 0x5deece66dn) & mask;
1051
+ const next = (bits: bigint): bigint => {
1052
+ state = (0x5deece66dn * state + 11n) & mask;
1053
+ return state >> (48n - bits);
1054
+ };
1055
+ return (min = 0, max = 1) => {
1056
+ const hi = next(26n);
1057
+ const lo = next(27n);
1058
+ const unit = Number((hi << 27n) + lo) / 2 ** 53;
1059
+ return min + unit * (max - min);
1060
+ };
1061
+ }
1062
+
1063
+ const seeded = new Map<number, (min?: float, max?: float) => float>();
1064
+
1065
+ /** Starts every seeded sequence of `math.random` again (called at the start of a script run). */
1066
+ export function resetRandom(): void {
1067
+ seeded.clear();
1068
+ }
1069
+
919
1070
  /**
920
1071
  * Returns a pseudo-random number.
921
1072
  *
922
- * @param min - Optional minimum value (inclusive)
923
- * @param max - Optional maximum value (exclusive)
924
- * @param seed - Optional seed for deterministic random
1073
+ * @param min - Optional minimum value (inclusive), default 0
1074
+ * @param max - Optional maximum value (exclusive), default 1
1075
+ * @param seed - Optional seed: a repeatable sequence, as PineScript (Java's `java.util.Random`)
925
1076
  * @returns A random number in the specified range, or [0, 1) if no range specified
926
1077
  *
927
1078
  * @remarks
928
- * - Without arguments: returns value in [0, 1)
929
- * - With min and max: returns value in [min, max)
930
- * - **WARNING**: The seed parameter is accepted for API compatibility but NOT currently implemented
931
- * - This implementation always generates non-deterministic random values
932
- * - PineScript's version supports deterministic seeding for repeatable sequences
1079
+ * - Without a seed: `Math.random()`
1080
+ * - With a seed: PineScript's sequence, one number per call (measured: the call on each bar draws the next number
1081
+ * of `new java.util.Random(seed).nextDouble()` from the first bar of the history). PineScript keeps one generator
1082
+ * per call site; here there is one generator per seed value (started again by `executeScript`): for two call sites
1083
+ * with the same seed use `callsite.random(seed)`
933
1084
  *
934
1085
  * @example
935
1086
  * ```typescript
936
1087
  * math.random() // Returns: value between 0 and 1 (e.g., 0.234...)
937
1088
  * math.random(0, 10) // Returns: value between 0 and 10 (e.g., 7.45...)
938
- * math.random(5, 15) // Returns: value between 5 and 15 (e.g., 11.82...)
1089
+ * math.random(0, 1, 42) // Returns: 0.7275636800328681 on the first call, as PineScript
939
1090
  * ```
940
1091
  */
941
- export function random(min?: float, max?: float, _seed?: int): float {
942
- // TODO: Implement deterministic seeding to match PineScript behavior
943
- // Currently _seed parameter is ignored
1092
+ export function random(min?: float, max?: float, seed?: int): float {
1093
+ if (seed !== undefined && seed !== null && !Number.isNaN(seed)) {
1094
+ let gen = seeded.get(seed);
1095
+ if (!gen) {
1096
+ gen = seededRandom(seed);
1097
+ seeded.set(seed, gen);
1098
+ }
1099
+ return gen(min ?? 0, max ?? 1);
1100
+ }
944
1101
  const rand = Math.random();
945
1102
  if (min !== undefined && max !== undefined) {
946
1103
  return min + rand * (max - min);
@@ -952,12 +1109,13 @@ export function random(min?: float, max?: float, _seed?: int): float {
952
1109
  * Returns the sign of a number.
953
1110
  *
954
1111
  * @param value - The number to check or Series
955
- * @returns 1 for positive, -1 for negative, 0 for zero
1112
+ * @returns 1 for positive, -1 for negative, 0 for zero, na for na
956
1113
  *
957
1114
  * @remarks
958
1115
  * - Returns 1 if value > 0
959
1116
  * - Returns -1 if value < 0
960
1117
  * - Returns 0 if value = 0
1118
+ * - Returns na if value is na (as PineScript)
961
1119
  * - Useful for determining direction or polarity
962
1120
  * - If value is a Series, returns a Series; if value is a number, returns a number
963
1121
  *
@@ -967,6 +1125,7 @@ export function random(min?: float, max?: float, _seed?: int): float {
967
1125
  * math.sign(-3) // Returns: -1
968
1126
  * math.sign(0) // Returns: 0
969
1127
  * math.sign(0.001) // Returns: 1
1128
+ * math.sign(NaN) // Returns: NaN
970
1129
  * ```
971
1130
  */
972
1131
  export function sign(value: Series): Series;
@@ -980,13 +1139,13 @@ export function sign(value: float | Series): int | Series {
980
1139
  const result: number[] = [];
981
1140
  for (let i = 0; i < length; i++) {
982
1141
  const v = valueArray[i] ?? NaN;
983
- result.push(v > 0 ? 1 : v < 0 ? -1 : 0);
1142
+ result.push(v > 0 ? 1 : v < 0 ? -1 : v === 0 ? 0 : NaN);
984
1143
  }
985
1144
 
986
1145
  return Series.fromArray(bars, result);
987
1146
  }
988
1147
 
989
- return value > 0 ? 1 : value < 0 ? -1 : 0;
1148
+ return value > 0 ? 1 : value < 0 ? -1 : value === 0 ? 0 : NaN;
990
1149
  }
991
1150
 
992
1151
  /**
@@ -1001,9 +1160,14 @@ export function sign(value: float | Series): int | Series {
1001
1160
  * - **JavaScript signature**: Requires explicit `mintick` OR use `createContext()` with syminfo
1002
1161
  * - Rounds to the nearest value divisible by mintick
1003
1162
  * - Returns NaN for NaN input
1004
- * - Ties round up (0.5 -> 1)
1005
1163
  * - If number is a Series, returns a Series; if number is a float, returns a float
1006
1164
  *
1165
+ * PineScript rules:
1166
+ * - mintick is `minmove / pricescale` (here rebuilt from `mintick`: the smallest power of 10 that makes it an
1167
+ * integer); the position on the tick grid is `|number| * pricescale / minmove`
1168
+ * - ties go away from zero, decided on the exact value of that product with a 1e-10 tolerance
1169
+ * (-1.075 -> -1.08, 1.005 -> 1.01, 2.675 -> 2.68 at mintick 0.01)
1170
+ *
1007
1171
  * @example
1008
1172
  * ```typescript
1009
1173
  * // Direct call with explicit mintick
@@ -1021,35 +1185,19 @@ export function round_to_mintick(number: float | Series, mintick?: float): float
1021
1185
  if (number instanceof Series) {
1022
1186
  const bars = number.bars;
1023
1187
  const valueArray = number.toArray();
1024
- const length = bars.length;
1025
-
1026
1188
  if (mintick === undefined) {
1027
1189
  throw new Error(
1028
1190
  'math.round_to_mintick() requires mintick value. ' +
1029
1191
  'Either pass it explicitly or use createContext({ syminfo: { mintick } }) for implicit data.'
1030
1192
  );
1031
1193
  }
1032
-
1033
1194
  const result: number[] = [];
1034
- if (mintick === 0) {
1035
- // No rounding needed
1036
- for (let i = 0; i < length; i++) {
1037
- result.push(valueArray[i] ?? NaN);
1038
- }
1039
- } else {
1040
- for (let i = 0; i < length; i++) {
1041
- const v = valueArray[i];
1042
- if (v === undefined || isNaN(v)) {
1043
- result.push(NaN);
1044
- } else {
1045
- result.push(Math.round(v / mintick) * mintick);
1046
- }
1047
- }
1195
+ for (let i = 0; i < bars.length; i++) {
1196
+ result.push(roundToTick(valueArray[i] ?? NaN, mintick));
1048
1197
  }
1049
-
1050
1198
  return Series.fromArray(bars, result);
1051
1199
  }
1052
-
1200
+
1053
1201
  // Scalar case
1054
1202
  if (isNaN(number)) {
1055
1203
  return NaN;
@@ -1061,17 +1209,53 @@ export function round_to_mintick(number: float | Series, mintick?: float): float
1061
1209
  'Either pass it explicitly or use createContext({ syminfo: { mintick } }) for implicit data.'
1062
1210
  );
1063
1211
  }
1212
+ return roundToTick(number, mintick);
1213
+ }
1064
1214
 
1065
- if (mintick === 0) {
1066
- return number;
1215
+ /** `[minmove, pricescale]` of a mintick: pricescale is the smallest power of 10 (up to 1e15) giving an integer. */
1216
+ function tickPair(mintick: number): [number, number] | null {
1217
+ for (let k = 0; k <= 15; k++) {
1218
+ const pricescale = Number(`1e${k}`);
1219
+ const scaled = mintick * pricescale;
1220
+ const minmove = Math.round(scaled);
1221
+ if (minmove >= 1 && Math.abs(scaled - minmove) <= 1e-9 * minmove) return [minmove, pricescale];
1067
1222
  }
1223
+ return null;
1224
+ }
1068
1225
 
1069
- // Round to nearest tick
1070
- return Math.round(number / mintick) * mintick;
1226
+ /** PineScript math.round_to_mintick of one value (see `round_to_mintick`). */
1227
+ function roundToTick(number: number, mintick: number): number {
1228
+ if (!Number.isFinite(number)) return NaN;
1229
+ if (mintick === 0) return number;
1230
+ const pair = tickPair(mintick);
1231
+ const negative = number < 0;
1232
+ const magnitude = negative ? -number : number;
1233
+ if (pair === null) {
1234
+ // A tick that is no decimal fraction: the grid of the double mintick, ties away from zero.
1235
+ const scaled = magnitude / mintick;
1236
+ let units = Math.trunc(scaled);
1237
+ if (scaled - units >= 0.5) units++;
1238
+ return (negative ? 0 - units : units) * mintick;
1239
+ }
1240
+ const [minmove, pricescale] = pair;
1241
+ const scaled = (magnitude * pricescale) / minmove;
1242
+ let units = Math.trunc(scaled);
1243
+ const offset = scaled - units - 0.5;
1244
+ const slack = 1e-6 + scaled * 1.8e-15;
1245
+ if (-slack < offset && offset < slack) {
1246
+ const [n, d] = exactRatio(magnitude);
1247
+ const num = n * BigInt(pricescale);
1248
+ const den = d * BigInt(minmove);
1249
+ const whole = num / den;
1250
+ units = Number(whole) + (reachesTie(num, den, whole) ? 1 : 0);
1251
+ } else if (offset > 0) {
1252
+ units++;
1253
+ }
1254
+ return ((negative ? 0 - units : units) * minmove) / pricescale;
1071
1255
  }
1072
1256
 
1073
1257
  // Constants
1074
1258
  export const pi = Math.PI;
1075
1259
  export const e = Math.E;
1076
1260
  export const phi = 1.618033988749895; // Golden ratio
1077
- export const rphi = 0.618033988749895; // Reciprocal of golden ratio
1261
+ export const rphi = 0.6180339887498948; // Reciprocal of golden ratio: 1 / phi
@@ -61,6 +61,8 @@ export function new_matrix<T>(
61
61
  * ```
62
62
  */
63
63
  export function get<T>(id: PineMatrix<T>, row: simple_int, column: simple_int): T {
64
+ // PineScript: an na index gives na
65
+ if (Number.isNaN(row) || Number.isNaN(column)) return NaN as T;
64
66
  if (row < 0 || row >= id.rows || column < 0 || column >= id.columns) {
65
67
  throw new Error(`Matrix index out of bounds: [${row}, ${column}] for matrix of size [${id.rows}, ${id.columns}]`);
66
68
  }
@@ -92,6 +94,8 @@ export function get<T>(id: PineMatrix<T>, row: simple_int, column: simple_int):
92
94
  * ```
93
95
  */
94
96
  export function set<T>(id: PineMatrix<T>, row: simple_int, column: simple_int, value: T): void {
97
+ // PineScript: an na index changes nothing
98
+ if (Number.isNaN(row) || Number.isNaN(column)) return;
95
99
  if (row < 0 || row >= id.rows || column < 0 || column >= id.columns) {
96
100
  throw new Error(`Matrix index out of bounds: [${row}, ${column}] for matrix of size [${id.rows}, ${id.columns}]`);
97
101
  }
@@ -1884,15 +1888,8 @@ export function det(id: PineMatrix<float>): float {
1884
1888
  return 1; // Empty matrix has determinant 1 by convention
1885
1889
  }
1886
1890
 
1887
- if (n === 1) {
1888
- return id.data[0]![0]!;
1889
- }
1890
-
1891
- if (n === 2) {
1892
- return id.data[0]![0]! * id.data[1]![1]! - id.data[0]![1]! * id.data[1]![0]!;
1893
- }
1894
-
1895
- // LU decomposition with partial pivoting
1891
+ // LU decomposition with partial pivoting, for every size (as PineScript: det([[0.1, 0.2], [0.3, 0.4]]) is
1892
+ // -0.019999999999999993, not the closed form's -0.01999999999999999)
1896
1893
  // Make a copy to work with
1897
1894
  const a: float[][] = id.data.map(row => [...row]);
1898
1895
  let determinant = 1;
@@ -1918,9 +1915,10 @@ export function det(id: PineMatrix<float>): float {
1918
1915
  swapCount++;
1919
1916
  }
1920
1917
 
1921
- // If pivot is zero, determinant is zero
1918
+ // As PineScript (LU with an absolute singularity threshold of 1e-11): det(1e-11 * I3) is 1e-33, a pivot below
1919
+ // 1e-11 makes the determinant 0 (det([[1, 1], [1, 1 + 1e-12]]) and det([[1..9]]) are 0)
1922
1920
  const pivot = a[col]![col]!;
1923
- if (Math.abs(pivot) < EPSILON) {
1921
+ if (Math.abs(pivot) < 1e-11) {
1924
1922
  return 0;
1925
1923
  }
1926
1924
 
@@ -2161,51 +2159,53 @@ export function rank(id: PineMatrix<float>): int {
2161
2159
  if (id.rows === 0 || id.columns === 0) {
2162
2160
  return 0;
2163
2161
  }
2162
+ // As PineScript: the number of singular values above max(rows, columns) * largest * 2^-52 (rank([[1..9]]) is 2,
2163
+ // rank(1e-11 * I3) is 3, rank([[1, 1], [1, 1 + 1e-12]]) is 2)
2164
+ const s = singularValues(id.data);
2165
+ const largest = Math.max(...s);
2166
+ if (!(largest > 0)) return 0;
2167
+ const tol = Math.max(id.rows, id.columns) * largest * 2 ** -52;
2168
+ return s.filter((v) => v > tol).length;
2169
+ }
2164
2170
 
2165
- // Make a copy to work with
2166
- const a: float[][] = id.data.map(row => [...row]);
2167
- const m = id.rows;
2168
- const n = id.columns;
2169
-
2170
- let r = 0; // Current rank (and current pivot row)
2171
-
2172
- for (let col = 0; col < n && r < m; col++) {
2173
- // Find pivot
2174
- let maxRow = r;
2175
- let maxVal = Math.abs(a[r]![col]!);
2176
- for (let row = r + 1; row < m; row++) {
2177
- const absVal = Math.abs(a[row]![col]!);
2178
- if (absVal > maxVal) {
2179
- maxVal = absVal;
2180
- maxRow = row;
2181
- }
2182
- }
2183
-
2184
- // Skip column if all values below are effectively zero
2185
- if (maxVal < EPSILON) {
2186
- continue;
2187
- }
2188
-
2189
- // Swap rows if needed
2190
- if (maxRow !== r) {
2191
- const temp = a[r]!;
2192
- a[r] = a[maxRow]!;
2193
- a[maxRow] = temp;
2194
- }
2195
-
2196
- // Eliminate below pivot
2197
- const pivot = a[r]![col]!;
2198
- for (let row = r + 1; row < m; row++) {
2199
- const factor = a[row]![col]! / pivot;
2200
- for (let j = col; j < n; j++) {
2201
- a[row]![j] = a[row]![j]! - factor * a[r]![j]!;
2171
+ /** Singular values of a matrix (one-sided Jacobi rotations on the columns; accurate for small values). */
2172
+ function singularValues(data: float[][]): float[] {
2173
+ // work on the orientation with at least as many rows as columns
2174
+ const tall = data.length >= (data[0]?.length ?? 0);
2175
+ const a: float[][] = tall ? data.map((row) => [...row]) : data[0]!.map((_, j) => data.map((row) => row[j]!));
2176
+ const m = a.length;
2177
+ const n = a[0]!.length;
2178
+ for (let sweep = 0; sweep < 60; sweep++) {
2179
+ let rotated = false;
2180
+ for (let p = 0; p < n - 1; p++) {
2181
+ for (let q = p + 1; q < n; q++) {
2182
+ let alpha = 0;
2183
+ let beta = 0;
2184
+ let gamma = 0;
2185
+ for (let i = 0; i < m; i++) {
2186
+ const x = a[i]![p]!;
2187
+ const y = a[i]![q]!;
2188
+ alpha += x * x;
2189
+ beta += y * y;
2190
+ gamma += x * y;
2191
+ }
2192
+ if (gamma === 0 || Math.abs(gamma) <= 1e-15 * Math.sqrt(alpha * beta)) continue;
2193
+ rotated = true;
2194
+ const zeta = (beta - alpha) / (2 * gamma);
2195
+ const tan = Math.sign(zeta || 1) / (Math.abs(zeta) + Math.sqrt(1 + zeta * zeta));
2196
+ const cos = 1 / Math.sqrt(1 + tan * tan);
2197
+ const sin = cos * tan;
2198
+ for (let i = 0; i < m; i++) {
2199
+ const x = a[i]![p]!;
2200
+ const y = a[i]![q]!;
2201
+ a[i]![p] = cos * x - sin * y;
2202
+ a[i]![q] = sin * x + cos * y;
2203
+ }
2202
2204
  }
2203
2205
  }
2204
-
2205
- r++;
2206
+ if (!rotated) break;
2206
2207
  }
2207
-
2208
- return r;
2208
+ return Array.from({ length: n }, (_, j) => Math.sqrt(a.reduce((sum, row) => sum + row[j]! * row[j]!, 0)));
2209
2209
  }
2210
2210
 
2211
2211
  /**