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
@@ -23,18 +23,49 @@ export function size<T>(id: PineArray<T>): int {
23
23
  return id.length;
24
24
  }
25
25
 
26
+ /** True for a PineScript `na` element: NaN, null or undefined. */
27
+ function isNa(v: unknown): boolean {
28
+ return v === undefined || v === null || (typeof v === 'number' && Number.isNaN(v));
29
+ }
30
+
31
+ /** The `na` of the array's element type: NaN for a numeric (or empty) array, undefined otherwise. */
32
+ function naOf<T>(id: PineArray<T>): T {
33
+ return (id.length === 0 || typeof id[0] === 'number' ? NaN : undefined) as T;
34
+ }
35
+
36
+ /**
37
+ * Position addressed by a PineScript index: truncated, a negative index counts from the end. An index outside
38
+ * `-size .. size - 1 + extra` is a runtime error.
39
+ */
40
+ function position(name: string, id: PineArray<unknown>, index: number, extra = 0): number {
41
+ const i = Math.trunc(index);
42
+ const n = id.length;
43
+ if (i < -n || i >= n + extra) {
44
+ throw new Error(`array.${name}: index ${i} is out of bounds, array size is ${n}`);
45
+ }
46
+ return i < 0 ? i + n : i;
47
+ }
48
+
26
49
  /**
27
- * Returns the element at the specified index
50
+ * Returns the element at the specified index.
51
+ *
52
+ * PineScript rules: a negative index counts from the end (`get(a, -1)` is the last element); an index outside
53
+ * `-size .. size - 1` is a runtime error (throws); an `na` index gives `na`.
28
54
  */
29
55
  export function get<T>(id: PineArray<T>, index: simple_int): T {
30
- return id[index]!;
56
+ if (Number.isNaN(index)) return naOf(id);
57
+ return id[position('get', id, index)]!;
31
58
  }
32
59
 
33
60
  /**
34
- * Sets the value of the element at the specified index
61
+ * Sets the value of the element at the specified index.
62
+ *
63
+ * PineScript rules: a negative index counts from the end; an index outside `-size .. size - 1` is a runtime error
64
+ * (throws); an `na` index does nothing.
35
65
  */
36
66
  export function set<T>(id: PineArray<T>, index: simple_int, value: T): void {
37
- id[index] = value;
67
+ if (Number.isNaN(index)) return;
68
+ id[position('set', id, index)] = value;
38
69
  }
39
70
 
40
71
  /**
@@ -73,38 +104,72 @@ export function clear<T>(id: PineArray<T>): void {
73
104
  }
74
105
 
75
106
  /**
76
- * Inserts a value at the specified index
107
+ * Inserts a value at the specified index.
108
+ *
109
+ * PineScript rules: a negative index counts from the end and `size` appends, so `-size .. size` is valid; any other
110
+ * index is a runtime error (throws); an `na` index appends.
77
111
  */
78
112
  export function insert<T>(id: PineArray<T>, index: simple_int, value: T): void {
79
- id.splice(index, 0, value);
113
+ if (Number.isNaN(index)) {
114
+ id.push(value);
115
+ return;
116
+ }
117
+ id.splice(position('insert', id, index, 1), 0, value);
80
118
  }
81
119
 
82
120
  /**
83
- * Removes the element at the specified index
121
+ * Removes the element at the specified index and returns it.
122
+ *
123
+ * PineScript rules: a negative index counts from the end; an index outside `-size .. size - 1` is a runtime error
124
+ * (throws); an `na` index removes nothing and gives `na`.
84
125
  */
85
126
  export function remove<T>(id: PineArray<T>, index: simple_int): T {
86
- return id.splice(index, 1)[0]!;
127
+ if (Number.isNaN(index)) return naOf(id);
128
+ return id.splice(position('remove', id, index), 1)[0]!;
129
+ }
130
+
131
+ /** PineScript `==` of two elements: numbers are equal when |a - b| <= 1e-10; `na` never equals anything. */
132
+ function pineEqual(a: unknown, b: unknown): boolean {
133
+ if (isNa(a) || isNa(b)) return false;
134
+ if (a === b) return true;
135
+ if (typeof a === 'number' && typeof b === 'number') {
136
+ const d = a - b;
137
+ return -1e-10 <= d && d <= 1e-10;
138
+ }
139
+ return false;
87
140
  }
88
141
 
89
142
  /**
90
- * Returns true if the array contains the value
143
+ * Returns true if the array contains the value.
144
+ *
145
+ * PineScript rules: numbers are compared with a 1e-10 tolerance (|a - b| <= 1e-10); `na` is never found.
91
146
  */
92
147
  export function includes<T>(id: PineArray<T>, value: T): bool {
93
- return id.includes(value);
148
+ return indexof(id, value) >= 0;
94
149
  }
95
150
 
96
151
  /**
97
- * Returns the index of the first occurrence of the value
152
+ * Returns the index of the first occurrence of the value, -1 when not found.
153
+ *
154
+ * PineScript rules: numbers are compared with a 1e-10 tolerance (|a - b| <= 1e-10); `na` is never found.
98
155
  */
99
156
  export function indexof<T>(id: PineArray<T>, value: T): int {
100
- return id.indexOf(value);
157
+ for (let i = 0; i < id.length; i++) {
158
+ if (pineEqual(id[i], value)) return i;
159
+ }
160
+ return -1;
101
161
  }
102
162
 
103
163
  /**
104
- * Returns the index of the last occurrence of the value
164
+ * Returns the index of the last occurrence of the value, -1 when not found.
165
+ *
166
+ * PineScript rules: numbers are compared with a 1e-10 tolerance (|a - b| <= 1e-10); `na` is never found.
105
167
  */
106
168
  export function lastindexof<T>(id: PineArray<T>, value: T): int {
107
- return id.lastIndexOf(value);
169
+ for (let i = id.length - 1; i >= 0; i--) {
170
+ if (pineEqual(id[i], value)) return i;
171
+ }
172
+ return -1;
108
173
  }
109
174
 
110
175
  /**
@@ -115,10 +180,11 @@ export function copy<T>(id: PineArray<T>): PineArray<T> {
115
180
  }
116
181
 
117
182
  /**
118
- * Concatenates two arrays
183
+ * Appends the elements of `id2` to `id1` and returns `id1` (PineScript `array.concat` changes `id1`).
119
184
  */
120
185
  export function concat<T>(id1: PineArray<T>, id2: PineArray<T>): PineArray<T> {
121
- return [...id1, ...id2] as PineArray<T>;
186
+ for (const v of Array.from(id2)) id1.push(v);
187
+ return id1;
122
188
  }
123
189
 
124
190
  /**
@@ -136,30 +202,132 @@ export function reverse<T>(id: PineArray<T>): void {
136
202
  }
137
203
 
138
204
  /**
139
- * Returns a slice of the array
205
+ * Returns the elements from `index_from` (included) to `index_to` (excluded, default: the size) as a live view of
206
+ * the array (PineScript `array.slice`).
207
+ *
208
+ * PineScript rules:
209
+ * - the slice shares the elements of the original array: a change of an element of the slice changes the original
210
+ * array, and adding / removing elements of the slice adds / removes them in the original array, inside the slice
211
+ * bounds (a push to the slice inserts at the end of the slice)
212
+ * - `index_from` must address an element (`0 .. size - 1`) and `index_to` may be `size`; other bounds, or
213
+ * `index_from > index_to`, are a runtime error (throws); an `na` bound means the start / the end of the array
140
214
  */
141
215
  export function slice<T>(id: PineArray<T>, index_from: simple_int, index_to?: simple_int): PineArray<T> {
142
- return id.slice(index_from, index_to) as PineArray<T>;
216
+ const n = id.length;
217
+ const start = Number.isNaN(index_from) ? 0 : Math.trunc(index_from);
218
+ const stop = index_to === undefined || Number.isNaN(index_to) ? n : Math.trunc(index_to);
219
+ if (start < 0 || start >= n) throw new Error(`array.slice: index ${start} is out of bounds, array size is ${n}`);
220
+ if (stop < 0 || stop > n) throw new Error(`array.slice: index ${stop} is out of bounds, array size is ${n}`);
221
+ if (start > stop) throw new Error(`array.slice: index_from ${start} is greater than index_to ${stop}`);
222
+ return sliceView(id, start, stop - start);
223
+ }
224
+
225
+ /** Index of a property key that is an array index, -1 otherwise. */
226
+ function indexKey(p: string | symbol): number {
227
+ if (typeof p !== 'string') return -1;
228
+ const i = Number(p);
229
+ return Number.isInteger(i) && i >= 0 && String(i) === p ? i : -1;
143
230
  }
144
231
 
145
232
  /**
146
- * Sorts array elements (numeric)
233
+ * Live view of `size` elements of `parent` from `start`: an array (a Proxy) whose element reads and writes go to
234
+ * `parent`, and whose size changes insert / remove elements in `parent` at the end of the view.
147
235
  */
148
- export function sort<T>(id: PineArray<T>, order: 'asc' | 'desc' = 'asc'): void {
149
- id.sort((a, b) => {
150
- if (order === 'asc') {
151
- return (a as any) - (b as any);
152
- } else {
153
- return (b as any) - (a as any);
154
- }
236
+ function sliceView<T>(parent: PineArray<T>, start: number, size: number): PineArray<T> {
237
+ const resize = (target: number): void => {
238
+ if (target > size) parent.splice(start + size, 0, ...Array<T>(target - size).fill(undefined as T));
239
+ else if (target < size) parent.splice(start + target, size - target);
240
+ size = target;
241
+ };
242
+ return new Proxy<T[]>([], {
243
+ get(target, p, receiver) {
244
+ const i = indexKey(p);
245
+ if (i >= 0) return i < size ? parent[start + i] : undefined;
246
+ if (p === 'length') return size;
247
+ return Reflect.get(target, p, receiver);
248
+ },
249
+ set(target, p, value, receiver) {
250
+ const i = indexKey(p);
251
+ if (i >= 0) {
252
+ if (i >= size) resize(i + 1);
253
+ parent[start + i] = value;
254
+ return true;
255
+ }
256
+ if (p === 'length') {
257
+ resize(Number(value));
258
+ return true;
259
+ }
260
+ return Reflect.set(target, p, value, receiver);
261
+ },
262
+ has(target, p) {
263
+ const i = indexKey(p);
264
+ return i >= 0 ? i < size : Reflect.has(target, p);
265
+ },
266
+ // Array methods delete the slots past the new end before they set the length, which does the removal.
267
+ deleteProperty(target, p) {
268
+ return indexKey(p) >= 0 ? true : Reflect.deleteProperty(target, p);
269
+ },
270
+ ownKeys(target) {
271
+ return [...Array.from({ length: size }, (_, i) => String(i)), ...Reflect.ownKeys(target)];
272
+ },
273
+ getOwnPropertyDescriptor(target, p) {
274
+ const i = indexKey(p);
275
+ if (i >= 0) {
276
+ return i < size ? { value: parent[start + i], writable: true, enumerable: true, configurable: true } : undefined;
277
+ }
278
+ if (p === 'length') return { value: size, writable: true, enumerable: false, configurable: false };
279
+ return Reflect.getOwnPropertyDescriptor(target, p);
280
+ },
155
281
  });
156
282
  }
157
283
 
284
+ /** Sort order of array.sort / array.sort_indices (PineScript `order.ascending` / `order.descending`). */
285
+ export type SortOrder = 'asc' | 'desc' | 'ascending' | 'descending';
286
+
287
+ /**
288
+ * Positions of the elements in PineScript sorted order: ascending values, `na` after the numbers (before the
289
+ * strings of a string array), stable; descending is the ascending result reversed.
290
+ */
291
+ function sortedPositions<T>(id: PineArray<T>, order: SortOrder): number[] {
292
+ const values: number[] = [];
293
+ const nas: number[] = [];
294
+ for (let i = 0; i < id.length; i++) (isNa(id[i]) ? nas : values).push(i);
295
+ values.sort((a, b) => {
296
+ const x = id[a] as unknown as number;
297
+ const y = id[b] as unknown as number;
298
+ return x < y ? -1 : x > y ? 1 : 0;
299
+ });
300
+ const naFirst = values.length > 0 && typeof id[values[0]!] === 'string';
301
+ const positions = naFirst ? [...nas, ...values] : [...values, ...nas];
302
+ if (order === 'desc' || order === 'descending') positions.reverse();
303
+ return positions;
304
+ }
305
+
306
+ /**
307
+ * Sorts the array in place.
308
+ *
309
+ * PineScript rules: `na` goes after the numbers in ascending order and before them in descending order (the
310
+ * ascending result reversed); in a string array `na` goes first in ascending order.
311
+ */
312
+ export function sort<T>(id: PineArray<T>, order: SortOrder = 'asc'): void {
313
+ const sorted = sortedPositions(id, order).map((p) => id[p]!);
314
+ for (let i = 0; i < sorted.length; i++) id[i] = sorted[i]!;
315
+ }
316
+
158
317
  /**
159
- * Returns the sum of all elements (numeric arrays only)
318
+ * Sum of the non-na elements (PineScript `array.sum(id)`), added from left to right; an array without values gives NaN.
319
+ * As PineScript, the running sum is set to 0 after a step when its absolute value is <= 1e-10.
160
320
  */
161
321
  export function sum(id: PineArray<float>): float {
162
- return id.reduce((acc, val) => acc + val, 0);
322
+ let total = 0;
323
+ let count = 0;
324
+ for (const v of id) {
325
+ if (Number.isNaN(v)) continue;
326
+ count++;
327
+ total += v;
328
+ if (Math.abs(total) <= 1e-10) total = 0;
329
+ }
330
+ return count ? total : NaN;
163
331
  }
164
332
 
165
333
  /**
@@ -170,79 +338,115 @@ export function avg(id: PineArray<float>): float {
170
338
  return values.length ? values.reduce((a, b) => a + b, 0) / values.length : NaN;
171
339
  }
172
340
 
341
+ /** The `nth` value of the non-na elements sorted with `compare` (array.min / array.max). */
342
+ function nthValue(name: string, id: PineArray<float>, nth: number, compare: (a: number, b: number) => number): float {
343
+ const rank = Number.isNaN(nth) ? 0 : Math.trunc(nth);
344
+ const values = id.filter((v) => !Number.isNaN(v));
345
+ if (!values.length) return NaN;
346
+ if (rank < 0 || rank >= id.length) {
347
+ throw new Error(`array.${name}: nth ${rank} is out of bounds, array size is ${id.length}`);
348
+ }
349
+ values.sort(compare);
350
+ return values[Math.min(rank, values.length - 1)]!;
351
+ }
352
+
173
353
  /**
174
354
  * The `nth` smallest value (0 = the smallest) of the non-na elements (PineScript `array.min(id, nth)`).
175
- * An `nth` past the end gives the greatest value; an array without values gives NaN.
355
+ *
356
+ * PineScript rules: an `nth` past the non-na values but inside the array size gives the greatest value; an `nth`
357
+ * outside `0 .. size - 1` is a runtime error (throws); an `na` nth counts as 0; an array without values gives NaN.
176
358
  */
177
359
  export function min(id: PineArray<float>, nth: simple_int = 0): float {
178
- const values = id.filter((v) => !Number.isNaN(v)).sort((a, b) => a - b);
179
- return values.length ? values[Math.min(nth, values.length - 1)]! : NaN;
360
+ return nthValue('min', id, nth, (a, b) => a - b);
180
361
  }
181
362
 
182
363
  /**
183
364
  * The `nth` greatest value (0 = the greatest) of the non-na elements (PineScript `array.max(id, nth)`).
184
- * An `nth` past the end gives the smallest value; an array without values gives NaN.
365
+ *
366
+ * PineScript rules: an `nth` past the non-na values but inside the array size gives the smallest value; an `nth`
367
+ * outside `0 .. size - 1` is a runtime error (throws); an `na` nth counts as 0; an array without values gives NaN.
185
368
  */
186
369
  export function max(id: PineArray<float>, nth: simple_int = 0): float {
187
- const values = id.filter((v) => !Number.isNaN(v)).sort((a, b) => b - a);
188
- return values.length ? values[Math.min(nth, values.length - 1)]! : NaN;
370
+ return nthValue('max', id, nth, (a, b) => b - a);
189
371
  }
190
372
 
191
373
  /**
192
- * Returns the median value (numeric arrays only)
374
+ * Median of the non-na elements (PineScript `array.median(id)`): the middle value, or the mean of the two middle
375
+ * values for an even count; an array without values gives NaN.
193
376
  */
194
377
  export function median(id: PineArray<float>): float {
195
- const sorted = [...id].sort((a, b) => a - b);
378
+ const sorted = id.filter((v) => !Number.isNaN(v)).sort((a, b) => a - b);
379
+ if (!sorted.length) return NaN;
196
380
  const mid = Math.floor(sorted.length / 2);
197
381
  return sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!;
198
382
  }
199
383
 
200
384
  /**
201
- * Returns the mode (most frequent value)
385
+ * Most frequent non-na element (PineScript `array.mode(id)`), compared exactly.
386
+ *
387
+ * PineScript rules: with several values of the same frequency, the smallest one; an array without values gives NaN.
202
388
  */
203
389
  export function mode(id: PineArray<float>): float {
204
- const frequency: Map<float, int> = new Map();
205
- let maxFreq = 0;
206
- let mode = id[0]!;
207
-
208
- for (const val of id) {
209
- const freq = (frequency.get(val) || 0) + 1;
210
- frequency.set(val, freq);
211
- if (freq > maxFreq) {
212
- maxFreq = freq;
213
- mode = val;
390
+ const frequency = new Map<float, int>();
391
+ for (const v of id) {
392
+ if (!Number.isNaN(v)) frequency.set(v, (frequency.get(v) ?? 0) + 1);
393
+ }
394
+ let best = NaN;
395
+ let bestCount = 0;
396
+ for (const [v, count] of frequency) {
397
+ if (count > bestCount || (count === bestCount && v < best)) {
398
+ best = v;
399
+ bestCount = count;
214
400
  }
215
401
  }
216
-
217
- return mode;
402
+ return best;
218
403
  }
219
404
 
220
405
  /**
221
- * Standard deviation of the non-na elements (PineScript `array.stdev(id, biased)`).
222
- * `biased` true (default) divides by the count, false by the count - 1.
406
+ * Standard deviation of the non-na elements (PineScript `array.stdev(id, biased)`): the square root of
407
+ * `array.variance(id, biased)`.
223
408
  */
224
409
  export function stdev(id: PineArray<float>, biased: bool = true): float {
225
410
  return Math.sqrt(variance(id, biased));
226
411
  }
227
412
 
413
+ /** Sum of the values and sum of their squares, both added from the first to the last value. */
414
+ function moments(values: number[]): [number, number] {
415
+ let p = 0;
416
+ let q = 0;
417
+ for (const v of values) p += v;
418
+ for (const v of values) q += v * v;
419
+ return [p, q];
420
+ }
421
+
228
422
  /**
229
423
  * Variance of the non-na elements (PineScript `array.variance(id, biased)`).
230
- * `biased` true (default) divides by the count, false by the count - 1.
424
+ *
425
+ * PineScript rule (moment form): with `p` the sum of the values, `q` the sum of their squares, `n` the count and
426
+ * `m = p / n`: biased (default) `max(0, q / n - m * m)`, unbiased `max(0, q / (n - 1) - (p / (n - 1)) * m)`.
427
+ * One value gives 0 biased and NaN unbiased; an array without values gives NaN.
231
428
  */
232
429
  export function variance(id: PineArray<float>, biased: bool = true): float {
233
430
  const values = id.filter((v) => !Number.isNaN(v));
234
- if (!values.length) return NaN;
235
- const mean = values.reduce((a, b) => a + b, 0) / values.length;
236
- const squares = values.reduce((a, b) => a + (b - mean) * (b - mean), 0);
237
- return squares / (biased ? values.length : values.length - 1);
431
+ const n = values.length;
432
+ if (!n) return NaN;
433
+ if (n < 2) return biased ? 0 : NaN;
434
+ const [p, q] = moments(values);
435
+ const mean = p / n;
436
+ return biased ? Math.max(0, q / n - mean * mean) : Math.max(0, q / (n - 1) - (p / (n - 1)) * mean);
238
437
  }
239
438
 
240
439
  /**
241
440
  * Fills the array with the specified value
242
441
  */
243
442
  export function fill<T>(id: PineArray<T>, value: T, index_from: simple_int = 0, index_to?: simple_int): void {
244
- const end = index_to ?? id.length;
245
- for (let i = index_from; i < end; i++) {
443
+ // PineScript: an na bound is the start / the end; a bound past the end is a runtime error
444
+ const start = index_from === undefined || index_from === null || Number.isNaN(index_from) ? 0 : index_from;
445
+ const end = index_to === undefined || index_to === null || Number.isNaN(index_to) ? id.length : index_to;
446
+ for (const i of [start, end]) {
447
+ if (i < 0 || i > id.length) throw new Error(`array.fill: index ${i} is out of bounds, array size is ${id.length}`);
448
+ }
449
+ for (let i = start; i < end; i++) {
246
450
  id[i] = value;
247
451
  }
248
452
  }
@@ -277,6 +481,8 @@ export function from<T>(...values: T[]): PineArray<T> {
277
481
  * ```
278
482
  */
279
483
  export function first<T>(id: PineArray<T>): T {
484
+ // PineScript: a runtime error on an empty array
485
+ if (id.length === 0) throw new Error('Cannot call array.first() if array is empty');
280
486
  return id[0]!;
281
487
  }
282
488
 
@@ -297,6 +503,8 @@ export function first<T>(id: PineArray<T>): T {
297
503
  * ```
298
504
  */
299
505
  export function last<T>(id: PineArray<T>): T {
506
+ // PineScript: a runtime error on an empty array (as array.first)
507
+ if (id.length === 0) throw new Error('Cannot call array.last() if array is empty');
300
508
  return id[id.length - 1]!;
301
509
  }
302
510
 
@@ -660,33 +868,36 @@ export function binary_search(id: PineArray<float>, val: float): int {
660
868
  * - Array must be sorted in ascending order
661
869
  * - For duplicate values, returns leftmost occurrence
662
870
  * - When value not found, returns position of next smaller element
871
+ * - PineScript rules: a value below every element gives 0; `na` counts as greater than every number (an array
872
+ * without na gives `size - 1`); the comparison is exact (no 1e-10 tolerance)
663
873
  */
664
874
  export function binary_search_leftmost(id: PineArray<float>, val: float): int {
665
- let left = 0;
666
- let right = id.length - 1;
667
- let result = -1;
668
- let found = false;
875
+ if (id.length === 0) return -1;
876
+ const at = searchBound(id, val, false);
877
+ return at < id.length && compareSorted(id[at]!, val) === 0 ? at : Math.max(0, at - 1);
878
+ }
669
879
 
670
- while (left <= right) {
671
- const mid = Math.floor((left + right) / 2);
880
+ /**
881
+ * Order of the PineScript binary searches: numbers in ascending order, na above every number (equal to na).
882
+ */
883
+ function compareSorted(a: float, b: float): number {
884
+ const an = Number.isNaN(a);
885
+ const bn = Number.isNaN(b);
886
+ if (an || bn) return an === bn ? 0 : an ? 1 : -1;
887
+ return a < b ? -1 : a > b ? 1 : 0;
888
+ }
672
889
 
673
- if (id[mid]! < val) {
674
- if (!found) {
675
- result = mid; // Track the last element smaller than val (only if not found)
676
- }
677
- left = mid + 1;
678
- } else if (id[mid]! === val) {
679
- result = mid;
680
- found = true;
681
- // Continue searching to the left for leftmost occurrence
682
- right = mid - 1;
683
- } else {
684
- // id[mid] > val
685
- right = mid - 1;
686
- }
890
+ /** First index whose element is >= `val` (`after` false) or > `val` (`after` true); `id.length` when none. */
891
+ function searchBound(id: PineArray<float>, val: float, after: boolean): int {
892
+ let lo = 0;
893
+ let hi = id.length;
894
+ while (lo < hi) {
895
+ const mid = (lo + hi) >>> 1;
896
+ const c = compareSorted(id[mid]!, val);
897
+ if (c < 0 || (after && c === 0)) lo = mid + 1;
898
+ else hi = mid;
687
899
  }
688
-
689
- return result;
900
+ return lo;
690
901
  }
691
902
 
692
903
  /**
@@ -717,33 +928,13 @@ export function binary_search_leftmost(id: PineArray<float>, val: float): int {
717
928
  * - Array must be sorted in ascending order
718
929
  * - For duplicate values, returns rightmost occurrence
719
930
  * - When value not found, returns position of next larger element
931
+ * - PineScript rules: a value above every element gives `size` (the position after the last element); `na` counts
932
+ * as greater than every number (an array without na gives `size`); the comparison is exact (no 1e-10 tolerance)
720
933
  */
721
934
  export function binary_search_rightmost(id: PineArray<float>, val: float): int {
722
- let left = 0;
723
- let right = id.length - 1;
724
- let result = -1;
725
- let found = false;
726
-
727
- while (left <= right) {
728
- const mid = Math.floor((left + right) / 2);
729
-
730
- if (id[mid]! <= val) {
731
- if (id[mid]! === val) {
732
- result = mid;
733
- found = true;
734
- }
735
- // Continue searching to the right for rightmost occurrence
736
- left = mid + 1;
737
- } else {
738
- // id[mid] > val
739
- if (!found) {
740
- result = mid; // Track the first element greater than val (only if not found)
741
- }
742
- right = mid - 1;
743
- }
744
- }
745
-
746
- return result;
935
+ if (id.length === 0) return -1;
936
+ const at = searchBound(id, val, true);
937
+ return at > 0 && compareSorted(id[at - 1]!, val) === 0 ? at - 1 : at;
747
938
  }
748
939
 
749
940
  /**
@@ -769,28 +960,36 @@ export function binary_search_rightmost(id: PineArray<float>, val: float): int {
769
960
  * - Unbiased (false): divides by n-1 (sample covariance)
770
961
  * - Returns NaN if arrays are empty or have different lengths
771
962
  * - Formula: Cov(X,Y) = E[(X - μX)(Y - μY)]
963
+ *
964
+ * PineScript rules: a pair where either value is `na` is skipped and the count is the number of pairs kept; both
965
+ * means first, then the sum of the products from the first to the last pair. One pair gives 0 biased, NaN unbiased.
772
966
  */
773
967
  export function covariance(id1: PineArray<float>, id2: PineArray<float>, biased: bool = true): float {
774
- if (id1.length === 0 || id2.length === 0) {
775
- return NaN;
776
- }
777
-
778
968
  if (id1.length !== id2.length) {
779
969
  return NaN;
780
970
  }
781
-
782
- const n = id1.length;
783
- const mean1 = avg(id1);
784
- const mean2 = avg(id2);
785
-
971
+ const xs: number[] = [];
972
+ const ys: number[] = [];
973
+ for (let i = 0; i < id1.length; i++) {
974
+ if (Number.isNaN(id1[i]!) || Number.isNaN(id2[i]!)) continue;
975
+ xs.push(id1[i]!);
976
+ ys.push(id2[i]!);
977
+ }
978
+ const n = xs.length;
979
+ if (!n || (!biased && n < 2)) return NaN;
980
+ let mean1 = 0;
981
+ let mean2 = 0;
982
+ for (let i = 0; i < n; i++) {
983
+ mean1 += xs[i]!;
984
+ mean2 += ys[i]!;
985
+ }
986
+ mean1 /= n;
987
+ mean2 /= n;
786
988
  let sum = 0;
787
989
  for (let i = 0; i < n; i++) {
788
- sum += (id1[i]! - mean1) * (id2[i]! - mean2);
990
+ sum += (xs[i]! - mean1) * (ys[i]! - mean2);
789
991
  }
790
-
791
- // Biased: divide by n, Unbiased: divide by n-1
792
- const divisor = biased ? n : n - 1;
793
- return sum / divisor;
992
+ return sum / (biased ? n : n - 1);
794
993
  }
795
994
 
796
995
  /**
@@ -816,23 +1015,28 @@ export function covariance(id1: PineArray<float>, id2: PineArray<float>, biased:
816
1015
  * - Returns NaN if array is empty
817
1016
  *
818
1017
  * PineScript rules:
819
- * - position in the sorted array: `percentage / 100 * size - 0.5`, clamped to the first / last value, linear
820
- * interpolation between the two neighbours
1018
+ * - 1-based position in the sorted array: `size * percentage / 100 + 0.5` (snapped to an integer within 1e-9),
1019
+ * clamped to the first / last value; between two values `lo * (1 - frac) + hi * frac`
821
1020
  * - `na` values are sorted after all numbers; with an `na` in the array, a position between two values gives `na`
1021
+ * - an `na` percentage gives `na`; a percentage outside 0 .. 100 is a runtime error (throws)
822
1022
  */
823
1023
  export function percentile_linear_interpolation(id: PineArray<float>, percentage: float): float {
824
- if (id.length === 0) {
1024
+ if (id.length === 0 || Number.isNaN(percentage)) {
825
1025
  return NaN;
826
1026
  }
1027
+ checkPercentage('percentile_linear_interpolation', percentage);
827
1028
  const s = sortedNaLast(id);
828
1029
  const n = s.length;
829
- const pos = (percentage / 100) * n - 0.5;
830
- if (pos <= 0) return s[0]!;
831
- if (pos >= n - 1) return s[n - 1]!;
832
- const lo = Math.floor(pos);
833
- const f = pos - lo;
834
- if (f === 0) return s[lo]!;
835
- return Number.isNaN(s[n - 1]!) ? NaN : s[lo]! + f * (s[lo + 1]! - s[lo]!);
1030
+ let pos = (n * percentage) / 100 + 0.5;
1031
+ const nearest = Math.round(pos);
1032
+ if (Math.abs(pos - nearest) < 1e-9) pos = nearest;
1033
+ if (pos <= 1) return s[0]!;
1034
+ if (pos >= n) return s[n - 1]!;
1035
+ const lower = Math.floor(pos);
1036
+ const frac = pos - lower;
1037
+ if (frac === 0) return s[lower - 1]!;
1038
+ if (Number.isNaN(s[n - 1]!)) return NaN;
1039
+ return s[lower - 1]! * (1 - frac) + s[lower]! * frac;
836
1040
  }
837
1041
 
838
1042
  /** Array values sorted ascending, `na` values after the numbers (array.percentile_*). */
@@ -841,6 +1045,13 @@ function sortedNaLast(id: PineArray<float>): number[] {
841
1045
  return [...numbers, ...Array<number>(id.length - numbers.length).fill(NaN)];
842
1046
  }
843
1047
 
1048
+ /** A percentage outside 0 .. 100 is a PineScript runtime error. */
1049
+ function checkPercentage(name: string, percentage: number): void {
1050
+ if (!(percentage >= 0 && percentage <= 100)) {
1051
+ throw new Error(`array.${name}: percentage ${percentage} must be between 0 and 100`);
1052
+ }
1053
+ }
1054
+
844
1055
  /**
845
1056
  * Percentile Nearest Rank
846
1057
  *
@@ -864,15 +1075,18 @@ function sortedNaLast(id: PineArray<float>): number[] {
864
1075
  * - Returns NaN if array is empty
865
1076
  *
866
1077
  * PineScript rules:
867
- * - the value at rank `ceil(percentage / 100 * size)` of the sorted array
1078
+ * - the value at rank `ceil(percentage * size / 100)` (clamped to 1 .. size) of the sorted array
868
1079
  * - `na` values are sorted after all numbers (na when that rank holds an `na`)
1080
+ * - an `na` percentage counts as 0; a percentage outside 0 .. 100 is a runtime error (throws)
869
1081
  */
870
1082
  export function percentile_nearest_rank(id: PineArray<float>, percentage: float): float {
871
1083
  if (id.length === 0) {
872
1084
  return NaN;
873
1085
  }
1086
+ const pct = Number.isNaN(percentage) ? 0 : percentage;
1087
+ checkPercentage('percentile_nearest_rank', pct);
874
1088
  const s = sortedNaLast(id);
875
- const rank = Math.min(s.length, Math.max(1, Math.ceil((percentage / 100) * s.length)));
1089
+ const rank = Math.min(s.length, Math.max(1, Math.ceil((pct * s.length) / 100)));
876
1090
  return s[rank - 1]!;
877
1091
  }
878
1092
 
@@ -891,35 +1105,38 @@ export function percentile_nearest_rank(id: PineArray<float>, percentage: float)
891
1105
  * ```typescript
892
1106
  * const arr = [1, 2, 3, 4, 5];
893
1107
  * const rank = array.percentrank(arr, 2); // Value at index 2 is 3
894
- * // Returns: 40 (40% of values <= 3)
1108
+ * // Returns: 50 ((3 - 1) * 100 / (5 - 1))
895
1109
  * ```
896
1110
  *
897
1111
  * @remarks
898
- * - Returns percentage of elements <= the value at given index
899
1112
  * - Range: 0 to 100
900
- * - Returns NaN if array is empty or index out of bounds
1113
+ *
1114
+ * PineScript rules:
1115
+ * - `(count - 1) * 100 / (size - 1)`, with `count` the number of non-na elements <= the element (exact
1116
+ * comparison, no 1e-10 tolerance); `na` elements count in the size
1117
+ * - an `na` element, a one-element array or an empty array gives NaN
1118
+ * - an `na` index counts as 0; an index outside `0 .. size - 1` is a runtime error (throws)
901
1119
  */
902
1120
  export function percentrank(id: PineArray<float>, index: int): float {
903
- if (id.length === 0 || index < 0 || index >= id.length) {
1121
+ const i = Number.isNaN(index) ? 0 : Math.trunc(index);
1122
+ if (id.length === 0) {
904
1123
  return NaN;
905
1124
  }
906
-
907
- const value = id[index]!;
908
-
909
- if (isNaN(value)) {
1125
+ if (i < 0 || i >= id.length) {
1126
+ throw new Error(`array.percentrank: index ${i} is out of bounds, array size is ${id.length}`);
1127
+ }
1128
+ if (id.length === 1) {
1129
+ return NaN;
1130
+ }
1131
+ const value = id[i]!;
1132
+ if (Number.isNaN(value)) {
910
1133
  return NaN;
911
1134
  }
912
-
913
- // Count elements less than or equal to the value
914
1135
  let count = 0;
915
- for (let i = 0; i < id.length; i++) {
916
- if (!isNaN(id[i]!) && id[i]! <= value) {
917
- count++;
918
- }
1136
+ for (const v of id) {
1137
+ if (v <= value) count++;
919
1138
  }
920
-
921
- // Calculate percentage
922
- return (count / id.length) * 100;
1139
+ return ((count - 1) * 100) / (id.length - 1);
923
1140
  }
924
1141
 
925
1142
  /**
@@ -947,21 +1164,11 @@ export function percentrank(id: PineArray<float>, index: int): float {
947
1164
  * - Original array is not modified
948
1165
  * - Returns indices that would sort the array
949
1166
  * - Useful for maintaining correspondence with other arrays
1167
+ * - PineScript rule: the indices of `na` elements go where `array.sort` puts them (after the numbers in ascending
1168
+ * order, in their original order: `sort_indices([na, na, 5, 1])` is `[3, 2, 0, 1]`)
950
1169
  */
951
- export function sort_indices(id: PineArray<float>, order: 'asc' | 'desc' = 'asc'): PineArray<int> {
952
- // Create array of indices
953
- const indices: PineArray<int> = Array.from({ length: id.length }, (_, i) => i) as PineArray<int>;
954
-
955
- // Sort indices based on values in original array
956
- indices.sort((a, b) => {
957
- if (order === 'asc') {
958
- return id[a]! - id[b]!;
959
- } else {
960
- return id[b]! - id[a]!;
961
- }
962
- });
963
-
964
- return indices;
1170
+ export function sort_indices<T>(id: PineArray<T>, order: SortOrder = 'asc'): PineArray<int> {
1171
+ return sortedPositions(id, order);
965
1172
  }
966
1173
 
967
1174
  /**
@@ -984,24 +1191,25 @@ export function sort_indices(id: PineArray<float>, order: 'asc' | 'desc' = 'asc'
984
1191
  * @remarks
985
1192
  * - Formula: z = (x - μ) / σ
986
1193
  * - Result has mean of 0 and standard deviation of 1
987
- * - Returns array of NaN if standard deviation is 0
988
1194
  * - Useful for comparing values on different scales
1195
+ *
1196
+ * PineScript rules:
1197
+ * - μ and σ are the population mean and standard deviation of the non-na elements, as `array.variance` (moment
1198
+ * form); `na` elements stay `na`
1199
+ * - when σ is 0 (all values equal) every non-na element gives 1
989
1200
  */
990
1201
  export function standardize(id: PineArray<float>): PineArray<float> {
991
- if (id.length === 0) {
992
- return [] as PineArray<float>;
1202
+ const values = id.filter((v) => !Number.isNaN(v));
1203
+ if (!values.length) {
1204
+ return id.map(() => NaN);
993
1205
  }
994
-
995
- const mean = avg(id);
996
- const stdDev = stdev(id);
997
-
998
- // If stddev is 0, all values are the same - return array of 0s or NaN
1206
+ const [p, q] = moments(values);
1207
+ const mean = p / values.length;
1208
+ const stdDev = Math.sqrt(Math.max(0, q / values.length - mean * mean));
999
1209
  if (stdDev === 0) {
1000
- return id.map(() => NaN) as PineArray<float>;
1210
+ return id.map((v) => (Number.isNaN(v) ? NaN : 1));
1001
1211
  }
1002
-
1003
- // Calculate z-score for each element
1004
- return id.map(value => (value - mean) / stdDev) as PineArray<float>;
1212
+ return id.map((v) => (Number.isNaN(v) ? NaN : (v - mean) / stdDev));
1005
1213
  }
1006
1214
 
1007
1215
  /**