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
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);
@@ -1003,9 +1160,14 @@ export function sign(value: float | Series): int | Series {
1003
1160
  * - **JavaScript signature**: Requires explicit `mintick` OR use `createContext()` with syminfo
1004
1161
  * - Rounds to the nearest value divisible by mintick
1005
1162
  * - Returns NaN for NaN input
1006
- * - Ties round up (0.5 -> 1)
1007
1163
  * - If number is a Series, returns a Series; if number is a float, returns a float
1008
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
+ *
1009
1171
  * @example
1010
1172
  * ```typescript
1011
1173
  * // Direct call with explicit mintick
@@ -1023,35 +1185,19 @@ export function round_to_mintick(number: float | Series, mintick?: float): float
1023
1185
  if (number instanceof Series) {
1024
1186
  const bars = number.bars;
1025
1187
  const valueArray = number.toArray();
1026
- const length = bars.length;
1027
-
1028
1188
  if (mintick === undefined) {
1029
1189
  throw new Error(
1030
1190
  'math.round_to_mintick() requires mintick value. ' +
1031
1191
  'Either pass it explicitly or use createContext({ syminfo: { mintick } }) for implicit data.'
1032
1192
  );
1033
1193
  }
1034
-
1035
1194
  const result: number[] = [];
1036
- if (mintick === 0) {
1037
- // No rounding needed
1038
- for (let i = 0; i < length; i++) {
1039
- result.push(valueArray[i] ?? NaN);
1040
- }
1041
- } else {
1042
- for (let i = 0; i < length; i++) {
1043
- const v = valueArray[i];
1044
- if (v === undefined || isNaN(v)) {
1045
- result.push(NaN);
1046
- } else {
1047
- result.push(Math.round(v / mintick) * mintick);
1048
- }
1049
- }
1195
+ for (let i = 0; i < bars.length; i++) {
1196
+ result.push(roundToTick(valueArray[i] ?? NaN, mintick));
1050
1197
  }
1051
-
1052
1198
  return Series.fromArray(bars, result);
1053
1199
  }
1054
-
1200
+
1055
1201
  // Scalar case
1056
1202
  if (isNaN(number)) {
1057
1203
  return NaN;
@@ -1063,17 +1209,53 @@ export function round_to_mintick(number: float | Series, mintick?: float): float
1063
1209
  'Either pass it explicitly or use createContext({ syminfo: { mintick } }) for implicit data.'
1064
1210
  );
1065
1211
  }
1212
+ return roundToTick(number, mintick);
1213
+ }
1066
1214
 
1067
- if (mintick === 0) {
1068
- 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];
1069
1222
  }
1223
+ return null;
1224
+ }
1070
1225
 
1071
- // Round to nearest tick
1072
- 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;
1073
1255
  }
1074
1256
 
1075
1257
  // Constants
1076
1258
  export const pi = Math.PI;
1077
1259
  export const e = Math.E;
1078
1260
  export const phi = 1.618033988749895; // Golden ratio
1079
- 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
  /**
@@ -174,6 +174,17 @@ export class BarData {
174
174
  * const materialized = complex.materialize(); // Breaks closure chain
175
175
  * ```
176
176
  */
177
+ /** Largest history reference of a script series (PineScript max_bars_back limit). */
178
+ export const MAX_BARS_BACK = 5000;
179
+
180
+ /** PineScript's runtime error RE10008 text. */
181
+ export function historyError(bar: number, barsBack: number): string {
182
+ return (
183
+ `Error on bar ${bar}: The script attempts to reference historical data that is too far from the current bar ` +
184
+ `(${barsBack} bars back). The history-referencing length for the expression must be a value between 0 and ${MAX_BARS_BACK}.`
185
+ );
186
+ }
187
+
177
188
  export class Series {
178
189
  private extractor: SeriesExtractor;
179
190
  private dataSource: BarData;
@@ -186,6 +197,12 @@ export class Series {
186
197
  * @param data - Bar data (Bar[] or BarData)
187
198
  * @param extractor - Function to extract/compute value for each bar
188
199
  */
200
+ /**
201
+ * True for PineScript's built-in price variables (open, high, low, close, volume, hl2...): their history covers
202
+ * every bar, so references further than 5000 bars back are allowed (measured: close[bar_index] runs).
203
+ */
204
+ fullHistory = false;
205
+
189
206
  constructor(data: Bar[] | BarData, extractor: SeriesExtractor) {
190
207
  // Support both Bar[] and BarData for backward compatibility
191
208
  this.dataSource = data instanceof BarData ? data : new BarData(data);
@@ -219,7 +236,9 @@ export class Series {
219
236
  * @returns Series with extracted field values
220
237
  */
221
238
  static fromBars(bars: Bar[] | BarData, field: 'open' | 'high' | 'low' | 'close' | 'volume'): Series {
222
- return new Series(bars, (bar) => bar[field] ?? NaN);
239
+ const series = new Series(bars, (bar) => bar[field] ?? NaN);
240
+ series.fullHistory = true;
241
+ return series;
223
242
  }
224
243
 
225
244
  /**
@@ -516,6 +535,8 @@ export class Series {
516
535
  */
517
536
  offset(offset: number): Series {
518
537
  if (offset < 0) throw new RangeError(`Series.offset(${offset}): the offset cannot be negative (it would read a future bar).`);
538
+ // PineScript runtime error RE10008: a script series keeps at most 5000 bars of history
539
+ if (offset > MAX_BARS_BACK && !this.fullHistory) throw new Error(historyError(0, Math.floor(offset)));
519
540
  return new Series(this.dataSource, (_bar, i, data) => {
520
541
  const targetIndex = i - offset;
521
542
  if (targetIndex < 0 || targetIndex >= data.length) {