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
@@ -11,7 +11,9 @@
11
11
  * loops, called only on the bars where the Pine call runs.
12
12
  * - {@link lowest} / {@link highest}: `ta.lowest` / `ta.highest` with a series length (a different length on each
13
13
  * call), which PineScript computes with a state kept between calls.
14
- * - {@link linreg}: `ta.linreg` in a conditional block, whose window is kept by bar (not by call).
14
+ * - {@link lowestByBar} / {@link highestByBar} / {@link linreg}: `ta.lowest` / `ta.highest` / `ta.linreg` in a
15
+ * conditional block, whose history is kept by bar (not by call).
16
+ * - {@link sum} / {@link sma}: `math.sum` / `ta.sma` in a `for` / `while` loop, called several times on one bar.
15
17
  *
16
18
  * Since PineScript v6, `and` / `or` are lazy: the right operand runs only when the left operand does not decide
17
19
  * the result. A ta.* call in the right operand of `a or b` runs on the bars where `a` is false, in `a and b` on the
@@ -42,6 +44,8 @@
42
44
 
43
45
  import type { Source } from '../types/index.js';
44
46
  import { linreg as linregSeries } from '../ta/index.js';
47
+ import { RunningSum, isNa } from '../ta/running-sum.js';
48
+ import { seededRandom } from '../math/index.js';
45
49
 
46
50
  /**
47
51
  * Runs `fn` on the bars where `called` is true only, like a ta.* call inside an `if` block.
@@ -67,24 +71,35 @@ export function whenCalled<T extends number | boolean>(
67
71
  return out;
68
72
  }
69
73
 
70
- /** A call site that keeps the two previous arguments (na before the first call). */
74
+ /**
75
+ * A call site that keeps the arguments of the last call where both were not na (na before such a call), as
76
+ * ta.crossover / ta.crossunder / ta.cross.
77
+ */
71
78
  function twoSeriesSite(test: (a: number, b: number, prevA: number, prevB: number) => boolean) {
72
79
  let prevA = NaN;
73
80
  let prevB = NaN;
74
81
  return (a: number, b: number): boolean => {
75
82
  const result = test(a, b, prevA, prevB);
76
- prevA = a;
77
- prevB = b;
83
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
84
+ prevA = a;
85
+ prevB = b;
86
+ }
78
87
  return result;
79
88
  };
80
89
  }
81
90
 
82
- /** One `ta.crossover(a, b)` call site: false on its first call, as its history is na. */
91
+ /**
92
+ * One `ta.crossover(a, b)` call site: compared with the last call where both arguments were not na; false before
93
+ * such a call.
94
+ */
83
95
  export function crossover(): (a: number, b: number) => boolean {
84
96
  return twoSeriesSite((a, b, pa, pb) => a > b && pa <= pb);
85
97
  }
86
98
 
87
- /** One `ta.crossunder(a, b)` call site: false on its first call, as its history is na. */
99
+ /**
100
+ * One `ta.crossunder(a, b)` call site: compared with the last call where both arguments were not na; false before
101
+ * such a call.
102
+ */
88
103
  export function crossunder(): (a: number, b: number) => boolean {
89
104
  return twoSeriesSite((a, b, pa, pb) => a < b && pa >= pb);
90
105
  }
@@ -102,9 +117,20 @@ export function barssince(): (condition: boolean | number) => number {
102
117
  };
103
118
  }
104
119
 
105
- /** One `ta.cross(a, b)` call site: false on its first call, as its history is na. */
120
+ /**
121
+ * One `ta.cross(a, b)` call site, rules of `ta.cross`: the site keeps the side of the last call where `a` was
122
+ * strictly above or strictly below `b` (none before such a call; equal values and na arguments do not change it),
123
+ * and is true when the arguments are strictly on the other side. False with an na argument.
124
+ */
106
125
  export function cross(): (a: number, b: number) => boolean {
107
- return twoSeriesSite((a, b, pa, pb) => (a > b && pa <= pb) || (a < b && pa >= pb));
126
+ let side = 0; // -1: last strictly below, 1: last strictly above, 0: none yet
127
+ return (a, b) => {
128
+ if (Number.isNaN(a) || Number.isNaN(b)) return false;
129
+ const result = (side < 0 && a > b) || (side > 0 && a < b);
130
+ if (a < b) side = -1;
131
+ else if (a > b) side = 1;
132
+ return result;
133
+ };
108
134
  }
109
135
 
110
136
  /** One `ta.lowest` / `ta.highest` call site with a series length; rules in {@link lowest}. */
@@ -179,6 +205,71 @@ export function highest(): (value: number, length: number) => number {
179
205
  return extremeSite(false);
180
206
  }
181
207
 
208
+ /** One `ta.lowest` / `ta.highest` call site whose history is kept by bar; rules in {@link lowestByBar}. */
209
+ function extremeByBarSite(isLowest: boolean): (barIndex: number, value: number, length: number) => number {
210
+ const better = isLowest ? (a: number, b: number) => a < b : (a: number, b: number) => a > b;
211
+ let ring: number[] = [];
212
+ let extreme = NaN;
213
+ let extremeAt = -1;
214
+ return (barIndex, value, length) => {
215
+ const len = Math.floor(length);
216
+ if (ring.length !== len + 1) {
217
+ ring = new Array<number>(len + 1).fill(0);
218
+ extreme = NaN;
219
+ }
220
+ ring[barIndex % (len + 1)] = value;
221
+ if (Number.isNaN(extreme) || better(value, extreme)) {
222
+ extreme = value;
223
+ extremeAt = barIndex;
224
+ } else if (barIndex - extremeAt >= len) {
225
+ extreme = NaN;
226
+ for (let b = barIndex - len + 1; b <= barIndex; b++) {
227
+ const v = ring[b % (len + 1)]!;
228
+ if (!Number.isNaN(v) && (Number.isNaN(extreme) || better(v, extreme))) {
229
+ extreme = v;
230
+ extremeAt = b;
231
+ }
232
+ }
233
+ }
234
+ return barIndex < len - 1 ? NaN : extreme;
235
+ };
236
+ }
237
+
238
+ /**
239
+ * One `ta.lowest(source, length)` call site in a conditional block whose history is kept by bar (not by call), called
240
+ * with `(barIndex, value, length)` on the bars where the Pine call runs.
241
+ *
242
+ * PineScript rules (measured on a call in a ternary branch, length 6):
243
+ * - the values are kept in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start and
244
+ * written only on the bars where the call runs: a bar before the first call counts as 0, and a bar without a call
245
+ * holds the value of the call `length + 1` bars earlier (0 if there was none)
246
+ * - on a call, the value replaces the kept extreme when there is none or when it is strictly lower; only otherwise,
247
+ * when the kept extreme is `length` bars old, the last `length` bars are read again from the ring (the oldest bar
248
+ * wins a tie)
249
+ * - na before bar `length - 1` (by bar, not by call)
250
+ *
251
+ * {@link lowest} keeps the history by call instead; both rules were measured, on different scripts.
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * // Pine: lo = cond ? ta.lowest(low, 6) : na
256
+ * const site = callsite.lowestByBar();
257
+ * const lo = lows.map((v, i) => (cond[i] ? site(i, v, 6) : NaN));
258
+ * ```
259
+ */
260
+ export function lowestByBar(): (barIndex: number, value: number, length: number) => number {
261
+ return extremeByBarSite(true);
262
+ }
263
+
264
+ /**
265
+ * One `ta.highest(source, length)` call site in a conditional block whose history is kept by bar, called with
266
+ * `(barIndex, value, length)`. Same rules as {@link lowestByBar} (a value replaces the kept extreme when it is
267
+ * strictly higher).
268
+ */
269
+ export function highestByBar(): (barIndex: number, value: number, length: number) => number {
270
+ return extremeByBarSite(false);
271
+ }
272
+
182
273
  /**
183
274
  * One `ta.linreg(source, length, offset)` call site in a conditional block, called with
184
275
  * `(barIndex, value, length, offset)` on the bars where the Pine call runs.
@@ -209,3 +300,136 @@ export function linreg(): (barIndex: number, value: number, length: number, offs
209
300
  };
210
301
  }
211
302
 
303
+
304
+ /**
305
+ * One `math.sum` call site in a loop; rules in {@link sum}. With `squares`, it also keeps the running sum of the
306
+ * squared values (for {@link stdev}). Returns `[sum, sumOfSquares]` (NaN when not kept).
307
+ */
308
+ function loopSumSite(
309
+ name: string,
310
+ squares: boolean
311
+ ): (barIndex: number, value: number, length: number) => [number, number] {
312
+ const running = new RunningSum();
313
+ const runningSq = new RunningSum();
314
+ let lastBar = NaN;
315
+ let pending = false;
316
+ return (barIndex, value, length) => {
317
+ const len = Math.floor(length);
318
+ if (!(len >= 1)) throw new Error(`callsite.${name}: length must be at least 1 (got ${length})`);
319
+ if (barIndex !== lastBar) {
320
+ if (pending) {
321
+ running.commit();
322
+ if (squares) runningSq.commit();
323
+ }
324
+ pending = false;
325
+ lastBar = barIndex;
326
+ }
327
+ if (isNa(value)) {
328
+ pending = false;
329
+ return [running.stepNa(len), squares ? runningSq.stepNa(len) : NaN];
330
+ }
331
+ const x = value;
332
+ const out: [number, number] = [running.step(x, len), squares ? runningSq.step(x * x, len) : NaN];
333
+ pending = true;
334
+ return out;
335
+ };
336
+ }
337
+
338
+ /**
339
+ * One `math.sum(source, length)` call site inside a `for` / `while` loop, called with `(barIndex, value, length)` on
340
+ * each call.
341
+ *
342
+ * PineScript rules (measured on calls in loops, 1 to 3 calls per bar, and bars without a call):
343
+ * - the call site keeps one history value per bar: the value of its last call on that bar
344
+ * - every call on a bar starts from the state of the earlier bars, so it gives the sum of its value and the last
345
+ * `length - 1` kept values; the last call of the bar is the one kept for the later bars
346
+ * - the length can change from one call to the next (also on the same bar): each call first moves the kept sum to
347
+ * its own length as `math.sum` with a series length does (Kahan steps of the raw values)
348
+ * - a bar without a call is not in the history (as a call in an `if` block)
349
+ * - the sum is the same compensated running sum as `math.sum` (bit for bit)
350
+ *
351
+ * An na value is skipped as in `math.sum` (it returns the sum of the last `length` earlier values); when it is the
352
+ * last call of a bar, the bar is not in the history (not measured).
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * // Pine: for i = 0 to 2
357
+ * // s = math.sum(close * (1 + 0.01 * i), 7)
358
+ * const site = callsite.sum();
359
+ * for (let b = 0; b < closes.length; b++) {
360
+ * for (let i = 0; i <= 2; i++) {
361
+ * const s = site(b, closes[b]! * (1 + 0.01 * i), 7);
362
+ * }
363
+ * }
364
+ * ```
365
+ */
366
+ export function sum(): (barIndex: number, value: number, length: number) => number {
367
+ const site = loopSumSite('sum', false);
368
+ return (barIndex, value, length) => site(barIndex, value, length)[0];
369
+ }
370
+
371
+ /**
372
+ * One `ta.sma(source, length)` call site inside a `for` / `while` loop, called with `(barIndex, value, length)` on
373
+ * each call: {@link sum} divided by the length (same rules).
374
+ *
375
+ * @example
376
+ * ```typescript
377
+ * // Pine: evaluate(ratio) => ta.sma(gann(ratio), n) // called once per ratio in a while loop
378
+ * const site = callsite.sma();
379
+ * for (let b = 0; b < closes.length; b++) {
380
+ * for (const ratio of ratios) {
381
+ * const avg = site(b, gann(b, ratio), n);
382
+ * }
383
+ * }
384
+ * ```
385
+ */
386
+ export function sma(): (barIndex: number, value: number, length: number) => number {
387
+ const site = loopSumSite('sma', false);
388
+ return (barIndex, value, length) => site(barIndex, value, length)[0] / Math.floor(length);
389
+ }
390
+
391
+ /**
392
+ * One `ta.stdev(source, length, biased)` call site inside a `for` / `while` loop, called with
393
+ * `(barIndex, value, length, biased?)` on each call: the running sums of the values and of their squares follow the
394
+ * rules of {@link sum}; the result is computed from them as `ta.stdev` (bit for bit).
395
+ *
396
+ * @example
397
+ * ```typescript
398
+ * // Pine: for len in lengths
399
+ * // [mid, up, lo] = ta.bb(close, len, 2)
400
+ * const avgSite = callsite.sma();
401
+ * const devSite = callsite.stdev();
402
+ * for (let b = 0; b < closes.length; b++) {
403
+ * for (const len of lengths) {
404
+ * const mid = avgSite(b, closes[b]!, len);
405
+ * const dev = devSite(b, closes[b]!, len);
406
+ * }
407
+ * }
408
+ * ```
409
+ */
410
+ export function stdev(): (barIndex: number, value: number, length: number, biased?: boolean) => number {
411
+ const site = loopSumSite('stdev', true);
412
+ return (barIndex, value, length, biased = true) => {
413
+ const len = Math.floor(length);
414
+ const [s, sq] = site(barIndex, value, length);
415
+ const mean = s / len;
416
+ const variance = biased ? sq / len - mean * mean : sq / (len - 1) - (mean * s) / (len - 1);
417
+ if (!biased && len <= 1) return NaN;
418
+ return Number.isNaN(variance) ? NaN : variance > 0 ? Math.sqrt(variance) : 0;
419
+ };
420
+ }
421
+
422
+ /**
423
+ * One `math.random(min, max, seed)` call site: PineScript keeps one generator per call site, so two call sites
424
+ * with the same seed give the same sequence. Called with `(min?, max?)` once per bar.
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * // Pine: a = math.random(0, 1, 42), b = math.random(0, 1, 42) // a == b on every bar
429
+ * const a = callsite.random(42);
430
+ * const b = callsite.random(42);
431
+ * ```
432
+ */
433
+ export function random(seed: number): (min?: number, max?: number) => number {
434
+ return seededRandom(seed);
435
+ }
@@ -107,7 +107,10 @@ export function from_hex(hex: string, transp?: simple_float): string {
107
107
  * - Replaces the transparency with the new value
108
108
  * - Useful for creating semi-transparent versions of existing colors
109
109
  * - PineScript clamps the transparency to 0..100 (`color.new(c, 140)` is fully transparent, `color.new(c, -5)` is
110
- * opaque); an `na` transparency is fully transparent
110
+ * opaque); an `na` transparency is fully transparent (measured: the colour is not na, its RGB is kept)
111
+ * - an `na` colour (null / undefined) gives black with the new transparency: `color.new(na, 30)` is
112
+ * `rgba(0, 0, 0, 0.7)`, as PineScript does for an na colour variable (measured, #150). In PineScript, the constant
113
+ * `color.new(color(na), 30)` is computed at compile time and stays na; a JS function cannot tell the two apart.
111
114
  *
112
115
  * @example
113
116
  * ```typescript
@@ -117,7 +120,8 @@ export function from_hex(hex: string, transp?: simple_float): string {
117
120
  * color.new_color(red, 100) // Returns: "rgba(255, 0, 0, 0)" - Fully transparent
118
121
  * ```
119
122
  */
120
- export function new_color(baseColor: color, transp: simple_float): string {
123
+ export function new_color(baseColor: color | null | undefined, transp: simple_float): string {
124
+ if (baseColor === null || baseColor === undefined) return rgb(0, 0, 0, transp); // na colour: black
121
125
  // Parse the color and apply transparency
122
126
  const rgba = parseColor(baseColor);
123
127
  return rgb(rgba.r, rgba.g, rgba.b, transp);
@@ -185,6 +189,7 @@ export function b(clr: color): int {
185
189
  * - Converts alpha channel (0-1) to transparency (0-100)
186
190
  * - PineScript stores the alpha as a byte (0-255) and returns an integer transparency:
187
191
  * `color.t(color.new(c, 33.3))` is 33
192
+ * - an `na` colour (null / undefined) is fully transparent: 100
188
193
  *
189
194
  * @example
190
195
  * ```typescript
@@ -193,7 +198,8 @@ export function b(clr: color): int {
193
198
  * color.t(color.rgb(255, 0, 0, 100)) // Returns: 100 (fully transparent)
194
199
  * ```
195
200
  */
196
- export function t(clr: color): float {
201
+ export function t(clr: color | null | undefined): float {
202
+ if (clr === null || clr === undefined) return 100;
197
203
  return Math.round((1 - alphaByte(parseColor(clr).a) / 255) * 100);
198
204
  }
199
205
 
@@ -362,8 +368,12 @@ export function from_gradient(
362
368
  const c2 = rgba(top_color);
363
369
  const a = c1.a * (1 - k) + c2.a * k;
364
370
  if (a === 0) return transparent;
365
- // operation order with the fewest differences to PineScript (channels as 0-1, alpha as a byte)
366
- const mix = (x1: number, x2: number) => Math.floor((((x1 / 255) * c1.a) * (1 - k) + ((x2 / 255) * c2.a) * k) / (a / 255));
371
+ // PineScript order (bit for bit, #146): premultiplied channels ((alpha / 255) * channel) * weight, divided by the
372
+ // byte-alpha mix / 255, truncated. At an end (k = 0 / 1) this is floor(x * a / a): the end colour or one unit less.
373
+ const scale = a / 255;
374
+ const w1 = c1.a / 255;
375
+ const w2 = c2.a / 255;
376
+ const mix = (x1: number, x2: number) => Math.floor(((w1 * x1) * (1 - k) + (w2 * x2) * k) / scale);
367
377
  const [r, g, b] = [mix(c1.r, c2.r), mix(c1.g, c2.g), mix(c1.b, c2.b)];
368
378
  const alpha = Math.floor(a);
369
379
  return alpha === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${alpha / 255})`;
package/src/index.ts CHANGED
@@ -164,7 +164,7 @@ export type { LightweightSeriesDefinitions } from './runtime/adapters/Lightweigh
164
164
  export { SimpleInputAdapter } from './runtime/adapters/SimpleInputAdapter.js';
165
165
 
166
166
  // Version
167
- export const VERSION = '0.9.4';
167
+ export const VERSION = '0.9.6';
168
168
 
169
169
  /**
170
170
  * Library information
@@ -85,7 +85,8 @@ export function new_label(...args: unknown[]): Label {
85
85
  y,
86
86
  xloc: xloc ?? 'bar_index',
87
87
  yloc: yloc ?? 'price',
88
- text,
88
+ // PineScript: a label without text has the text "" (label.get_text gives "")
89
+ text: text ?? '',
89
90
  tooltip,
90
91
  color: labelColor,
91
92
  style,
@@ -40,6 +40,8 @@ import type { Linefill, Line, color } from '../types/index.js';
40
40
  * ```
41
41
  */
42
42
  export function new_linefill(line1: Line, line2: Line, fillColor?: color): Linefill {
43
+ // PineScript: an na line gives an na linefill
44
+ if (line1 === null || line1 === undefined || line2 === null || line2 === undefined) return null as unknown as Linefill;
43
45
  return register('linefill', {
44
46
  line1,
45
47
  line2,