oakscriptjs 0.10.1 → 0.11.0

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/box/index.d.ts.map +1 -1
  2. package/dist/cjs/box/index.js +7 -1
  3. package/dist/cjs/box/index.js.map +2 -2
  4. package/dist/cjs/callsite/index.d.ts +73 -32
  5. package/dist/cjs/callsite/index.d.ts.map +1 -1
  6. package/dist/cjs/callsite/index.js +69 -65
  7. package/dist/cjs/callsite/index.js.map +2 -2
  8. package/dist/cjs/drawing/registry.d.ts +75 -0
  9. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  10. package/dist/cjs/drawing/registry.js +139 -6
  11. package/dist/cjs/drawing/registry.js.map +2 -2
  12. package/dist/cjs/index.d.ts +6 -1
  13. package/dist/cjs/index.d.ts.map +1 -1
  14. package/dist/cjs/index.js +28 -1
  15. package/dist/cjs/index.js.map +2 -2
  16. package/dist/cjs/label/index.d.ts.map +1 -1
  17. package/dist/cjs/label/index.js +3 -0
  18. package/dist/cjs/label/index.js.map +2 -2
  19. package/dist/cjs/line/index.d.ts.map +1 -1
  20. package/dist/cjs/line/index.js +7 -1
  21. package/dist/cjs/line/index.js.map +2 -2
  22. package/dist/cjs/math/index.d.ts.map +1 -1
  23. package/dist/cjs/math/index.js +2 -0
  24. package/dist/cjs/math/index.js.map +2 -2
  25. package/dist/cjs/polyline/index.d.ts.map +1 -1
  26. package/dist/cjs/polyline/index.js +3 -0
  27. package/dist/cjs/polyline/index.js.map +2 -2
  28. package/dist/cjs/script/index.d.ts +30 -13
  29. package/dist/cjs/script/index.d.ts.map +1 -1
  30. package/dist/cjs/script/index.js +48 -9
  31. package/dist/cjs/script/index.js.map +3 -3
  32. package/dist/cjs/session/bars.d.ts +15 -1
  33. package/dist/cjs/session/bars.d.ts.map +1 -1
  34. package/dist/cjs/session/bars.js +41 -6
  35. package/dist/cjs/session/bars.js.map +3 -3
  36. package/dist/cjs/session/calendar.d.ts +9 -1
  37. package/dist/cjs/session/calendar.d.ts.map +1 -1
  38. package/dist/cjs/session/calendar.js +28 -2
  39. package/dist/cjs/session/calendar.js.map +2 -2
  40. package/dist/cjs/session/context.d.ts +75 -0
  41. package/dist/cjs/session/context.d.ts.map +1 -0
  42. package/dist/cjs/session/context.js +113 -0
  43. package/dist/cjs/session/context.js.map +7 -0
  44. package/dist/cjs/str/index.d.ts +3 -1
  45. package/dist/cjs/str/index.d.ts.map +1 -1
  46. package/dist/cjs/str/index.js +2 -1
  47. package/dist/cjs/str/index.js.map +2 -2
  48. package/dist/cjs/str/numberformat.d.ts +4 -2
  49. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  50. package/dist/cjs/str/numberformat.js +1 -1
  51. package/dist/cjs/str/numberformat.js.map +2 -2
  52. package/dist/cjs/ta/index.d.ts +33 -17
  53. package/dist/cjs/ta/index.d.ts.map +1 -1
  54. package/dist/cjs/ta/index.js +70 -29
  55. package/dist/cjs/ta/index.js.map +2 -2
  56. package/dist/cjs/ta/limits.d.ts +24 -0
  57. package/dist/cjs/ta/limits.d.ts.map +1 -0
  58. package/dist/cjs/ta/limits.js +50 -0
  59. package/dist/cjs/ta/limits.js.map +7 -0
  60. package/dist/cjs/ta/running-sum.d.ts +0 -16
  61. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  62. package/dist/cjs/ta/running-sum.js +2 -0
  63. package/dist/cjs/ta/running-sum.js.map +2 -2
  64. package/dist/cjs/ta-series.d.ts +7 -7
  65. package/dist/cjs/ta-series.d.ts.map +1 -1
  66. package/dist/cjs/ta-series.js +9 -7
  67. package/dist/cjs/ta-series.js.map +2 -2
  68. package/dist/cjs/time/index.d.ts +5 -2
  69. package/dist/cjs/time/index.d.ts.map +1 -1
  70. package/dist/cjs/time/index.js +2 -2
  71. package/dist/cjs/time/index.js.map +2 -2
  72. package/dist/cjs/time/session.d.ts +17 -1
  73. package/dist/cjs/time/session.d.ts.map +1 -1
  74. package/dist/cjs/time/session.js +61 -7
  75. package/dist/cjs/time/session.js.map +2 -2
  76. package/dist/cjs/types/metadata.d.ts +27 -3
  77. package/dist/cjs/types/metadata.d.ts.map +1 -1
  78. package/dist/cjs/types/metadata.js.map +1 -1
  79. package/dist/esm/box/index.d.ts.map +1 -1
  80. package/dist/esm/box/index.js +8 -2
  81. package/dist/esm/box/index.js.map +1 -1
  82. package/dist/esm/callsite/index.d.ts +73 -32
  83. package/dist/esm/callsite/index.d.ts.map +1 -1
  84. package/dist/esm/callsite/index.js +143 -106
  85. package/dist/esm/callsite/index.js.map +1 -1
  86. package/dist/esm/drawing/registry.d.ts +75 -0
  87. package/dist/esm/drawing/registry.d.ts.map +1 -1
  88. package/dist/esm/drawing/registry.js +237 -6
  89. package/dist/esm/drawing/registry.js.map +1 -1
  90. package/dist/esm/index.d.ts +6 -1
  91. package/dist/esm/index.d.ts.map +1 -1
  92. package/dist/esm/index.js +10 -1
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/label/index.d.ts.map +1 -1
  95. package/dist/esm/label/index.js +4 -1
  96. package/dist/esm/label/index.js.map +1 -1
  97. package/dist/esm/line/index.d.ts.map +1 -1
  98. package/dist/esm/line/index.js +8 -2
  99. package/dist/esm/line/index.js.map +1 -1
  100. package/dist/esm/math/index.d.ts.map +1 -1
  101. package/dist/esm/math/index.js +3 -0
  102. package/dist/esm/math/index.js.map +1 -1
  103. package/dist/esm/polyline/index.d.ts.map +1 -1
  104. package/dist/esm/polyline/index.js +5 -1
  105. package/dist/esm/polyline/index.js.map +1 -1
  106. package/dist/esm/script/index.d.ts +30 -13
  107. package/dist/esm/script/index.d.ts.map +1 -1
  108. package/dist/esm/script/index.js +54 -11
  109. package/dist/esm/script/index.js.map +1 -1
  110. package/dist/esm/session/bars.d.ts +15 -1
  111. package/dist/esm/session/bars.d.ts.map +1 -1
  112. package/dist/esm/session/bars.js +50 -6
  113. package/dist/esm/session/bars.js.map +1 -1
  114. package/dist/esm/session/calendar.d.ts +9 -1
  115. package/dist/esm/session/calendar.d.ts.map +1 -1
  116. package/dist/esm/session/calendar.js +35 -3
  117. package/dist/esm/session/calendar.js.map +1 -1
  118. package/dist/esm/session/context.d.ts +75 -0
  119. package/dist/esm/session/context.d.ts.map +1 -0
  120. package/dist/esm/session/context.js +122 -0
  121. package/dist/esm/session/context.js.map +1 -0
  122. package/dist/esm/str/index.d.ts +3 -1
  123. package/dist/esm/str/index.d.ts.map +1 -1
  124. package/dist/esm/str/index.js +8 -2
  125. package/dist/esm/str/index.js.map +1 -1
  126. package/dist/esm/str/numberformat.d.ts +4 -2
  127. package/dist/esm/str/numberformat.d.ts.map +1 -1
  128. package/dist/esm/str/numberformat.js +4 -2
  129. package/dist/esm/str/numberformat.js.map +1 -1
  130. package/dist/esm/ta/index.d.ts +33 -17
  131. package/dist/esm/ta/index.d.ts.map +1 -1
  132. package/dist/esm/ta/index.js +130 -56
  133. package/dist/esm/ta/index.js.map +1 -1
  134. package/dist/esm/ta/limits.d.ts +24 -0
  135. package/dist/esm/ta/limits.d.ts.map +1 -0
  136. package/dist/esm/ta/limits.js +42 -0
  137. package/dist/esm/ta/limits.js.map +1 -0
  138. package/dist/esm/ta/running-sum.d.ts +0 -16
  139. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  140. package/dist/esm/ta/running-sum.js +2 -0
  141. package/dist/esm/ta/running-sum.js.map +1 -1
  142. package/dist/esm/ta-series.d.ts +7 -7
  143. package/dist/esm/ta-series.d.ts.map +1 -1
  144. package/dist/esm/ta-series.js +9 -7
  145. package/dist/esm/ta-series.js.map +1 -1
  146. package/dist/esm/time/index.d.ts +5 -2
  147. package/dist/esm/time/index.d.ts.map +1 -1
  148. package/dist/esm/time/index.js +6 -3
  149. package/dist/esm/time/index.js.map +1 -1
  150. package/dist/esm/time/session.d.ts +17 -1
  151. package/dist/esm/time/session.d.ts.map +1 -1
  152. package/dist/esm/time/session.js +97 -10
  153. package/dist/esm/time/session.js.map +1 -1
  154. package/dist/esm/types/metadata.d.ts +27 -3
  155. package/dist/esm/types/metadata.d.ts.map +1 -1
  156. package/package.json +1 -1
  157. package/src/box/index.ts +8 -2
  158. package/src/callsite/index.ts +150 -100
  159. package/src/drawing/registry.ts +255 -5
  160. package/src/index.ts +34 -1
  161. package/src/label/index.ts +4 -1
  162. package/src/line/index.ts +8 -2
  163. package/src/math/index.ts +2 -0
  164. package/src/polyline/index.ts +3 -1
  165. package/src/script/index.ts +77 -18
  166. package/src/session/bars.ts +47 -6
  167. package/src/session/calendar.ts +37 -3
  168. package/src/session/context.ts +160 -0
  169. package/src/str/index.ts +7 -2
  170. package/src/str/numberformat.ts +5 -3
  171. package/src/ta/index.ts +148 -66
  172. package/src/ta/limits.ts +47 -0
  173. package/src/ta/running-sum.ts +2 -0
  174. package/src/ta-series.ts +16 -14
  175. package/src/time/index.ts +6 -3
  176. package/src/time/session.ts +89 -9
  177. package/src/types/metadata.ts +27 -3
package/src/ta/index.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  import type { series_float, series_bool, series_int, int, Source, simple_int, simple_float, simple_bool } from '../types/index.js';
12
12
  import { eq, ge, gt, le, lt } from '../compare/index.js';
13
13
  import { runningSum, runningSumSeries, runningVariance, runningVarianceSeries } from './running-sum.js';
14
+ import { checkSumLength, checkWindowLength } from './limits.js';
14
15
 
15
16
  /**
16
17
  * Simple Moving Average - returns the moving average (sum of last y values divided by y).
@@ -42,6 +43,7 @@ export function sma(source: Source, length: simple_int | ArrayLike<number>): ser
42
43
  }
43
44
  // Floor the length to match PineScript's auto-truncation of float to int
44
45
  const len = Math.floor(length);
46
+ checkSumLength('ta.sma', len, 0);
45
47
  return runningSum(source, len).map((sum) => sum / len);
46
48
  }
47
49
 
@@ -234,6 +236,7 @@ export function bb(
234
236
  length: simple_int,
235
237
  mult: simple_float
236
238
  ): [series_float, series_float, series_float] {
239
+ checkSumLength('ta.bb', length, 0);
237
240
  const basis = sma(series, length);
238
241
  const dev = stdev(series, length);
239
242
 
@@ -285,14 +288,20 @@ function lastValues(source: Source, i: number, length: number): number[] | null
285
288
 
286
289
  /**
287
290
  * The extreme of the window of `length` bars ending at each bar, in one pass (monotonic deque): the bar index of
288
- * the oldest highest (or lowest) value, or -1 when the window is empty. As PineScript does for ta.highest /
289
- * ta.lowest / ta.highestbars / ta.lowestbars, the window stops at the first `na` value going back, so a bar whose
290
- * own value is `na` has an empty window. `zeros[i]` tells whether the window holds a +0 (highest) or a -0
291
- * (lowest), the zero that `Math.max` / `Math.min` return when the extreme is 0.
291
+ * the oldest highest (or lowest) value (the newest one with `newest`), or -1 when the window is empty. The window
292
+ * stops at the first `na` value going back (the callers pass the non-na values only, see {@link keptValues}), and a
293
+ * fractional length is truncated (7.5 is 7 bars), as PineScript does.
294
+ * `zeros[i]` tells whether the window holds a +0 (highest) or a -0 (lowest), the zero that `Math.max` / `Math.min`
295
+ * return when the extreme is 0 (not kept with `newest`).
292
296
  */
293
- function windowExtremes(source: Source, length: number, lowest: boolean): { at: Int32Array; zeros: Uint8Array } {
297
+ function windowExtremes(
298
+ source: Source,
299
+ length: number,
300
+ lowest: boolean,
301
+ newest = false
302
+ ): { at: Int32Array; zeros: Uint8Array } {
294
303
  const n = source.length;
295
- const span = length > 0 ? Math.ceil(length) : 0; // the bars j = 0, 1... with j < length
304
+ const span = length > 0 ? Math.trunc(length) : 0; // PineScript casts the length to int
296
305
  const at = new Int32Array(n).fill(-1);
297
306
  const zeros = new Uint8Array(n);
298
307
  const queue = new Int32Array(n); // bar indices; values from best to worst, the oldest first among equal values
@@ -310,8 +319,10 @@ function windowExtremes(source: Source, length: number, lowest: boolean): { at:
310
319
  zeroCount = 0;
311
320
  continue;
312
321
  }
313
- // drop the values that the new one beats; equal values stay (the oldest wins ties)
314
- while (tail > head && (lowest ? source[queue[tail - 1]!]! > x : source[queue[tail - 1]!]! < x)) {
322
+ // drop the values that the new one beats; equal values stay (the oldest wins ties), or go with `newest`
323
+ while (tail > head) {
324
+ const last = source[queue[tail - 1]!]!;
325
+ if (!(lowest ? last > x : last < x) && !(newest && last === x)) break;
315
326
  if (isPreferredZero(queue[--tail]!)) zeroCount--;
316
327
  }
317
328
  queue[tail++] = i;
@@ -328,20 +339,79 @@ function windowExtremes(source: Source, length: number, lowest: boolean): { at:
328
339
  return { at, zeros };
329
340
  }
330
341
 
331
- /** Offset of the best value of the window; on ties the oldest bar wins. 0 when the window is empty. */
332
- function extremeOffset(source: Source, length: simple_int, lowest: boolean): series_int {
333
- const { at } = windowExtremes(source, length, lowest);
334
- // -(i - at): -0 when the extreme is the current bar, as the offset -j of the window scan
335
- return source.map((_, i) => (i < length - 1 ? NaN : at[i]! < 0 ? 0 : -(i - at[i]!)));
342
+ /**
343
+ * The non-na values of `source` with their bars, and for each bar the position of its last non-na value (-1 when
344
+ * there is none yet). PineScript's ta.highest / ta.lowest / ta.highestbars / ta.lowestbars / ta.pivothigh /
345
+ * ta.pivotlow keep the non-na values they receive and nothing else.
346
+ */
347
+ function keptValues(source: Source): { values: number[]; bars: number[]; last: Int32Array } {
348
+ const values: number[] = [];
349
+ const bars: number[] = [];
350
+ const last = new Int32Array(source.length);
351
+ for (let i = 0; i < source.length; i++) {
352
+ const x = source[i]!;
353
+ if (!Number.isNaN(x)) {
354
+ values.push(x);
355
+ bars.push(i);
356
+ }
357
+ last[i] = values.length - 1;
358
+ }
359
+ return { values, bars, last };
360
+ }
361
+
362
+ /**
363
+ * ta.highest / ta.lowest / ta.highestbars / ta.lowestbars with a series length (a length per bar): on each bar the
364
+ * best of the last `length[i]` non-na values (the newest one on ties), na before bar `length[i] - 1`, as PineScript
365
+ * (measured with a length that changes on each bar, with and without na in the source).
366
+ */
367
+ function extremeSeriesLength(source: Source, lengths: ArrayLike<number>, lowest: boolean, offsets: boolean): number[] {
368
+ const name = (lowest ? 'lowest' : 'highest') + (offsets ? 'bars' : '');
369
+ const { values, bars, last } = keptValues(source);
370
+ return Array.from(source, (_, i) => {
371
+ const len = Math.trunc(lengths[i]!);
372
+ checkWindowLength(name, len, i);
373
+ const k = last[i]!;
374
+ if (!(len >= 1) || i < len - 1 || k < 0) return NaN;
375
+ let best = k;
376
+ for (let j = k - 1; j >= 0 && j > k - len; j--) {
377
+ if (lowest ? values[j]! < values[best]! : values[j]! > values[best]!) best = j;
378
+ }
379
+ return offsets ? -(i - bars[best]!) : values[best]!;
380
+ });
381
+ }
382
+
383
+ /**
384
+ * Offset to the best of the last `length` non-na values (the newest one on ties): the distance in bars, so below
385
+ * `-(length - 1)` after an na value. na before bar `length - 1` and while there is no value.
386
+ */
387
+ function extremeOffset(source: Source, length: simple_int | ArrayLike<number>, lowest: boolean): series_int {
388
+ if (typeof length !== 'number') return extremeSeriesLength(source, length, lowest, true);
389
+ checkWindowLength(lowest ? 'lowestbars' : 'highestbars', length, 0);
390
+ const { values, bars, last } = keptValues(source);
391
+ const { at } = windowExtremes(values, length, lowest, true);
392
+ const len = Math.trunc(length);
393
+ // -(i - bar): -0 when the extreme is the current bar, as the offset -j of the window scan
394
+ return source.map((_, i) => {
395
+ const k = last[i]!;
396
+ return i < len - 1 || k < 0 || at[k]! < 0 ? NaN : -(i - bars[at[k]!]!);
397
+ });
336
398
  }
337
399
 
338
- /** Best value of the window (`na` when the window is empty), with the zero sign of `Math.max` / `Math.min`. */
339
- function extremeValue(source: Source, length: simple_int, lowest: boolean): series_float {
340
- const { at, zeros } = windowExtremes(source, length, lowest);
400
+ /**
401
+ * Best of the last `length` non-na values (of all of them when there are fewer), with the zero sign of `Math.max` /
402
+ * `Math.min`. na before bar `length - 1` and while there is no value.
403
+ */
404
+ function extremeValue(source: Source, length: simple_int | ArrayLike<number>, lowest: boolean): series_float {
405
+ if (typeof length !== 'number') return extremeSeriesLength(source, length, lowest, false);
406
+ checkWindowLength(lowest ? 'lowest' : 'highest', length, 0);
407
+ const { values, last } = keptValues(source);
408
+ const { at, zeros } = windowExtremes(values, length, lowest);
409
+ const len = Math.trunc(length);
341
410
  return source.map((_, i) => {
342
- if (i < length - 1 || at[i]! < 0) return NaN;
343
- const v = source[at[i]!]!;
344
- return v === 0 && zeros[i] ? (lowest ? -0 : 0) : v;
411
+ const k = last[i]!;
412
+ if (i < len - 1 || k < 0 || at[k]! < 0) return NaN;
413
+ const v = values[at[k]!]!;
414
+ return v === 0 && zeros[k] ? (lowest ? -0 : 0) : v;
345
415
  });
346
416
  }
347
417
 
@@ -763,6 +833,8 @@ function wmaAdd(a: number, b: number): number {
763
833
  * - Summed from the oldest value to the newest; as PineScript, a partial sum `s + t` with
764
834
  * `|s + t| <= 1e-10 * max(1, |s| + |t|)` is 0 (a cancellation residue, or values below 1e-10)
765
835
  * - More responsive to recent price changes than SMA
836
+ * - `length` can be an array (a length per bar, PineScript series length): each bar gives the result of its own
837
+ * length (`na` for a length below 1)
766
838
  *
767
839
  * @example
768
840
  * ```typescript
@@ -770,10 +842,10 @@ function wmaAdd(a: number, b: number): number {
770
842
  * // WMA gives more weight to recent prices
771
843
  * ```
772
844
  */
773
- export function wma(source: Source, length: simple_int): series_float {
845
+ export function wma(source: Source, length: simple_int | ArrayLike<number>): series_float {
774
846
  const result: series_float = [];
775
847
  // Floor the length to match PineScript's auto-truncation of float to int
776
- const len = Math.floor(length);
848
+ const fixed = typeof length === 'number' ? Math.floor(length) : NaN;
777
849
  // PineScript: na on a bar with an na source (also +/-Infinity) and until `len` non-na values were received;
778
850
  // otherwise the last `len` bars, each na replaced by the previous non-na value
779
851
  const filled: number[] = [];
@@ -787,7 +859,9 @@ export function wma(source: Source, length: simple_int): series_float {
787
859
  count++;
788
860
  }
789
861
  filled.push(last);
790
- if (isNa || count < len) {
862
+ // a series length: the window of this bar's length (the result of ta.wma with that fixed length)
863
+ const len = typeof length === 'number' ? fixed : Math.floor(length[i]!);
864
+ if (isNa || !(len >= 1) || count < len) {
791
865
  result.push(NaN);
792
866
  continue;
793
867
  }
@@ -812,11 +886,15 @@ export function wma(source: Source, length: simple_int): series_float {
812
886
  * @returns Highest value series (na on the first `length - 1` bars)
813
887
  *
814
888
  * @remarks
815
- * As in PineScript: an `na` value ends the window, so only the values after the last `na`
816
- * count, and a bar whose own value is `na` gives `na`.
889
+ * As in PineScript: `na` values are skipped. The result is the highest of the last `length` non-na values (of all
890
+ * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
891
+ * value was received. A fractional length is truncated (7.5 is 7 bars).
892
+ * `length` can be an array (a length per bar, PineScript series length), also for `ta.lowest`, `ta.highestbars`
893
+ * and `ta.lowestbars`: each bar uses its own length.
894
+ * A length above 15,000 throws the PineScript runtime error (also `ta.lowest`, `ta.highestbars`, `ta.lowestbars`).
817
895
  * PineScript `ta.highest(length)` (source = high) is `ta.highest(length)` in the script API.
818
896
  */
819
- export function highest(source: Source, length: simple_int): series_float {
897
+ export function highest(source: Source, length: simple_int | ArrayLike<number>): series_float {
820
898
  return extremeValue(source, length, false);
821
899
  }
822
900
 
@@ -828,10 +906,11 @@ export function highest(source: Source, length: simple_int): series_float {
828
906
  * @returns Lowest value series (na on the first `length - 1` bars)
829
907
  *
830
908
  * @remarks
831
- * As in PineScript: an `na` value ends the window, so only the values after the last `na`
832
- * count, and a bar whose own value is `na` gives `na`.
909
+ * As in PineScript: `na` values are skipped. The result is the lowest of the last `length` non-na values (of all
910
+ * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
911
+ * value was received.
833
912
  */
834
- export function lowest(source: Source, length: simple_int): series_float {
913
+ export function lowest(source: Source, length: simple_int | ArrayLike<number>): series_float {
835
914
  return extremeValue(source, length, true);
836
915
  }
837
916
 
@@ -1127,6 +1206,7 @@ function varianceOf(
1127
1206
  return biased ? result : result.map((v, i) => (Math.floor(length[i]!) > 1 ? v : NaN));
1128
1207
  }
1129
1208
  const len = Math.floor(length);
1209
+ checkSumLength(name, len, 0);
1130
1210
  // PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
1131
1211
  if ((biased ? len : len - 1) <= 0) return source.map(() => NaN);
1132
1212
  return runningVariance(source, len, biased);
@@ -1738,35 +1818,31 @@ export function sar(
1738
1818
  return result;
1739
1819
  }
1740
1820
 
1821
+ /** A pivot length: one value, or a value per bar (PineScript series length). */
1822
+ type PivotLength = simple_int | ArrayLike<number>;
1823
+
1741
1824
  /**
1742
1825
  * Pivot detection as PineScript computes it.
1743
1826
  *
1744
- * The value of the pivot bar is returned `rightbars` bars later, on the bar where the pivot is
1745
- * confirmed, so no future bar is used. A pivot high may equal values on its left but must be higher
1746
- * than every value on its right (a pivot low: equal on the left, lower on the right). An `na`
1747
- * neighbour ends the check on its side; an `na` pivot value gives no pivot.
1748
- */
1749
- function pivot(source: Source, leftbars: simple_int, rightbars: simple_int, isHigh: boolean): series_float {
1750
- // "beats" = the neighbour prevents the pivot
1751
- const beatsLeft = isHigh ? (x: number, v: number) => x > v : (x: number, v: number) => x < v;
1752
- const beatsRight = isHigh ? (x: number, v: number) => x >= v : (x: number, v: number) => x <= v;
1753
-
1754
- return source.map((_, i) => {
1755
- const center = i - rightbars;
1756
- if (center - leftbars < 0) return NaN;
1757
- const v = source[center]!;
1758
- if (Number.isNaN(v)) return NaN;
1759
- for (let j = 1; j <= leftbars; j++) {
1760
- const x = source[center - j]!;
1761
- if (Number.isNaN(x)) break;
1762
- if (beatsLeft(x, v)) return NaN;
1763
- }
1764
- for (let j = 1; j <= rightbars; j++) {
1765
- const x = source[center + j]!;
1766
- if (Number.isNaN(x)) break;
1767
- if (beatsRight(x, v)) return NaN;
1768
- }
1769
- return v;
1827
+ * The function keeps the non-na values it receives. On each bar it takes the last `leftbars + rightbars + 1` of them
1828
+ * (all of them when there are fewer) and finds the highest (the newest one on equal values; the lowest for a pivot
1829
+ * low): there is a pivot when that value is on the bar `rightbars` bars back, and its value is returned on this bar,
1830
+ * so no future bar is used. A pivot high may so equal values on its left but must be higher than every value on its
1831
+ * right. `na` before bar `leftbars + rightbars`.
1832
+ */
1833
+ function pivot(source: Source, leftbars: PivotLength, rightbars: PivotLength, isHigh: boolean): series_float {
1834
+ const { values, bars, last } = keptValues(source);
1835
+ return Array.from(source, (_, i) => {
1836
+ // a series length: the lengths of the current bar
1837
+ const left = Math.trunc(typeof leftbars === 'number' ? leftbars : leftbars[i]!);
1838
+ const right = Math.trunc(typeof rightbars === 'number' ? rightbars : rightbars[i]!);
1839
+ const k = last[i]!;
1840
+ if (!(i >= left + right) || k < 0) return NaN;
1841
+ let best = k;
1842
+ for (let j = k - 1; j >= 0 && j >= k - left - right; j--) {
1843
+ if (isHigh ? values[j]! > values[best]! : values[j]! < values[best]!) best = j;
1844
+ }
1845
+ return bars[best] === i - right ? values[best]! : NaN;
1770
1846
  });
1771
1847
  }
1772
1848
 
@@ -1784,7 +1860,10 @@ function pivot(source: Source, leftbars: simple_int, rightbars: simple_int, isHi
1784
1860
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1785
1861
  * index is `i - rightbars`
1786
1862
  * - A pivot high may equal values on its left but must be higher than all values on its right
1787
- * - An `na` neighbour ends the check on its side
1863
+ * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the highest of
1864
+ * them must be on the bar `rightbars` bars back
1865
+ * - `leftbars` / `rightbars` can be arrays (a length per bar, PineScript series length), also for `ta.pivotlow`:
1866
+ * each bar uses its own lengths
1788
1867
  *
1789
1868
  * @example
1790
1869
  * ```typescript
@@ -1794,8 +1873,8 @@ function pivot(source: Source, leftbars: simple_int, rightbars: simple_int, isHi
1794
1873
  */
1795
1874
  export function pivothigh(
1796
1875
  sourceOrLeftbars: Source | simple_int,
1797
- leftbarsOrRightbars: simple_int,
1798
- rightbars?: simple_int,
1876
+ leftbarsOrRightbars: PivotLength,
1877
+ rightbars?: PivotLength,
1799
1878
  high?: Source
1800
1879
  ): series_float {
1801
1880
  if (rightbars === undefined) {
@@ -1821,7 +1900,8 @@ export function pivothigh(
1821
1900
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1822
1901
  * index is `i - rightbars`
1823
1902
  * - A pivot low may equal values on its left but must be lower than all values on its right
1824
- * - An `na` neighbour ends the check on its side
1903
+ * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the lowest of
1904
+ * them must be on the bar `rightbars` bars back
1825
1905
  *
1826
1906
  * @example
1827
1907
  * ```typescript
@@ -1831,8 +1911,8 @@ export function pivothigh(
1831
1911
  */
1832
1912
  export function pivotlow(
1833
1913
  sourceOrLeftbars: Source | simple_int,
1834
- leftbarsOrRightbars: simple_int,
1835
- rightbars?: simple_int,
1914
+ leftbarsOrRightbars: PivotLength,
1915
+ rightbars?: PivotLength,
1836
1916
  low?: Source
1837
1917
  ): series_float {
1838
1918
  if (rightbars === undefined) {
@@ -2420,10 +2500,11 @@ export function range(source: Source, length: simple_int): series_float {
2420
2500
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2421
2501
  *
2422
2502
  * @remarks
2423
- * As in PineScript: on equal values the oldest bar wins; an `na` value ends the window;
2424
- * 0 when the window has no value.
2503
+ * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2504
+ * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2505
+ * `length - 1` and while no value was received; a fractional length is truncated.
2425
2506
  */
2426
- export function highestbars(source: Source, length: simple_int): series_int {
2507
+ export function highestbars(source: Source, length: simple_int | ArrayLike<number>): series_int {
2427
2508
  return extremeOffset(source, length, false);
2428
2509
  }
2429
2510
 
@@ -2432,10 +2513,11 @@ export function highestbars(source: Source, length: simple_int): series_int {
2432
2513
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2433
2514
  *
2434
2515
  * @remarks
2435
- * As in PineScript: on equal values the oldest bar wins; an `na` value ends the window;
2436
- * 0 when the window has no value.
2516
+ * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2517
+ * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2518
+ * `length - 1` and while no value was received; a fractional length is truncated.
2437
2519
  */
2438
- export function lowestbars(source: Source, length: simple_int): series_int {
2520
+ export function lowestbars(source: Source, length: simple_int | ArrayLike<number>): series_int {
2439
2521
  return extremeOffset(source, length, true);
2440
2522
  }
2441
2523
 
@@ -0,0 +1,47 @@
1
+ /**
2
+ * History limits of the window functions.
3
+ *
4
+ * PineScript stops a script when `ta.highest` / `ta.lowest` / `ta.highestbars` / `ta.lowestbars` ask for more than
5
+ * 15,000 bars (runtime error RE10004): with a constant length on the first bar, with a length that changes on the
6
+ * bar where it passes the limit. `math.sum`, `ta.sma`, `ta.variance`, `ta.stdev` and `ta.bb` have the same error
7
+ * above 5,000 bars; the message names the function that keeps the window ('stdev'->'variance', 'bb'->'sma').
8
+ */
9
+
10
+ /** Largest length of `ta.highest` / `ta.lowest` / `ta.highestbars` / `ta.lowestbars`. */
11
+ export const MAX_WINDOW_LENGTH = 15000;
12
+
13
+ /** Largest length of `math.sum` / `ta.sma` / `ta.variance` / `ta.stdev` / `ta.bb`. */
14
+ export const MAX_SUM_LENGTH = 5000;
15
+
16
+ /** Names of the PineScript message for the functions with the 5,000 limit ("a->b": a calls b). */
17
+ export const SUM_FUNCTIONS: Record<string, string> = {
18
+ 'math.sum': 'sum',
19
+ 'ta.sma': 'sma',
20
+ 'ta.variance': 'variance',
21
+ 'ta.stdev': 'stdev->variance',
22
+ 'ta.bb': 'bb->sma',
23
+ };
24
+
25
+ /**
26
+ * PineScript's runtime error RE10004 text; `bar` is the bar index of the call when it is known. `name` is the
27
+ * function, or a call path "stdev->variance" (printed 'stdev'->'variance').
28
+ */
29
+ export function windowError(name: string, length: number, bar?: number, max = MAX_WINDOW_LENGTH): string {
30
+ const fn = name.split('->').map((part) => `'${part}'`).join('->');
31
+ return (
32
+ (bar === undefined ? '' : `Error on bar ${bar}: `) +
33
+ `The ${fn} function references too many historical candles (${length}), the limit is ${max}.`
34
+ );
35
+ }
36
+
37
+ /** Throws RE10004 when the (truncated) length of a window function is above the limit. */
38
+ export function checkWindowLength(name: string, length: number, bar?: number, max = MAX_WINDOW_LENGTH): void {
39
+ const len = Math.trunc(length);
40
+ if (len > max) throw new Error(windowError(name, len, bar, max));
41
+ }
42
+
43
+ /** Throws RE10004 for a function of {@link SUM_FUNCTIONS} ("ta.sma", ...) with a length above 5,000. */
44
+ export function checkSumLength(name: string, length: number, bar?: number): void {
45
+ const fn = SUM_FUNCTIONS[name];
46
+ if (fn !== undefined) checkWindowLength(fn, length, bar, MAX_SUM_LENGTH);
47
+ }
@@ -14,6 +14,7 @@
14
14
  *
15
15
  * @internal
16
16
  */
17
+ import { checkSumLength } from './limits.js';
17
18
 
18
19
  /**
19
20
  * Whether the compensation `c` is (partly) lost when it is applied to `x`: `x + |c|` in floating point moves `x` by
@@ -177,6 +178,7 @@ function seriesLengths(lengths: ArrayLike<number>, count: number, name: string):
177
178
  return Array.from({ length: count }, (_, i) => {
178
179
  const len = Math.floor(lengths[i]!);
179
180
  if (!(len >= 1)) throw new Error(`${name}: length must be at least 1 (bar ${i}: ${lengths[i]})`);
181
+ checkSumLength(name, len, i);
180
182
  return len;
181
183
  });
182
184
  }
package/src/ta-series.ts CHANGED
@@ -40,10 +40,10 @@ export function ema(source: Series, length: number): Series {
40
40
  * @param length - Period length
41
41
  * @returns Series with WMA values
42
42
  */
43
- export function wma(source: Series, length: number): Series {
43
+ export function wma(source: Series, length: number | Series): Series {
44
44
  const bars = source.bars as Bar[];
45
45
  const sourceValues = source.toArray();
46
- const result = taCore.wma(sourceValues, length);
46
+ const result = taCore.wma(sourceValues, length instanceof Series ? length.toArray() : length);
47
47
  return Series.fromArray(bars, result);
48
48
  }
49
49
 
@@ -333,10 +333,10 @@ export function roc(source: Series, length: number): Series {
333
333
  * @param length - Period length
334
334
  * @returns Series with highest values
335
335
  */
336
- export function highest(source: Series, length: number): Series {
336
+ export function highest(source: Series, length: number | Series): Series {
337
337
  const bars = source.bars as Bar[];
338
338
  const sourceValues = source.toArray();
339
- const result = taCore.highest(sourceValues, length);
339
+ const result = taCore.highest(sourceValues, length instanceof Series ? length.toArray() : length);
340
340
  return Series.fromArray(bars, result);
341
341
  }
342
342
 
@@ -346,10 +346,10 @@ export function highest(source: Series, length: number): Series {
346
346
  * @param length - Period length
347
347
  * @returns Series with lowest values
348
348
  */
349
- export function lowest(source: Series, length: number): Series {
349
+ export function lowest(source: Series, length: number | Series): Series {
350
350
  const bars = source.bars as Bar[];
351
351
  const sourceValues = source.toArray();
352
- const result = taCore.lowest(sourceValues, length);
352
+ const result = taCore.lowest(sourceValues, length instanceof Series ? length.toArray() : length);
353
353
  return Series.fromArray(bars, result);
354
354
  }
355
355
 
@@ -688,9 +688,10 @@ export function hma(source: Series, length: number): Series {
688
688
  * @returns Series with the pivot high value on the bar where the pivot is confirmed,
689
689
  * `rightbars` bars after the pivot bar (as in PineScript), NaN elsewhere
690
690
  */
691
- export function pivothigh(source: Series, leftbars: number, rightbars: number): Series {
691
+ export function pivothigh(source: Series, leftbars: number | Series, rightbars: number | Series): Series {
692
692
  const bars = source.bars as Bar[];
693
- const result = taCore.pivothigh(source.toArray(), leftbars, rightbars);
693
+ const arr = (v: number | Series) => (v instanceof Series ? v.toArray() : v);
694
+ const result = taCore.pivothigh(source.toArray(), arr(leftbars), arr(rightbars));
694
695
  return Series.fromArray(bars, result);
695
696
  }
696
697
 
@@ -702,9 +703,10 @@ export function pivothigh(source: Series, leftbars: number, rightbars: number):
702
703
  * @returns Series with the pivot low value on the bar where the pivot is confirmed,
703
704
  * `rightbars` bars after the pivot bar (as in PineScript), NaN elsewhere
704
705
  */
705
- export function pivotlow(source: Series, leftbars: number, rightbars: number): Series {
706
+ export function pivotlow(source: Series, leftbars: number | Series, rightbars: number | Series): Series {
706
707
  const bars = source.bars as Bar[];
707
- const result = taCore.pivotlow(source.toArray(), leftbars, rightbars);
708
+ const arr = (v: number | Series) => (v instanceof Series ? v.toArray() : v);
709
+ const result = taCore.pivotlow(source.toArray(), arr(leftbars), arr(rightbars));
708
710
  return Series.fromArray(bars, result);
709
711
  }
710
712
 
@@ -861,9 +863,9 @@ export function kcw(bars: Bar[], source: Series, length: number = 20, mult: numb
861
863
  * @param length - Period length
862
864
  * @returns Series with bar offsets to highest value
863
865
  */
864
- export function highestbars(source: Series, length: number): Series {
866
+ export function highestbars(source: Series, length: number | Series): Series {
865
867
  const bars = source.bars as Bar[];
866
- const result = taCore.highestbars(source.toArray(), length);
868
+ const result = taCore.highestbars(source.toArray(), length instanceof Series ? length.toArray() : length);
867
869
  return Series.fromArray(bars, result);
868
870
  }
869
871
 
@@ -873,9 +875,9 @@ export function highestbars(source: Series, length: number): Series {
873
875
  * @param length - Period length
874
876
  * @returns Series with bar offsets to lowest value
875
877
  */
876
- export function lowestbars(source: Series, length: number): Series {
878
+ export function lowestbars(source: Series, length: number | Series): Series {
877
879
  const bars = source.bars as Bar[];
878
- const result = taCore.lowestbars(source.toArray(), length);
880
+ const result = taCore.lowestbars(source.toArray(), length instanceof Series ? length.toArray() : length);
879
881
  return Series.fromArray(bars, result);
880
882
  }
881
883
 
package/src/time/index.ts CHANGED
@@ -156,8 +156,11 @@ export function second(time: int, timezone: string): int {
156
156
  * ":" (1 = Sunday ... 7 = Saturday), e.g. "0930-1600:23456" or "1000-1100,1400-1500".
157
157
  * Without days, one period covers every day and several periods cover Monday to Friday.
158
158
  * A period includes its start and excludes its end. An overnight period ("1800-0930")
159
- * belongs to the day it ends. "0000-0000" and "24x7" cover the whole day.
159
+ * belongs to the day it ends. "0000-0000" and "24x7" cover the whole day. A period that ends at 0000 ends at 24:00 of
160
+ * the day it starts. On the days around a clock change the periods are placed as PineScript places them (see
161
+ * time/session.ts); `barMs`, the duration of the bars in ms, is needed for one of these rules (the day before the
162
+ * clocks go back).
160
163
  */
161
- export function inSession(time: int, session: string, timezone: string): boolean {
162
- return sessionContains(time, session, timezone);
164
+ export function inSession(time: int, session: string, timezone: string, barMs?: number): boolean {
165
+ return sessionContains(time, session, timezone, barMs);
163
166
  }