oakscriptjs 0.10.1 → 0.11.1

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 (271) hide show
  1. package/dist/cjs/array/index.d.ts +8 -2
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +2 -1
  4. package/dist/cjs/array/index.js.map +2 -2
  5. package/dist/cjs/box/index.d.ts.map +1 -1
  6. package/dist/cjs/box/index.js +7 -1
  7. package/dist/cjs/box/index.js.map +2 -2
  8. package/dist/cjs/callsite/index.d.ts +73 -32
  9. package/dist/cjs/callsite/index.d.ts.map +1 -1
  10. package/dist/cjs/callsite/index.js +69 -65
  11. package/dist/cjs/callsite/index.js.map +2 -2
  12. package/dist/cjs/drawing/registry.d.ts +84 -0
  13. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  14. package/dist/cjs/drawing/registry.js +153 -6
  15. package/dist/cjs/drawing/registry.js.map +2 -2
  16. package/dist/cjs/index.d.ts +8 -2
  17. package/dist/cjs/index.d.ts.map +1 -1
  18. package/dist/cjs/index.js +33 -1
  19. package/dist/cjs/index.js.map +2 -2
  20. package/dist/cjs/input.d.ts +9 -1
  21. package/dist/cjs/input.d.ts.map +1 -1
  22. package/dist/cjs/input.js +16 -0
  23. package/dist/cjs/input.js.map +2 -2
  24. package/dist/cjs/label/index.d.ts.map +1 -1
  25. package/dist/cjs/label/index.js +3 -0
  26. package/dist/cjs/label/index.js.map +2 -2
  27. package/dist/cjs/line/index.d.ts +10 -27
  28. package/dist/cjs/line/index.d.ts.map +1 -1
  29. package/dist/cjs/line/index.js +11 -22
  30. package/dist/cjs/line/index.js.map +2 -2
  31. package/dist/cjs/linefill/index.d.ts +3 -0
  32. package/dist/cjs/linefill/index.d.ts.map +1 -1
  33. package/dist/cjs/linefill/index.js +15 -1
  34. package/dist/cjs/linefill/index.js.map +3 -3
  35. package/dist/cjs/math/index.d.ts.map +1 -1
  36. package/dist/cjs/math/index.js +2 -0
  37. package/dist/cjs/math/index.js.map +2 -2
  38. package/dist/cjs/polyline/index.d.ts.map +1 -1
  39. package/dist/cjs/polyline/index.js +3 -0
  40. package/dist/cjs/polyline/index.js.map +2 -2
  41. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js +1 -1
  42. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js.map +2 -2
  43. package/dist/cjs/runtime/index.d.ts +1 -1
  44. package/dist/cjs/runtime/index.d.ts.map +1 -1
  45. package/dist/cjs/runtime/index.js +1 -0
  46. package/dist/cjs/runtime/index.js.map +2 -2
  47. package/dist/cjs/runtime/inputs.d.ts +9 -0
  48. package/dist/cjs/runtime/inputs.d.ts.map +1 -1
  49. package/dist/cjs/runtime/inputs.js +8 -0
  50. package/dist/cjs/runtime/inputs.js.map +2 -2
  51. package/dist/cjs/runtime/types.d.ts +1 -1
  52. package/dist/cjs/runtime/types.d.ts.map +1 -1
  53. package/dist/cjs/runtime/types.js.map +1 -1
  54. package/dist/cjs/script/index.d.ts +91 -21
  55. package/dist/cjs/script/index.d.ts.map +1 -1
  56. package/dist/cjs/script/index.js +207 -19
  57. package/dist/cjs/script/index.js.map +3 -3
  58. package/dist/cjs/security/other.d.ts +92 -0
  59. package/dist/cjs/security/other.d.ts.map +1 -0
  60. package/dist/cjs/security/other.js +164 -0
  61. package/dist/cjs/security/other.js.map +7 -0
  62. package/dist/cjs/session/bars.d.ts +24 -1
  63. package/dist/cjs/session/bars.d.ts.map +1 -1
  64. package/dist/cjs/session/bars.js +56 -7
  65. package/dist/cjs/session/bars.js.map +3 -3
  66. package/dist/cjs/session/calendar.d.ts +13 -2
  67. package/dist/cjs/session/calendar.d.ts.map +1 -1
  68. package/dist/cjs/session/calendar.js +39 -9
  69. package/dist/cjs/session/calendar.js.map +2 -2
  70. package/dist/cjs/session/context.d.ts +76 -0
  71. package/dist/cjs/session/context.d.ts.map +1 -0
  72. package/dist/cjs/session/context.js +114 -0
  73. package/dist/cjs/session/context.js.map +7 -0
  74. package/dist/cjs/str/floatingdecimal.d.ts +31 -0
  75. package/dist/cjs/str/floatingdecimal.d.ts.map +1 -0
  76. package/dist/cjs/str/floatingdecimal.js +298 -0
  77. package/dist/cjs/str/floatingdecimal.js.map +7 -0
  78. package/dist/cjs/str/index.d.ts +4 -1
  79. package/dist/cjs/str/index.d.ts.map +1 -1
  80. package/dist/cjs/str/index.js +2 -1
  81. package/dist/cjs/str/index.js.map +2 -2
  82. package/dist/cjs/str/numberformat.d.ts +7 -3
  83. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  84. package/dist/cjs/str/numberformat.js +4 -3
  85. package/dist/cjs/str/numberformat.js.map +2 -2
  86. package/dist/cjs/ta/index.d.ts +67 -34
  87. package/dist/cjs/ta/index.d.ts.map +1 -1
  88. package/dist/cjs/ta/index.js +202 -145
  89. package/dist/cjs/ta/index.js.map +2 -2
  90. package/dist/cjs/ta/limits.d.ts +35 -0
  91. package/dist/cjs/ta/limits.d.ts.map +1 -0
  92. package/dist/cjs/ta/limits.js +64 -0
  93. package/dist/cjs/ta/limits.js.map +7 -0
  94. package/dist/cjs/ta/running-sum.d.ts +0 -16
  95. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  96. package/dist/cjs/ta/running-sum.js +2 -0
  97. package/dist/cjs/ta/running-sum.js.map +2 -2
  98. package/dist/cjs/ta-series.d.ts +8 -8
  99. package/dist/cjs/ta-series.d.ts.map +1 -1
  100. package/dist/cjs/ta-series.js +10 -8
  101. package/dist/cjs/ta-series.js.map +2 -2
  102. package/dist/cjs/table/index.d.ts +3 -1
  103. package/dist/cjs/table/index.d.ts.map +1 -1
  104. package/dist/cjs/table/index.js +1 -0
  105. package/dist/cjs/table/index.js.map +3 -3
  106. package/dist/cjs/time/index.d.ts +5 -2
  107. package/dist/cjs/time/index.d.ts.map +1 -1
  108. package/dist/cjs/time/index.js +2 -2
  109. package/dist/cjs/time/index.js.map +2 -2
  110. package/dist/cjs/time/session.d.ts +26 -1
  111. package/dist/cjs/time/session.d.ts.map +1 -1
  112. package/dist/cjs/time/session.js +94 -12
  113. package/dist/cjs/time/session.js.map +3 -3
  114. package/dist/cjs/time/timezone.d.ts +6 -1
  115. package/dist/cjs/time/timezone.d.ts.map +1 -1
  116. package/dist/cjs/time/timezone.js +2 -2
  117. package/dist/cjs/time/timezone.js.map +2 -2
  118. package/dist/cjs/types/metadata.d.ts +28 -4
  119. package/dist/cjs/types/metadata.d.ts.map +1 -1
  120. package/dist/cjs/types/metadata.js.map +1 -1
  121. package/dist/esm/array/index.d.ts +8 -2
  122. package/dist/esm/array/index.d.ts.map +1 -1
  123. package/dist/esm/array/index.js +12 -3
  124. package/dist/esm/array/index.js.map +1 -1
  125. package/dist/esm/box/index.d.ts.map +1 -1
  126. package/dist/esm/box/index.js +8 -2
  127. package/dist/esm/box/index.js.map +1 -1
  128. package/dist/esm/callsite/index.d.ts +73 -32
  129. package/dist/esm/callsite/index.d.ts.map +1 -1
  130. package/dist/esm/callsite/index.js +143 -106
  131. package/dist/esm/callsite/index.js.map +1 -1
  132. package/dist/esm/drawing/registry.d.ts +84 -0
  133. package/dist/esm/drawing/registry.d.ts.map +1 -1
  134. package/dist/esm/drawing/registry.js +260 -6
  135. package/dist/esm/drawing/registry.js.map +1 -1
  136. package/dist/esm/index.d.ts +8 -2
  137. package/dist/esm/index.d.ts.map +1 -1
  138. package/dist/esm/index.js +13 -2
  139. package/dist/esm/index.js.map +1 -1
  140. package/dist/esm/input.d.ts +9 -1
  141. package/dist/esm/input.d.ts.map +1 -1
  142. package/dist/esm/input.js +16 -0
  143. package/dist/esm/input.js.map +1 -1
  144. package/dist/esm/label/index.d.ts.map +1 -1
  145. package/dist/esm/label/index.js +4 -1
  146. package/dist/esm/label/index.js.map +1 -1
  147. package/dist/esm/line/index.d.ts +10 -27
  148. package/dist/esm/line/index.d.ts.map +1 -1
  149. package/dist/esm/line/index.js +22 -60
  150. package/dist/esm/line/index.js.map +1 -1
  151. package/dist/esm/linefill/index.d.ts +3 -0
  152. package/dist/esm/linefill/index.d.ts.map +1 -1
  153. package/dist/esm/linefill/index.js +20 -1
  154. package/dist/esm/linefill/index.js.map +1 -1
  155. package/dist/esm/math/index.d.ts.map +1 -1
  156. package/dist/esm/math/index.js +3 -0
  157. package/dist/esm/math/index.js.map +1 -1
  158. package/dist/esm/polyline/index.d.ts.map +1 -1
  159. package/dist/esm/polyline/index.js +5 -1
  160. package/dist/esm/polyline/index.js.map +1 -1
  161. package/dist/esm/runtime/adapters/SimpleInputAdapter.js +1 -1
  162. package/dist/esm/runtime/adapters/SimpleInputAdapter.js.map +1 -1
  163. package/dist/esm/runtime/index.d.ts +1 -1
  164. package/dist/esm/runtime/index.d.ts.map +1 -1
  165. package/dist/esm/runtime/index.js +1 -1
  166. package/dist/esm/runtime/index.js.map +1 -1
  167. package/dist/esm/runtime/inputs.d.ts +9 -0
  168. package/dist/esm/runtime/inputs.d.ts.map +1 -1
  169. package/dist/esm/runtime/inputs.js +15 -0
  170. package/dist/esm/runtime/inputs.js.map +1 -1
  171. package/dist/esm/runtime/types.d.ts +1 -1
  172. package/dist/esm/runtime/types.d.ts.map +1 -1
  173. package/dist/esm/script/index.d.ts +91 -21
  174. package/dist/esm/script/index.d.ts.map +1 -1
  175. package/dist/esm/script/index.js +259 -28
  176. package/dist/esm/script/index.js.map +1 -1
  177. package/dist/esm/security/other.d.ts +92 -0
  178. package/dist/esm/security/other.d.ts.map +1 -0
  179. package/dist/esm/security/other.js +211 -0
  180. package/dist/esm/security/other.js.map +1 -0
  181. package/dist/esm/session/bars.d.ts +24 -1
  182. package/dist/esm/session/bars.d.ts.map +1 -1
  183. package/dist/esm/session/bars.js +75 -6
  184. package/dist/esm/session/bars.js.map +1 -1
  185. package/dist/esm/session/calendar.d.ts +13 -2
  186. package/dist/esm/session/calendar.d.ts.map +1 -1
  187. package/dist/esm/session/calendar.js +54 -10
  188. package/dist/esm/session/calendar.js.map +1 -1
  189. package/dist/esm/session/context.d.ts +76 -0
  190. package/dist/esm/session/context.d.ts.map +1 -0
  191. package/dist/esm/session/context.js +124 -0
  192. package/dist/esm/session/context.js.map +1 -0
  193. package/dist/esm/str/floatingdecimal.d.ts +31 -0
  194. package/dist/esm/str/floatingdecimal.d.ts.map +1 -0
  195. package/dist/esm/str/floatingdecimal.js +272 -0
  196. package/dist/esm/str/floatingdecimal.js.map +1 -0
  197. package/dist/esm/str/index.d.ts +4 -1
  198. package/dist/esm/str/index.d.ts.map +1 -1
  199. package/dist/esm/str/index.js +9 -2
  200. package/dist/esm/str/index.js.map +1 -1
  201. package/dist/esm/str/numberformat.d.ts +7 -3
  202. package/dist/esm/str/numberformat.d.ts.map +1 -1
  203. package/dist/esm/str/numberformat.js +11 -5
  204. package/dist/esm/str/numberformat.js.map +1 -1
  205. package/dist/esm/ta/index.d.ts +67 -34
  206. package/dist/esm/ta/index.d.ts.map +1 -1
  207. package/dist/esm/ta/index.js +284 -240
  208. package/dist/esm/ta/index.js.map +1 -1
  209. package/dist/esm/ta/limits.d.ts +35 -0
  210. package/dist/esm/ta/limits.d.ts.map +1 -0
  211. package/dist/esm/ta/limits.js +67 -0
  212. package/dist/esm/ta/limits.js.map +1 -0
  213. package/dist/esm/ta/running-sum.d.ts +0 -16
  214. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  215. package/dist/esm/ta/running-sum.js +2 -0
  216. package/dist/esm/ta/running-sum.js.map +1 -1
  217. package/dist/esm/ta-series.d.ts +8 -8
  218. package/dist/esm/ta-series.d.ts.map +1 -1
  219. package/dist/esm/ta-series.js +11 -8
  220. package/dist/esm/ta-series.js.map +1 -1
  221. package/dist/esm/table/index.d.ts +3 -1
  222. package/dist/esm/table/index.d.ts.map +1 -1
  223. package/dist/esm/table/index.js +6 -1
  224. package/dist/esm/table/index.js.map +1 -1
  225. package/dist/esm/time/index.d.ts +5 -2
  226. package/dist/esm/time/index.d.ts.map +1 -1
  227. package/dist/esm/time/index.js +6 -3
  228. package/dist/esm/time/index.js.map +1 -1
  229. package/dist/esm/time/session.d.ts +26 -1
  230. package/dist/esm/time/session.d.ts.map +1 -1
  231. package/dist/esm/time/session.js +141 -16
  232. package/dist/esm/time/session.js.map +1 -1
  233. package/dist/esm/time/timezone.d.ts +6 -1
  234. package/dist/esm/time/timezone.d.ts.map +1 -1
  235. package/dist/esm/time/timezone.js +8 -2
  236. package/dist/esm/time/timezone.js.map +1 -1
  237. package/dist/esm/types/metadata.d.ts +28 -4
  238. package/dist/esm/types/metadata.d.ts.map +1 -1
  239. package/package.json +1 -1
  240. package/src/array/index.ts +11 -3
  241. package/src/box/index.ts +8 -2
  242. package/src/callsite/index.ts +150 -100
  243. package/src/drawing/registry.ts +278 -5
  244. package/src/index.ts +44 -1
  245. package/src/input.ts +18 -1
  246. package/src/label/index.ts +4 -1
  247. package/src/line/index.ts +22 -64
  248. package/src/linefill/index.ts +20 -1
  249. package/src/math/index.ts +2 -0
  250. package/src/polyline/index.ts +3 -1
  251. package/src/runtime/adapters/SimpleInputAdapter.ts +1 -1
  252. package/src/runtime/index.ts +1 -0
  253. package/src/runtime/inputs.ts +16 -0
  254. package/src/runtime/types.ts +1 -1
  255. package/src/script/index.ts +355 -34
  256. package/src/security/other.ts +269 -0
  257. package/src/session/bars.ts +70 -6
  258. package/src/session/calendar.ts +60 -10
  259. package/src/session/context.ts +162 -0
  260. package/src/str/floatingdecimal.ts +268 -0
  261. package/src/str/index.ts +8 -2
  262. package/src/str/numberformat.ts +13 -6
  263. package/src/ta/index.ts +304 -248
  264. package/src/ta/limits.ts +74 -0
  265. package/src/ta/running-sum.ts +2 -0
  266. package/src/ta-series.ts +19 -16
  267. package/src/table/index.ts +4 -1
  268. package/src/time/index.ts +6 -3
  269. package/src/time/session.ts +137 -15
  270. package/src/time/timezone.ts +9 -2
  271. package/src/types/metadata.ts +28 -4
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 { checkPositiveLength, 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
 
@@ -66,6 +68,7 @@ export function sma(source: Source, length: simple_int | ArrayLike<number>): ser
66
68
  * ```
67
69
  */
68
70
  export function ema(source: Source, length: simple_int): series_float {
71
+ checkPositiveLength('ema', length, 0);
69
72
  const result: series_float = [];
70
73
  const len = Math.floor(length);
71
74
  const multiplier = 2 / (len + 1);
@@ -131,7 +134,25 @@ export function ema(source: Source, length: simple_int): series_float {
131
134
  * const oversold = rsi14.map(v => v < 30);
132
135
  * ```
133
136
  */
134
- export function rsi(source: Source, length: simple_int): series_float {
137
+ export function rsi(source: Source, length: simple_int): series_float;
138
+ /**
139
+ * PineScript v4 `rsi(x, y)` with two series (#180): `x` and `y` are the averages of the upward and downward changes,
140
+ * already computed. As in PineScript: 100 when `y` is within 1e-10 of zero (also with an na `x`), else 0 when `x` is,
141
+ * else `100 - 100 / (1 + x / y)`, na when that divides by zero (`x == -y`). Versions 5 and 6 have the length form only.
142
+ */
143
+ export function rsi(x: Source, y: Source): series_float;
144
+ export function rsi(source: Source, length: simple_int | Source): series_float {
145
+ if (typeof length !== 'number') {
146
+ return Array.from(source, (xv, i) => {
147
+ const x = xv ?? NaN;
148
+ const y = length[i] ?? NaN;
149
+ if (eq(y, 0)) return 100;
150
+ if (eq(x, 0)) return 0;
151
+ const r = 100 - 100 / (1 + x / y);
152
+ return Number.isFinite(r) ? r : NaN;
153
+ });
154
+ }
155
+ checkPositiveLength('rsi', length, 0, 'y');
135
156
  const result: series_float = [];
136
157
 
137
158
  // Calculate price changes
@@ -234,6 +255,7 @@ export function bb(
234
255
  length: simple_int,
235
256
  mult: simple_float
236
257
  ): [series_float, series_float, series_float] {
258
+ checkSumLength('ta.bb', length, 0);
237
259
  const basis = sma(series, length);
238
260
  const dev = stdev(series, length);
239
261
 
@@ -285,14 +307,20 @@ function lastValues(source: Source, i: number, length: number): number[] | null
285
307
 
286
308
  /**
287
309
  * 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.
310
+ * the oldest highest (or lowest) value (the newest one with `newest`), or -1 when the window is empty. The window
311
+ * stops at the first `na` value going back (the callers pass the non-na values only, see {@link keptValues}), and a
312
+ * fractional length is truncated (7.5 is 7 bars), as PineScript does.
313
+ * `zeros[i]` tells whether the window holds a +0 (highest) or a -0 (lowest), the zero that `Math.max` / `Math.min`
314
+ * return when the extreme is 0 (not kept with `newest`).
292
315
  */
293
- function windowExtremes(source: Source, length: number, lowest: boolean): { at: Int32Array; zeros: Uint8Array } {
316
+ function windowExtremes(
317
+ source: Source,
318
+ length: number,
319
+ lowest: boolean,
320
+ newest = false
321
+ ): { at: Int32Array; zeros: Uint8Array } {
294
322
  const n = source.length;
295
- const span = length > 0 ? Math.ceil(length) : 0; // the bars j = 0, 1... with j < length
323
+ const span = length > 0 ? Math.trunc(length) : 0; // PineScript casts the length to int
296
324
  const at = new Int32Array(n).fill(-1);
297
325
  const zeros = new Uint8Array(n);
298
326
  const queue = new Int32Array(n); // bar indices; values from best to worst, the oldest first among equal values
@@ -310,8 +338,10 @@ function windowExtremes(source: Source, length: number, lowest: boolean): { at:
310
338
  zeroCount = 0;
311
339
  continue;
312
340
  }
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)) {
341
+ // drop the values that the new one beats; equal values stay (the oldest wins ties), or go with `newest`
342
+ while (tail > head) {
343
+ const last = source[queue[tail - 1]!]!;
344
+ if (!(lowest ? last > x : last < x) && !(newest && last === x)) break;
315
345
  if (isPreferredZero(queue[--tail]!)) zeroCount--;
316
346
  }
317
347
  queue[tail++] = i;
@@ -328,20 +358,79 @@ function windowExtremes(source: Source, length: number, lowest: boolean): { at:
328
358
  return { at, zeros };
329
359
  }
330
360
 
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]!)));
361
+ /**
362
+ * The non-na values of `source` with their bars, and for each bar the position of its last non-na value (-1 when
363
+ * there is none yet). PineScript's ta.highest / ta.lowest / ta.highestbars / ta.lowestbars / ta.pivothigh /
364
+ * ta.pivotlow keep the non-na values they receive and nothing else.
365
+ */
366
+ function keptValues(source: Source): { values: number[]; bars: number[]; last: Int32Array } {
367
+ const values: number[] = [];
368
+ const bars: number[] = [];
369
+ const last = new Int32Array(source.length);
370
+ for (let i = 0; i < source.length; i++) {
371
+ const x = source[i]!;
372
+ if (!Number.isNaN(x)) {
373
+ values.push(x);
374
+ bars.push(i);
375
+ }
376
+ last[i] = values.length - 1;
377
+ }
378
+ return { values, bars, last };
379
+ }
380
+
381
+ /**
382
+ * ta.highest / ta.lowest / ta.highestbars / ta.lowestbars with a series length (a length per bar): on each bar the
383
+ * best of the last `length[i]` non-na values (the newest one on ties), na before bar `length[i] - 1`, as PineScript
384
+ * (measured with a length that changes on each bar, with and without na in the source).
385
+ */
386
+ function extremeSeriesLength(source: Source, lengths: ArrayLike<number>, lowest: boolean, offsets: boolean): number[] {
387
+ const name = (lowest ? 'lowest' : 'highest') + (offsets ? 'bars' : '');
388
+ const { values, bars, last } = keptValues(source);
389
+ return Array.from(source, (_, i) => {
390
+ const len = Math.trunc(lengths[i]!);
391
+ checkWindowLength(name, len, i);
392
+ const k = last[i]!;
393
+ if (!(len >= 1) || i < len - 1 || k < 0) return NaN;
394
+ let best = k;
395
+ for (let j = k - 1; j >= 0 && j > k - len; j--) {
396
+ if (lowest ? values[j]! < values[best]! : values[j]! > values[best]!) best = j;
397
+ }
398
+ return offsets ? -(i - bars[best]!) : values[best]!;
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Offset to the best of the last `length` non-na values (the newest one on ties): the distance in bars, so below
404
+ * `-(length - 1)` after an na value. na before bar `length - 1` and while there is no value.
405
+ */
406
+ function extremeOffset(source: Source, length: simple_int | ArrayLike<number>, lowest: boolean): series_int {
407
+ if (typeof length !== 'number') return extremeSeriesLength(source, length, lowest, true);
408
+ checkWindowLength(lowest ? 'lowestbars' : 'highestbars', length, 0);
409
+ const { values, bars, last } = keptValues(source);
410
+ const { at } = windowExtremes(values, length, lowest, true);
411
+ const len = Math.trunc(length);
412
+ // -(i - bar): -0 when the extreme is the current bar, as the offset -j of the window scan
413
+ return source.map((_, i) => {
414
+ const k = last[i]!;
415
+ return i < len - 1 || k < 0 || at[k]! < 0 ? NaN : -(i - bars[at[k]!]!);
416
+ });
336
417
  }
337
418
 
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);
419
+ /**
420
+ * Best of the last `length` non-na values (of all of them when there are fewer), with the zero sign of `Math.max` /
421
+ * `Math.min`. na before bar `length - 1` and while there is no value.
422
+ */
423
+ function extremeValue(source: Source, length: simple_int | ArrayLike<number>, lowest: boolean): series_float {
424
+ if (typeof length !== 'number') return extremeSeriesLength(source, length, lowest, false);
425
+ checkWindowLength(lowest ? 'lowest' : 'highest', length, 0);
426
+ const { values, last } = keptValues(source);
427
+ const { at, zeros } = windowExtremes(values, length, lowest);
428
+ const len = Math.trunc(length);
341
429
  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;
430
+ const k = last[i]!;
431
+ if (i < len - 1 || k < 0 || at[k]! < 0) return NaN;
432
+ const v = values[at[k]!]!;
433
+ return v === 0 && zeros[k] ? (lowest ? -0 : 0) : v;
345
434
  });
346
435
  }
347
436
 
@@ -436,6 +525,7 @@ export function crossunder(series1: Source, series2: Source): series_bool {
436
525
  * ```
437
526
  */
438
527
  export function change(source: Source, length: simple_int = 1): series_float {
528
+ checkPositiveLength('change', length, 0);
439
529
  const result: series_float = [];
440
530
 
441
531
  for (let i = 0; i < source.length; i++) {
@@ -548,6 +638,7 @@ export function tr(
548
638
  * ```
549
639
  */
550
640
  export function atr(length: simple_int, high?: Source, low?: Source, close?: Source): series_float {
641
+ checkPositiveLength('atr->rma', length, 0);
551
642
  if (!high || !low || !close) {
552
643
  throw new Error(
553
644
  'ta.atr() requires high, low, and close series. ' +
@@ -693,6 +784,7 @@ export function supertrend(
693
784
  * ```
694
785
  */
695
786
  export function rma(source: Source, length: simple_int): series_float {
787
+ checkPositiveLength('rma', length, 0);
696
788
  const result: series_float = [];
697
789
  // Floor the length to match PineScript's auto-truncation of float to int
698
790
  const len = Math.floor(length);
@@ -763,6 +855,8 @@ function wmaAdd(a: number, b: number): number {
763
855
  * - Summed from the oldest value to the newest; as PineScript, a partial sum `s + t` with
764
856
  * `|s + t| <= 1e-10 * max(1, |s| + |t|)` is 0 (a cancellation residue, or values below 1e-10)
765
857
  * - More responsive to recent price changes than SMA
858
+ * - `length` can be an array (a length per bar, PineScript series length): each bar gives the result of its own
859
+ * length (`na` for a length below 1)
766
860
  *
767
861
  * @example
768
862
  * ```typescript
@@ -770,10 +864,11 @@ function wmaAdd(a: number, b: number): number {
770
864
  * // WMA gives more weight to recent prices
771
865
  * ```
772
866
  */
773
- export function wma(source: Source, length: simple_int): series_float {
867
+ export function wma(source: Source, length: simple_int | ArrayLike<number>): series_float {
774
868
  const result: series_float = [];
775
869
  // Floor the length to match PineScript's auto-truncation of float to int
776
- const len = Math.floor(length);
870
+ const fixed = typeof length === 'number' ? Math.floor(length) : NaN;
871
+ if (typeof length === 'number') checkPositiveLength('wma', fixed, 0);
777
872
  // PineScript: na on a bar with an na source (also +/-Infinity) and until `len` non-na values were received;
778
873
  // otherwise the last `len` bars, each na replaced by the previous non-na value
779
874
  const filled: number[] = [];
@@ -787,7 +882,10 @@ export function wma(source: Source, length: simple_int): series_float {
787
882
  count++;
788
883
  }
789
884
  filled.push(last);
790
- if (isNa || count < len) {
885
+ // a series length: the window of this bar's length (the result of ta.wma with that fixed length)
886
+ const len = typeof length === 'number' ? fixed : Math.floor(length[i]!);
887
+ if (typeof length !== 'number') checkPositiveLength('wma', len, i);
888
+ if (isNa || !(len >= 1) || count < len) {
791
889
  result.push(NaN);
792
890
  continue;
793
891
  }
@@ -812,11 +910,15 @@ export function wma(source: Source, length: simple_int): series_float {
812
910
  * @returns Highest value series (na on the first `length - 1` bars)
813
911
  *
814
912
  * @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`.
913
+ * As in PineScript: `na` values are skipped. The result is the highest of the last `length` non-na values (of all
914
+ * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
915
+ * value was received. A fractional length is truncated (7.5 is 7 bars).
916
+ * `length` can be an array (a length per bar, PineScript series length), also for `ta.lowest`, `ta.highestbars`
917
+ * and `ta.lowestbars`: each bar uses its own length.
918
+ * A length above 15,000 throws the PineScript runtime error (also `ta.lowest`, `ta.highestbars`, `ta.lowestbars`).
817
919
  * PineScript `ta.highest(length)` (source = high) is `ta.highest(length)` in the script API.
818
920
  */
819
- export function highest(source: Source, length: simple_int): series_float {
921
+ export function highest(source: Source, length: simple_int | ArrayLike<number>): series_float {
820
922
  return extremeValue(source, length, false);
821
923
  }
822
924
 
@@ -828,10 +930,11 @@ export function highest(source: Source, length: simple_int): series_float {
828
930
  * @returns Lowest value series (na on the first `length - 1` bars)
829
931
  *
830
932
  * @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`.
933
+ * As in PineScript: `na` values are skipped. The result is the lowest of the last `length` non-na values (of all
934
+ * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
935
+ * value was received.
833
936
  */
834
- export function lowest(source: Source, length: simple_int): series_float {
937
+ export function lowest(source: Source, length: simple_int | ArrayLike<number>): series_float {
835
938
  return extremeValue(source, length, true);
836
939
  }
837
940
 
@@ -943,6 +1046,7 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
943
1046
  * ```
944
1047
  */
945
1048
  export function rising(source: Source, length: simple_int): series_bool {
1049
+ checkPositiveLength('rising', length, 0);
946
1050
  return monotonic(source, length, gt);
947
1051
  }
948
1052
 
@@ -968,6 +1072,7 @@ export function rising(source: Source, length: simple_int): series_bool {
968
1072
  * ```
969
1073
  */
970
1074
  export function falling(source: Source, length: simple_int): series_bool {
1075
+ checkPositiveLength('falling', length, 0);
971
1076
  return monotonic(source, length, lt);
972
1077
  }
973
1078
 
@@ -993,6 +1098,7 @@ export function falling(source: Source, length: simple_int): series_bool {
993
1098
  * ```
994
1099
  */
995
1100
  export function roc(source: Source, length: simple_int): series_float {
1101
+ checkPositiveLength('roc', length, 0);
996
1102
  const result: series_float = [];
997
1103
 
998
1104
  for (let i = 0; i < source.length; i++) {
@@ -1033,6 +1139,7 @@ export function roc(source: Source, length: simple_int): series_float {
1033
1139
  * ```
1034
1140
  */
1035
1141
  export function mom(source: Source, length: simple_int): series_float {
1142
+ checkPositiveLength('mom', length, 0);
1036
1143
  const result: series_float = [];
1037
1144
 
1038
1145
  for (let i = 0; i < source.length; i++) {
@@ -1072,6 +1179,7 @@ export function mom(source: Source, length: simple_int): series_float {
1072
1179
  * ```
1073
1180
  */
1074
1181
  export function dev(source: Source, length: simple_int): series_float {
1182
+ checkPositiveLength('dev', length, 0);
1075
1183
  const len = Math.floor(length);
1076
1184
  return sma(source, len).map((mean, i) => {
1077
1185
  if (i < len - 1 || Number.isNaN(mean)) return NaN;
@@ -1127,6 +1235,7 @@ function varianceOf(
1127
1235
  return biased ? result : result.map((v, i) => (Math.floor(length[i]!) > 1 ? v : NaN));
1128
1236
  }
1129
1237
  const len = Math.floor(length);
1238
+ checkSumLength(name, len, 0);
1130
1239
  // PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
1131
1240
  if ((biased ? len : len - 1) <= 0) return source.map(() => NaN);
1132
1241
  return runningVariance(source, len, biased);
@@ -1154,6 +1263,7 @@ function varianceOf(
1154
1263
  * ```
1155
1264
  */
1156
1265
  export function median(source: Source, length: simple_int): series_float {
1266
+ checkPositiveLength('median', length, 0);
1157
1267
  const len = Math.floor(length);
1158
1268
  const n = source.length;
1159
1269
  const out: series_float = new Array<number>(n).fill(NaN);
@@ -1241,6 +1351,7 @@ export function swma(source: Source): series_float {
1241
1351
  * ```
1242
1352
  */
1243
1353
  export function vwma(source: Source, length: simple_int, volume?: Source): series_float {
1354
+ checkPositiveLength('vwma->sma', length, 0);
1244
1355
  if (!volume) {
1245
1356
  throw new Error(
1246
1357
  'ta.vwma() requires volume series. ' +
@@ -1294,6 +1405,7 @@ export function vwma(source: Source, length: simple_int, volume?: Source): serie
1294
1405
  * ```
1295
1406
  */
1296
1407
  export function linreg(source: Source, length: simple_int, offset: simple_int = 0): series_float {
1408
+ checkPositiveLength('linreg', length, 0);
1297
1409
  const result: series_float = [];
1298
1410
  // Floor the length to match PineScript's auto-truncation of float to int
1299
1411
  const len = Math.floor(length);
@@ -1363,6 +1475,7 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
1363
1475
  * ```
1364
1476
  */
1365
1477
  export function correlation(source1: Source, source2: Source, length: simple_int): series_float {
1478
+ checkPositiveLength('correlation', length, 0);
1366
1479
  // PineScript: from compensated running sums, cov = sma(x * y) - sma(x) * sma(y), r = cov / sqrt(var(x) * var(y))
1367
1480
  // (biased variances); each running sum skips its own na values
1368
1481
  const product = Array.from({ length: source1.length }, (_, i) => {
@@ -1401,8 +1514,8 @@ export function correlation(source1: Source, source2: Source, length: simple_int
1401
1514
  *
1402
1515
  * PineScript rules:
1403
1516
  * - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
1404
- * - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
1405
- * `<=` the current value
1517
+ * - a value from bar `length` on; an `na` value in the window counts as not `<=` the current value, and an `na`
1518
+ * current value gives 0 (#177)
1406
1519
  * - the comparison uses the 1e-10 tolerance
1407
1520
  *
1408
1521
  * @example
@@ -1413,15 +1526,16 @@ export function correlation(source1: Source, source2: Source, length: simple_int
1413
1526
  * ```
1414
1527
  */
1415
1528
  export function percentrank(source: Source, length: simple_int): series_float {
1529
+ checkPositiveLength('percentrank', length, 0);
1416
1530
  const result: series_float = [];
1417
1531
 
1418
1532
  for (let i = 0; i < source.length; i++) {
1419
1533
  const currentValue = source[i]!;
1420
- if (i < length || Number.isNaN(currentValue)) {
1534
+ if (i < length) {
1421
1535
  result.push(NaN);
1422
1536
  continue;
1423
1537
  }
1424
- // le() is false for an na value in the window
1538
+ // le() is false for an na value in the window, and for all of them when the current value is na (0)
1425
1539
  let countLessOrEqual = 0;
1426
1540
  for (let j = 1; j <= length; j++) {
1427
1541
  if (le(source[i - j]!, currentValue)) countLessOrEqual++;
@@ -1461,6 +1575,7 @@ export function percentrank(source: Source, length: simple_int): series_float {
1461
1575
  * \`\`\`
1462
1576
  */
1463
1577
  export function cci(source: Source, length: simple_int): series_float {
1578
+ checkPositiveLength('cci', length, 0);
1464
1579
  const smaValues = sma(source, length);
1465
1580
  const devValues = dev(source, length);
1466
1581
  let last = 0;
@@ -1497,6 +1612,7 @@ export function cci(source: Source, length: simple_int): series_float {
1497
1612
  * \`\`\`
1498
1613
  */
1499
1614
  export function stoch(source: Source, high: Source, low: Source, length: simple_int): series_float {
1615
+ checkPositiveLength('stoch->highest', length, 0);
1500
1616
  const result: series_float = [];
1501
1617
  const lowestValues = lowest(low, length);
1502
1618
  const highestValues = highest(high, length);
@@ -1549,6 +1665,7 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
1549
1665
  * ```
1550
1666
  */
1551
1667
  export function mfi(source: Source, length: simple_int, volume?: Source): series_float {
1668
+ checkPositiveLength('mfi', length, 0);
1552
1669
  if (!volume) {
1553
1670
  throw new Error(
1554
1671
  'ta.mfi() requires volume series. ' +
@@ -1607,6 +1724,7 @@ export function mfi(source: Source, length: simple_int, volume?: Source): series
1607
1724
  * ```
1608
1725
  */
1609
1726
  export function hma(source: Source, length: simple_int): series_float {
1727
+ checkPositiveLength('hma', length, 0);
1610
1728
  const halfLength = Math.floor(length / 2);
1611
1729
  const sqrtLength = Math.floor(Math.sqrt(length));
1612
1730
 
@@ -1738,35 +1856,31 @@ export function sar(
1738
1856
  return result;
1739
1857
  }
1740
1858
 
1859
+ /** A pivot length: one value, or a value per bar (PineScript series length). */
1860
+ type PivotLength = simple_int | ArrayLike<number>;
1861
+
1741
1862
  /**
1742
1863
  * Pivot detection as PineScript computes it.
1743
1864
  *
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;
1865
+ * The function keeps the non-na values it receives. On each bar it takes the last `leftbars + rightbars + 1` of them
1866
+ * (all of them when there are fewer) and finds the highest (the newest one on equal values; the lowest for a pivot
1867
+ * low): there is a pivot when that value is on the bar `rightbars` bars back, and its value is returned on this bar,
1868
+ * 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
1869
+ * right. `na` before bar `leftbars + rightbars`.
1870
+ */
1871
+ function pivot(source: Source, leftbars: PivotLength, rightbars: PivotLength, isHigh: boolean): series_float {
1872
+ const { values, bars, last } = keptValues(source);
1873
+ return Array.from(source, (_, i) => {
1874
+ // a series length: the lengths of the current bar
1875
+ const left = Math.trunc(typeof leftbars === 'number' ? leftbars : leftbars[i]!);
1876
+ const right = Math.trunc(typeof rightbars === 'number' ? rightbars : rightbars[i]!);
1877
+ const k = last[i]!;
1878
+ if (!(i >= left + right) || k < 0) return NaN;
1879
+ let best = k;
1880
+ for (let j = k - 1; j >= 0 && j >= k - left - right; j--) {
1881
+ if (isHigh ? values[j]! > values[best]! : values[j]! < values[best]!) best = j;
1882
+ }
1883
+ return bars[best] === i - right ? values[best]! : NaN;
1770
1884
  });
1771
1885
  }
1772
1886
 
@@ -1784,7 +1898,10 @@ function pivot(source: Source, leftbars: simple_int, rightbars: simple_int, isHi
1784
1898
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1785
1899
  * index is `i - rightbars`
1786
1900
  * - 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
1901
+ * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the highest of
1902
+ * them must be on the bar `rightbars` bars back
1903
+ * - `leftbars` / `rightbars` can be arrays (a length per bar, PineScript series length), also for `ta.pivotlow`:
1904
+ * each bar uses its own lengths
1788
1905
  *
1789
1906
  * @example
1790
1907
  * ```typescript
@@ -1794,8 +1911,8 @@ function pivot(source: Source, leftbars: simple_int, rightbars: simple_int, isHi
1794
1911
  */
1795
1912
  export function pivothigh(
1796
1913
  sourceOrLeftbars: Source | simple_int,
1797
- leftbarsOrRightbars: simple_int,
1798
- rightbars?: simple_int,
1914
+ leftbarsOrRightbars: PivotLength,
1915
+ rightbars?: PivotLength,
1799
1916
  high?: Source
1800
1917
  ): series_float {
1801
1918
  if (rightbars === undefined) {
@@ -1821,7 +1938,8 @@ export function pivothigh(
1821
1938
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1822
1939
  * index is `i - rightbars`
1823
1940
  * - 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
1941
+ * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the lowest of
1942
+ * them must be on the bar `rightbars` bars back
1825
1943
  *
1826
1944
  * @example
1827
1945
  * ```typescript
@@ -1831,8 +1949,8 @@ export function pivothigh(
1831
1949
  */
1832
1950
  export function pivotlow(
1833
1951
  sourceOrLeftbars: Source | simple_int,
1834
- leftbarsOrRightbars: simple_int,
1835
- rightbars?: simple_int,
1952
+ leftbarsOrRightbars: PivotLength,
1953
+ rightbars?: PivotLength,
1836
1954
  low?: Source
1837
1955
  ): series_float {
1838
1956
  if (rightbars === undefined) {
@@ -2013,6 +2131,7 @@ export function dmi(
2013
2131
  * ```
2014
2132
  */
2015
2133
  export function tsi(source: Source, shortLength: simple_int, longLength: simple_int): series_float {
2134
+ checkPositiveLength('tsi->ema', shortLength, 0);
2016
2135
  const momentum: series_float = [];
2017
2136
 
2018
2137
  // Calculate momentum (price change)
@@ -2062,6 +2181,7 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
2062
2181
  * ```
2063
2182
  */
2064
2183
  export function cmo(source: Source, length: simple_int): series_float {
2184
+ checkPositiveLength('cmo', length, 0);
2065
2185
  // PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
2066
2186
  // sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
2067
2187
  // math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
@@ -2111,6 +2231,7 @@ export function kc(
2111
2231
  low?: Source,
2112
2232
  close?: Source
2113
2233
  ): [series_float, series_float, series_float] {
2234
+ checkPositiveLength('kc', length, 0);
2114
2235
  // Calculate middle band (EMA of source)
2115
2236
  const middle = ema(source, length);
2116
2237
 
@@ -2170,6 +2291,7 @@ export function kc(
2170
2291
  * ```
2171
2292
  */
2172
2293
  export function bbw(source: Source, length: simple_int, mult: simple_float): series_float {
2294
+ checkPositiveLength('bbw', length, 0);
2173
2295
  const [basis, upper, lower] = bb(source, length, mult);
2174
2296
  const result: series_float = [];
2175
2297
 
@@ -2213,6 +2335,7 @@ export function bbw(source: Source, length: simple_int, mult: simple_float): ser
2213
2335
  * ```
2214
2336
  */
2215
2337
  export function wpr(high: Source, low: Source, close: Source, length: simple_int = 14): series_float {
2338
+ checkPositiveLength('wpr', length, 0);
2216
2339
  const highs = highest(high, length);
2217
2340
  const lows = lowest(low, length);
2218
2341
  return Array.from({ length: close.length }, (_, i) => {
@@ -2313,6 +2436,7 @@ export function alma(
2313
2436
  sigma: simple_float = 6,
2314
2437
  floor: boolean = false
2315
2438
  ): series_float {
2439
+ checkPositiveLength('alma', length, 0);
2316
2440
  const result: series_float = [];
2317
2441
  const m = floor ? Math.floor(offset * (length - 1)) : offset * (length - 1);
2318
2442
  const s = length / sigma;
@@ -2409,6 +2533,7 @@ export function kcw(
2409
2533
  * values), and a bar whose own value is `na` still gets a result.
2410
2534
  */
2411
2535
  export function range(source: Source, length: simple_int): series_float {
2536
+ checkPositiveLength('range', length, 0);
2412
2537
  return source.map((_, i) => {
2413
2538
  const values = lastValues(source, i, length);
2414
2539
  return values ? Math.max(...values) - Math.min(...values) : NaN;
@@ -2420,10 +2545,11 @@ export function range(source: Source, length: simple_int): series_float {
2420
2545
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2421
2546
  *
2422
2547
  * @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.
2548
+ * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2549
+ * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2550
+ * `length - 1` and while no value was received; a fractional length is truncated.
2425
2551
  */
2426
- export function highestbars(source: Source, length: simple_int): series_int {
2552
+ export function highestbars(source: Source, length: simple_int | ArrayLike<number>): series_int {
2427
2553
  return extremeOffset(source, length, false);
2428
2554
  }
2429
2555
 
@@ -2432,10 +2558,11 @@ export function highestbars(source: Source, length: simple_int): series_int {
2432
2558
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2433
2559
  *
2434
2560
  * @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.
2561
+ * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2562
+ * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2563
+ * `length - 1` and while no value was received; a fractional length is truncated.
2437
2564
  */
2438
- export function lowestbars(source: Source, length: simple_int): series_int {
2565
+ export function lowestbars(source: Source, length: simple_int | ArrayLike<number>): series_int {
2439
2566
  return extremeOffset(source, length, true);
2440
2567
  }
2441
2568
 
@@ -2492,6 +2619,7 @@ export function min(source: Source): series_float {
2492
2619
  * ```
2493
2620
  */
2494
2621
  export function cog(source: Source, length: simple_int = 10): series_float {
2622
+ checkPositiveLength('cog', length, 0);
2495
2623
  const len = Math.floor(length);
2496
2624
  return runningSum(source, len).map((sum, i) => {
2497
2625
  if (i < len - 1 || Number.isNaN(sum) || sum === 0) return NaN;
@@ -2525,6 +2653,7 @@ export function cog(source: Source, length: simple_int = 10): series_float {
2525
2653
  * - If no mode exists, returns the smallest value
2526
2654
  */
2527
2655
  export function mode(source: Source, length: simple_int): series_float {
2656
+ checkPositiveLength('mode', length, 0);
2528
2657
  const result: series_float = [];
2529
2658
  const len = Math.floor(length);
2530
2659
  const seen: number[] = []; // non-na values so far
@@ -2602,6 +2731,7 @@ export function percentile_linear_interpolation(
2602
2731
  length: simple_int,
2603
2732
  percentage: number
2604
2733
  ): series_float {
2734
+ checkPositiveLength('percentile_linear_interpolation', length, 0);
2605
2735
  return sortedWindow(source, length, (s) => {
2606
2736
  const pos = (percentage / 100) * length - 0.5;
2607
2737
  if (pos <= 0) return s[0]!;
@@ -2671,6 +2801,7 @@ export function percentile_nearest_rank(
2671
2801
  length: simple_int,
2672
2802
  percentage: number
2673
2803
  ): series_float {
2804
+ checkPositiveLength('percentile_nearest_rank', length, 0);
2674
2805
  const rank = Math.min(length, Math.max(1, Math.ceil((percentage / 100) * length)));
2675
2806
  return sortedWindow(source, length, (s) => s[rank - 1]!);
2676
2807
  }
@@ -2707,6 +2838,7 @@ export function percentile_nearest_rank(
2707
2838
  * where the `na` was
2708
2839
  */
2709
2840
  export function rci(source: Source, length: simple_int): series_float {
2841
+ checkPositiveLength('rci', length, 0);
2710
2842
  const len = Math.floor(length);
2711
2843
  const result: series_float = [];
2712
2844
  // PineScript keeps the values in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start
@@ -2770,31 +2902,42 @@ export function rci(source: Source, length: simple_int): series_float {
2770
2902
  }
2771
2903
 
2772
2904
  /**
2773
- * Pivot Point Levels
2905
+ * Pivot Point Levels (PineScript `ta.pivot_point_levels`).
2774
2906
  *
2775
- * Calculates pivot point levels using various calculation methods.
2776
- * Returns an array containing: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
2907
+ * Returns 11 series: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5] (one value per bar in each).
2777
2908
  *
2778
2909
  * @param type - Calculation type: "Traditional", "Fibonacci", "Woodie", "Classic", "DM", "Camarilla"
2779
- * @param anchor - Condition that triggers reset of calculations
2780
- * @param developing - If true, pivots recalculate continuously; if false, use last anchor values
2781
- * @param high - High price series (optional, uses context if not provided)
2782
- * @param low - Low price series (optional, uses context if not provided)
2783
- * @param close - Close price series (optional, uses context if not provided)
2784
- * @param open - Open price series (optional, uses context if not provided)
2785
- * @returns Array of 11 pivot levels
2910
+ * @param anchor - Condition that starts a new period (`timeframe.change("W")`...)
2911
+ * @param developing - false: the levels of the previous period, constant until the next anchor; true: the levels of
2912
+ * the current period so far, recalculated on each bar
2913
+ * @param high - High price series
2914
+ * @param low - Low price series
2915
+ * @param close - Close price series
2916
+ * @param open - Open price series (Woodie and DM)
2917
+ * @returns 11 series of levels
2786
2918
  *
2787
2919
  * @example
2788
2920
  * ```typescript
2789
2921
  * const weekChange = [false, false, false, false, true, false, ...]; // Weekly anchor
2790
- * const pivots = ta.pivot_point_levels("Traditional", weekChange, false, high, low, close);
2791
- * // pivots[i]! = [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
2922
+ * const levels = ta.pivot_point_levels("Traditional", weekChange, false, high, low, close, open);
2923
+ * // levels[0]![i] is P on bar i, levels[5]![i] is R3
2792
2924
  * ```
2793
2925
  *
2794
2926
  * @remarks
2795
- * - Woodie type cannot use developing=true (will error in PineScript)
2796
- * - DM type only calculates P, R1, S1 (other levels are NaN)
2797
- * - All calculations follow PineScript v6 specifications
2927
+ * As in PineScript (#177):
2928
+ * - a period runs from an anchor bar to the bar before the next anchor; the bars before the first anchor are a
2929
+ * period too (from bar 0). The fixed levels use the high and low of the previous period and the close of the bar
2930
+ * before the anchor (na with an anchor on bar 0), and are na until the first anchor; the developing levels use
2931
+ * the current period's high and low and the bar's close, from bar 0
2932
+ * - Traditional: R3 = P * 2 + (H - 2 * L), S3 = P * 2 - (2 * H - L), R4 = P * 3 + (H - 3 * L), S4 = P * 3 - (3 * H - L),
2933
+ * R5 = P * 4 + (H - 4 * L), S5 = P * 4 - (4 * H - L)
2934
+ * - Fibonacci: P +- 0.382 / 0.618 / 1 * (H - L); R4 to S5 na
2935
+ * - Classic: R3 / S3 = P +- 2 * (H - L), R4 / S4 = P +- 3 * (H - L); R5 / S5 na
2936
+ * - Woodie: P = (H + L + 2 * open of the current period) / 4, R3 = H + 2 * (P - L), S3 = L - 2 * (H - P), R4 / S4 =
2937
+ * R3 + (H - L) / S3 - (H - L); R5 / S5 na. `developing` true throws (RE10128)
2938
+ * - DM: x = H + 2 * L + C (C < open of the period), 2 * H + L + C (C > O) or H + L + 2 * C; P = x / 4,
2939
+ * R1 = x / 2 - L, S1 = x / 2 - H; the other levels na
2940
+ * - Camarilla: C +- (H - L) * 1.1 / 12, / 6, / 4, / 2 for R1 to S4, R5 = (H / L) * C, S5 = 2 * C - R5
2798
2941
  */
2799
2942
  export function pivot_point_levels(
2800
2943
  type: series_float | string,
@@ -2805,183 +2948,96 @@ export function pivot_point_levels(
2805
2948
  close?: Source,
2806
2949
  open?: Source
2807
2950
  ): series_float[] {
2808
- // For simplicity, we need to handle the case where type is a string array
2809
- // But according to the docs, type is "series string", meaning it can change per bar
2810
- // For this implementation, we'll support constant type strings
2811
-
2812
2951
  const typeStr = typeof type === 'string' ? type : String(type[0]);
2813
- const isDeveloping = typeof developing === 'boolean' ? developing : developing[0];
2814
-
2815
- // Woodie cannot be developing
2952
+ const isDeveloping = typeof developing === 'boolean' ? developing : Boolean(developing[0]);
2816
2953
  if (typeStr === 'Woodie' && isDeveloping) {
2817
- throw new Error('ta.pivot_point_levels: Woodie type cannot use developing=true');
2954
+ throw new Error(
2955
+ 'Error on bar 0: The `developing` parameter of the `ta.pivot_point_levels()` cannot be `true` when `type` is "Woodie".'
2956
+ );
2818
2957
  }
2819
-
2820
- // Ensure all series have the same length
2821
- const length = anchor.length;
2822
- if (high && high.length !== length) throw new Error('High series length mismatch');
2823
- if (low && low.length !== length) throw new Error('Low series length mismatch');
2824
- if (close && close.length !== length) throw new Error('Close series length mismatch');
2825
- if (open && open.length !== length) throw new Error('Open series length mismatch');
2826
-
2827
- // Initialize result arrays for all 11 levels
2828
- const results: series_float[] = Array.from({ length: 11 }, () => []);
2829
-
2830
- // Track the last anchor point data
2831
- let lastH = NaN, lastL = NaN, lastC = NaN, lastO = NaN;
2832
- let lastAnchorIndex = -1;
2833
-
2834
- for (let i = 0; i < length; i++) {
2835
- // Check if anchor triggered
2836
- if (anchor[i]!) {
2837
- lastAnchorIndex = i;
2838
- // Store OHLC at anchor point (these will be used for calculations)
2839
- lastH = high ? high[i]! : NaN;
2840
- lastL = low ? low[i]! : NaN;
2841
- lastC = close ? close[i]! : NaN;
2842
- lastO = open ? open[i]! : NaN;
2843
- }
2844
-
2845
- // Calculate data to use
2846
- let h: number, l: number, c: number, o: number;
2847
-
2848
- if (isDeveloping && lastAnchorIndex >= 0) {
2849
- // Developing: use max/min/last since anchor
2850
- h = high ? Math.max(...high.slice(lastAnchorIndex, i + 1).filter(v => !isNaN(v))) : NaN;
2851
- l = low ? Math.min(...low.slice(lastAnchorIndex, i + 1).filter(v => !isNaN(v))) : NaN;
2852
- c = close ? close[i]! : NaN;
2853
- o = open && lastAnchorIndex >= 0 ? open[lastAnchorIndex]! : NaN;
2854
- } else {
2855
- // Not developing: use last anchor values
2856
- h = lastH;
2857
- l = lastL;
2858
- c = lastC;
2859
- o = lastO;
2860
- }
2861
-
2862
- // Calculate pivot levels based on type
2863
- const levels = calculatePivotLevels(typeStr, h, l, c, o);
2864
-
2865
- // Push to results
2866
- for (let j = 0; j < 11; j++) {
2867
- results[j]!.push(levels[j]!);
2868
- }
2958
+ const n = anchor.length;
2959
+ for (const [s, name] of [[high, 'High'], [low, 'Low'], [close, 'Close'], [open, 'Open']] as const) {
2960
+ if (s && s.length !== n) throw new Error(`${name} series length mismatch`);
2961
+ }
2962
+ const at = (s: Source | undefined, i: number): number => (s ? (s[i] ?? NaN) : NaN);
2963
+ const results: series_float[] = Array.from({ length: 11 }, () => new Array<number>(n));
2964
+ // the current period (from bar 0 until the first anchor) and the previous one
2965
+ let curH = NaN, curL = NaN, curO = NaN;
2966
+ let prevH = NaN, prevL = NaN, prevC = NaN, prevO = NaN;
2967
+ let anchored = false;
2968
+ for (let i = 0; i < n; i++) {
2969
+ const h = at(high, i), l = at(low, i);
2970
+ if (i === 0) {
2971
+ // the first period starts on bar 0: an anchor there makes bar 0 its own previous period
2972
+ curH = h;
2973
+ curL = l;
2974
+ curO = at(open, i);
2975
+ }
2976
+ if (anchor[i]) {
2977
+ prevH = curH;
2978
+ prevL = curL;
2979
+ prevC = i > 0 ? at(close, i - 1) : NaN;
2980
+ prevO = curO;
2981
+ curH = h;
2982
+ curL = l;
2983
+ curO = at(open, i);
2984
+ anchored = true;
2985
+ } else if (i > 0) {
2986
+ if (h > curH || Number.isNaN(curH)) curH = h;
2987
+ if (l < curL || Number.isNaN(curL)) curL = l;
2988
+ }
2989
+ const levels = isDeveloping
2990
+ ? pivotLevels(typeStr, curH, curL, at(close, i), curO, at(close, i))
2991
+ : anchored
2992
+ ? pivotLevels(typeStr, prevH, prevL, prevC, prevO, curO)
2993
+ : NA_LEVELS;
2994
+ for (let k = 0; k < 11; k++) results[k]![i] = levels[k]!;
2869
2995
  }
2870
-
2871
2996
  return results;
2872
2997
  }
2873
2998
 
2999
+ const NA_LEVELS: readonly number[] = new Array<number>(11).fill(NaN);
3000
+
2874
3001
  /**
2875
- * Helper function to calculate pivot levels
3002
+ * The 11 levels of one bar, in PineScript's operation order (bit for bit). `o` is the open of the period (DM),
3003
+ * `wo` the close term of Woodie (the open of the current period, or the bar's close while developing).
2876
3004
  */
2877
- function calculatePivotLevels(
2878
- type: string,
2879
- h: number,
2880
- l: number,
2881
- c: number,
2882
- o: number
2883
- ): number[] {
2884
- // Return NaN array if data is insufficient
2885
- if (isNaN(h) || isNaN(l) || isNaN(c)) {
2886
- return Array(11).fill(NaN);
2887
- }
2888
-
2889
- const levels: number[] = Array(11).fill(NaN);
2890
-
2891
- // Calculate pivot point (P)
2892
- let P: number;
2893
-
2894
- switch (type) {
2895
- case 'Traditional':
2896
- case 'Fibonacci':
2897
- case 'Classic':
2898
- P = (h + l + c) / 3;
2899
- break;
2900
- case 'Woodie':
2901
- P = (h + l + 2 * c) / 4;
2902
- break;
2903
- case 'DM':
2904
- P = (h + l + c) / 3;
2905
- break;
2906
- case 'Camarilla':
2907
- P = (h + l + c) / 3;
2908
- break;
2909
- default:
2910
- P = (h + l + c) / 3;
2911
- }
2912
-
2913
- levels[0] = P; // P is at index 0
2914
-
2915
- // Calculate resistance and support levels based on type
3005
+ function pivotLevels(type: string, h: number, l: number, c: number, o: number, wo: number): readonly number[] {
3006
+ const N = NaN;
3007
+ const r = h - l;
2916
3008
  switch (type) {
2917
- case 'Traditional':
2918
- case 'Classic':
2919
- levels[1] = 2 * P - l; // R1
2920
- levels[2] = 2 * P - h; // S1
2921
- levels[3] = P + (h - l); // R2
2922
- levels[4] = P - (h - l); // S2
2923
- levels[5] = h + 2 * (P - l); // R3
2924
- levels[6] = l - 2 * (h - P); // S3
2925
- levels[7] = levels[5] + (h - l); // R4
2926
- levels[8] = levels[6] - (h - l); // S4
2927
- levels[9] = levels[7] + (h - l); // R5
2928
- levels[10] = levels[8] - (h - l); // S5
2929
- break;
2930
-
2931
- case 'Fibonacci':
2932
- levels[1] = P + 0.382 * (h - l); // R1
2933
- levels[2] = P - 0.382 * (h - l); // S1
2934
- levels[3] = P + 0.618 * (h - l); // R2
2935
- levels[4] = P - 0.618 * (h - l); // S2
2936
- levels[5] = P + (h - l); // R3
2937
- levels[6] = P - (h - l); // S3
2938
- levels[7] = levels[5] + 0.618 * (h - l); // R4
2939
- levels[8] = levels[6] - 0.618 * (h - l); // S4
2940
- levels[9] = levels[7] + 0.382 * (h - l); // R5
2941
- levels[10] = levels[8] - 0.382 * (h - l); // S5
2942
- break;
2943
-
2944
- case 'Woodie':
2945
- levels[1] = 2 * P - l; // R1
2946
- levels[2] = 2 * P - h; // S1
2947
- levels[3] = P + (h - l); // R2
2948
- levels[4] = P - (h - l); // S2
2949
- levels[5] = h + 2 * (P - l); // R3
2950
- levels[6] = l - 2 * (h - P); // S3
2951
- levels[7] = levels[5] + (h - l); // R4
2952
- levels[8] = levels[6] - (h - l); // S4
2953
- levels[9] = levels[7] + (h - l); // R5
2954
- levels[10] = levels[8] - (h - l); // S5
2955
- break;
2956
-
3009
+ case 'Traditional': {
3010
+ const p = (h + l + c) / 3;
3011
+ return [p, 2 * p - l, 2 * p - h, p + r, p - r, p * 2 + (h - 2 * l), p * 2 - (2 * h - l), p * 3 + (h - 3 * l),
3012
+ p * 3 - (3 * h - l), p * 4 + (h - 4 * l), p * 4 - (4 * h - l)];
3013
+ }
3014
+ case 'Fibonacci': {
3015
+ const p = (h + l + c) / 3;
3016
+ return [p, p + 0.382 * r, p - 0.382 * r, p + 0.618 * r, p - 0.618 * r, p + 1 * r, p - 1 * r, N, N, N, N];
3017
+ }
3018
+ case 'Classic': {
3019
+ const p = (h + l + c) / 3;
3020
+ return [p, 2 * p - l, 2 * p - h, p + r, p - r, p + 2 * r, p - 2 * r, p + 3 * r, p - 3 * r, N, N];
3021
+ }
3022
+ case 'Woodie': {
3023
+ const p = (h + l + 2 * wo) / 4;
3024
+ const r3 = h + 2 * (p - l);
3025
+ const s3 = l - 2 * (h - p);
3026
+ return [p, 2 * p - l, 2 * p - h, p + r, p - r, r3, s3, r3 + r, s3 - r, N, N];
3027
+ }
2957
3028
  case 'DM': {
2958
- // DM (Demark) only calculates P, R1, S1
2959
- const x = h + l + (c * 2) + (isNaN(o) ? c : o);
2960
- const newP = x / (isNaN(o) ? 4 : 5);
2961
- levels[0] = newP;
2962
- levels[1] = x / 2 - l; // R1
2963
- levels[2] = x / 2 - h; // S1
2964
- // R2-S5 remain NaN
2965
- break;
3029
+ const x = c < o ? h + 2 * l + c : c > o ? 2 * h + l + c : h + l + 2 * c;
3030
+ return [x / 4, x / 2 - l, x / 2 - h, N, N, N, N, N, N, N, N];
2966
3031
  }
2967
-
2968
3032
  case 'Camarilla': {
2969
- const range = h - l;
2970
- levels[1] = c + range * 1.1 / 12; // R1
2971
- levels[2] = c - range * 1.1 / 12; // S1
2972
- levels[3] = c + range * 1.1 / 6; // R2
2973
- levels[4] = c - range * 1.1 / 6; // S2
2974
- levels[5] = c + range * 1.1 / 4; // R3
2975
- levels[6] = c - range * 1.1 / 4; // S3
2976
- levels[7] = c + range * 1.1 / 2; // R4
2977
- levels[8] = c - range * 1.1 / 2; // S4
2978
- levels[9] = h; // R5 (high)
2979
- levels[10] = l; // S5 (low)
2980
- break;
3033
+ const p = (h + l + c) / 3;
3034
+ const r5 = (h / l) * c;
3035
+ return [p, c + (r * 1.1) / 12, c - (r * 1.1) / 12, c + (r * 1.1) / 6, c - (r * 1.1) / 6, c + (r * 1.1) / 4,
3036
+ c - (r * 1.1) / 4, c + (r * 1.1) / 2, c - (r * 1.1) / 2, r5, 2 * c - r5];
2981
3037
  }
3038
+ default:
3039
+ return NA_LEVELS;
2982
3040
  }
2983
-
2984
- return levels;
2985
3041
  }
2986
3042
 
2987
3043
  /**