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
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { eq, ge, gt, le, lt } from '../compare/index.js';
11
11
  import { runningSum, runningSumSeries, runningVariance, runningVarianceSeries } from './running-sum.js';
12
+ import { checkPositiveLength, checkSumLength, checkWindowLength } from './limits.js';
12
13
  /**
13
14
  * Simple Moving Average - returns the moving average (sum of last y values divided by y).
14
15
  *
@@ -39,6 +40,7 @@ export function sma(source, length) {
39
40
  }
40
41
  // Floor the length to match PineScript's auto-truncation of float to int
41
42
  const len = Math.floor(length);
43
+ checkSumLength('ta.sma', len, 0);
42
44
  return runningSum(source, len).map((sum) => sum / len);
43
45
  }
44
46
  /**
@@ -62,6 +64,7 @@ export function sma(source, length) {
62
64
  * ```
63
65
  */
64
66
  export function ema(source, length) {
67
+ checkPositiveLength('ema', length, 0);
65
68
  const result = [];
66
69
  const len = Math.floor(length);
67
70
  const multiplier = 2 / (len + 1);
@@ -99,32 +102,20 @@ export function ema(source, length) {
99
102
  }
100
103
  return result;
101
104
  }
102
- /**
103
- * Relative Strength Index - momentum oscillator measuring speed and magnitude of price changes.
104
- *
105
- * @param source - Series of values to process
106
- * @param length - Number of bars (length)
107
- * @returns RSI series (values range from 0 to 100)
108
- *
109
- * @remarks
110
- * - RSI values above 70 typically indicate overbought conditions
111
- * - RSI values below 30 typically indicate oversold conditions
112
- * - Uses `ta.rma()` (Relative Moving Average) for smoothing, matching PineScript v6
113
- * - Formula: RSI = 100 - (100 / (1 + RS)), where RS = Average Gain / Average Loss
114
- * - As in PineScript (`down == 0 ? 100 : up == 0 ? 0 : ...`): 100 when the average loss is 0, else 0 when the
115
- * average gain is 0, both with the 1e-10 tolerance of the comparison operators
116
- * - Average Gain and Average Loss are calculated using RMA (alpha = 1 / length)
117
- * - `na` values in the source series are ignored
118
- *
119
- * @example
120
- * ```typescript
121
- * const rsi14 = ta.rsi(closePrices, 14);
122
- * // Identify overbought/oversold conditions
123
- * const overbought = rsi14.map(v => v > 70);
124
- * const oversold = rsi14.map(v => v < 30);
125
- * ```
126
- */
127
105
  export function rsi(source, length) {
106
+ if (typeof length !== 'number') {
107
+ return Array.from(source, (xv, i) => {
108
+ const x = xv ?? NaN;
109
+ const y = length[i] ?? NaN;
110
+ if (eq(y, 0))
111
+ return 100;
112
+ if (eq(x, 0))
113
+ return 0;
114
+ const r = 100 - 100 / (1 + x / y);
115
+ return Number.isFinite(r) ? r : NaN;
116
+ });
117
+ }
118
+ checkPositiveLength('rsi', length, 0, 'y');
128
119
  const result = [];
129
120
  // Calculate price changes
130
121
  const changes = [];
@@ -208,6 +199,7 @@ export function macd(source, fastLength, slowLength, signalLength) {
208
199
  * ```
209
200
  */
210
201
  export function bb(series, length, mult) {
202
+ checkSumLength('ta.bb', length, 0);
211
203
  const basis = sma(series, length);
212
204
  const dev = stdev(series, length);
213
205
  const upper = [];
@@ -250,14 +242,15 @@ function lastValues(source, i, length) {
250
242
  }
251
243
  /**
252
244
  * The extreme of the window of `length` bars ending at each bar, in one pass (monotonic deque): the bar index of
253
- * the oldest highest (or lowest) value, or -1 when the window is empty. As PineScript does for ta.highest /
254
- * ta.lowest / ta.highestbars / ta.lowestbars, the window stops at the first `na` value going back, so a bar whose
255
- * own value is `na` has an empty window. `zeros[i]` tells whether the window holds a +0 (highest) or a -0
256
- * (lowest), the zero that `Math.max` / `Math.min` return when the extreme is 0.
245
+ * the oldest highest (or lowest) value (the newest one with `newest`), or -1 when the window is empty. The window
246
+ * stops at the first `na` value going back (the callers pass the non-na values only, see {@link keptValues}), and a
247
+ * fractional length is truncated (7.5 is 7 bars), as PineScript does.
248
+ * `zeros[i]` tells whether the window holds a +0 (highest) or a -0 (lowest), the zero that `Math.max` / `Math.min`
249
+ * return when the extreme is 0 (not kept with `newest`).
257
250
  */
258
- function windowExtremes(source, length, lowest) {
251
+ function windowExtremes(source, length, lowest, newest = false) {
259
252
  const n = source.length;
260
- const span = length > 0 ? Math.ceil(length) : 0; // the bars j = 0, 1... with j < length
253
+ const span = length > 0 ? Math.trunc(length) : 0; // PineScript casts the length to int
261
254
  const at = new Int32Array(n).fill(-1);
262
255
  const zeros = new Uint8Array(n);
263
256
  const queue = new Int32Array(n); // bar indices; values from best to worst, the oldest first among equal values
@@ -275,8 +268,11 @@ function windowExtremes(source, length, lowest) {
275
268
  zeroCount = 0;
276
269
  continue;
277
270
  }
278
- // drop the values that the new one beats; equal values stay (the oldest wins ties)
279
- while (tail > head && (lowest ? source[queue[tail - 1]] > x : source[queue[tail - 1]] < x)) {
271
+ // drop the values that the new one beats; equal values stay (the oldest wins ties), or go with `newest`
272
+ while (tail > head) {
273
+ const last = source[queue[tail - 1]];
274
+ if (!(lowest ? last > x : last < x) && !(newest && last === x))
275
+ break;
280
276
  if (isPreferredZero(queue[--tail]))
281
277
  zeroCount--;
282
278
  }
@@ -295,20 +291,81 @@ function windowExtremes(source, length, lowest) {
295
291
  }
296
292
  return { at, zeros };
297
293
  }
298
- /** Offset of the best value of the window; on ties the oldest bar wins. 0 when the window is empty. */
294
+ /**
295
+ * The non-na values of `source` with their bars, and for each bar the position of its last non-na value (-1 when
296
+ * there is none yet). PineScript's ta.highest / ta.lowest / ta.highestbars / ta.lowestbars / ta.pivothigh /
297
+ * ta.pivotlow keep the non-na values they receive and nothing else.
298
+ */
299
+ function keptValues(source) {
300
+ const values = [];
301
+ const bars = [];
302
+ const last = new Int32Array(source.length);
303
+ for (let i = 0; i < source.length; i++) {
304
+ const x = source[i];
305
+ if (!Number.isNaN(x)) {
306
+ values.push(x);
307
+ bars.push(i);
308
+ }
309
+ last[i] = values.length - 1;
310
+ }
311
+ return { values, bars, last };
312
+ }
313
+ /**
314
+ * ta.highest / ta.lowest / ta.highestbars / ta.lowestbars with a series length (a length per bar): on each bar the
315
+ * best of the last `length[i]` non-na values (the newest one on ties), na before bar `length[i] - 1`, as PineScript
316
+ * (measured with a length that changes on each bar, with and without na in the source).
317
+ */
318
+ function extremeSeriesLength(source, lengths, lowest, offsets) {
319
+ const name = (lowest ? 'lowest' : 'highest') + (offsets ? 'bars' : '');
320
+ const { values, bars, last } = keptValues(source);
321
+ return Array.from(source, (_, i) => {
322
+ const len = Math.trunc(lengths[i]);
323
+ checkWindowLength(name, len, i);
324
+ const k = last[i];
325
+ if (!(len >= 1) || i < len - 1 || k < 0)
326
+ return NaN;
327
+ let best = k;
328
+ for (let j = k - 1; j >= 0 && j > k - len; j--) {
329
+ if (lowest ? values[j] < values[best] : values[j] > values[best])
330
+ best = j;
331
+ }
332
+ return offsets ? -(i - bars[best]) : values[best];
333
+ });
334
+ }
335
+ /**
336
+ * Offset to the best of the last `length` non-na values (the newest one on ties): the distance in bars, so below
337
+ * `-(length - 1)` after an na value. na before bar `length - 1` and while there is no value.
338
+ */
299
339
  function extremeOffset(source, length, lowest) {
300
- const { at } = windowExtremes(source, length, lowest);
301
- // -(i - at): -0 when the extreme is the current bar, as the offset -j of the window scan
302
- return source.map((_, i) => (i < length - 1 ? NaN : at[i] < 0 ? 0 : -(i - at[i])));
340
+ if (typeof length !== 'number')
341
+ return extremeSeriesLength(source, length, lowest, true);
342
+ checkWindowLength(lowest ? 'lowestbars' : 'highestbars', length, 0);
343
+ const { values, bars, last } = keptValues(source);
344
+ const { at } = windowExtremes(values, length, lowest, true);
345
+ const len = Math.trunc(length);
346
+ // -(i - bar): -0 when the extreme is the current bar, as the offset -j of the window scan
347
+ return source.map((_, i) => {
348
+ const k = last[i];
349
+ return i < len - 1 || k < 0 || at[k] < 0 ? NaN : -(i - bars[at[k]]);
350
+ });
303
351
  }
304
- /** Best value of the window (`na` when the window is empty), with the zero sign of `Math.max` / `Math.min`. */
352
+ /**
353
+ * Best of the last `length` non-na values (of all of them when there are fewer), with the zero sign of `Math.max` /
354
+ * `Math.min`. na before bar `length - 1` and while there is no value.
355
+ */
305
356
  function extremeValue(source, length, lowest) {
306
- const { at, zeros } = windowExtremes(source, length, lowest);
357
+ if (typeof length !== 'number')
358
+ return extremeSeriesLength(source, length, lowest, false);
359
+ checkWindowLength(lowest ? 'lowest' : 'highest', length, 0);
360
+ const { values, last } = keptValues(source);
361
+ const { at, zeros } = windowExtremes(values, length, lowest);
362
+ const len = Math.trunc(length);
307
363
  return source.map((_, i) => {
308
- if (i < length - 1 || at[i] < 0)
364
+ const k = last[i];
365
+ if (i < len - 1 || k < 0 || at[k] < 0)
309
366
  return NaN;
310
- const v = source[at[i]];
311
- return v === 0 && zeros[i] ? (lowest ? -0 : 0) : v;
367
+ const v = values[at[k]];
368
+ return v === 0 && zeros[k] ? (lowest ? -0 : 0) : v;
312
369
  });
313
370
  }
314
371
  /**
@@ -395,6 +452,7 @@ export function crossunder(series1, series2) {
395
452
  * ```
396
453
  */
397
454
  export function change(source, length = 1) {
455
+ checkPositiveLength('change', length, 0);
398
456
  const result = [];
399
457
  for (let i = 0; i < source.length; i++) {
400
458
  if (i < length) {
@@ -492,6 +550,7 @@ export function tr(handle_na = false, high, low, close) {
492
550
  * ```
493
551
  */
494
552
  export function atr(length, high, low, close) {
553
+ checkPositiveLength('atr->rma', length, 0);
495
554
  if (!high || !low || !close) {
496
555
  throw new Error('ta.atr() requires high, low, and close series. ' +
497
556
  'Either pass them explicitly or use createContext({ chart: { high, low, close } }) for implicit data.');
@@ -616,6 +675,7 @@ export function supertrend(factor, atrPeriod, high, low, close, wicks = false) {
616
675
  * ```
617
676
  */
618
677
  export function rma(source, length) {
678
+ checkPositiveLength('rma', length, 0);
619
679
  const result = [];
620
680
  // Floor the length to match PineScript's auto-truncation of float to int
621
681
  const len = Math.floor(length);
@@ -682,6 +742,8 @@ function wmaAdd(a, b) {
682
742
  * - Summed from the oldest value to the newest; as PineScript, a partial sum `s + t` with
683
743
  * `|s + t| <= 1e-10 * max(1, |s| + |t|)` is 0 (a cancellation residue, or values below 1e-10)
684
744
  * - More responsive to recent price changes than SMA
745
+ * - `length` can be an array (a length per bar, PineScript series length): each bar gives the result of its own
746
+ * length (`na` for a length below 1)
685
747
  *
686
748
  * @example
687
749
  * ```typescript
@@ -692,7 +754,9 @@ function wmaAdd(a, b) {
692
754
  export function wma(source, length) {
693
755
  const result = [];
694
756
  // Floor the length to match PineScript's auto-truncation of float to int
695
- const len = Math.floor(length);
757
+ const fixed = typeof length === 'number' ? Math.floor(length) : NaN;
758
+ if (typeof length === 'number')
759
+ checkPositiveLength('wma', fixed, 0);
696
760
  // PineScript: na on a bar with an na source (also +/-Infinity) and until `len` non-na values were received;
697
761
  // otherwise the last `len` bars, each na replaced by the previous non-na value
698
762
  const filled = [];
@@ -706,7 +770,11 @@ export function wma(source, length) {
706
770
  count++;
707
771
  }
708
772
  filled.push(last);
709
- if (isNa || count < len) {
773
+ // a series length: the window of this bar's length (the result of ta.wma with that fixed length)
774
+ const len = typeof length === 'number' ? fixed : Math.floor(length[i]);
775
+ if (typeof length !== 'number')
776
+ checkPositiveLength('wma', len, i);
777
+ if (isNa || !(len >= 1) || count < len) {
710
778
  result.push(NaN);
711
779
  continue;
712
780
  }
@@ -730,8 +798,12 @@ export function wma(source, length) {
730
798
  * @returns Highest value series (na on the first `length - 1` bars)
731
799
  *
732
800
  * @remarks
733
- * As in PineScript: an `na` value ends the window, so only the values after the last `na`
734
- * count, and a bar whose own value is `na` gives `na`.
801
+ * As in PineScript: `na` values are skipped. The result is the highest of the last `length` non-na values (of all
802
+ * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
803
+ * value was received. A fractional length is truncated (7.5 is 7 bars).
804
+ * `length` can be an array (a length per bar, PineScript series length), also for `ta.lowest`, `ta.highestbars`
805
+ * and `ta.lowestbars`: each bar uses its own length.
806
+ * A length above 15,000 throws the PineScript runtime error (also `ta.lowest`, `ta.highestbars`, `ta.lowestbars`).
735
807
  * PineScript `ta.highest(length)` (source = high) is `ta.highest(length)` in the script API.
736
808
  */
737
809
  export function highest(source, length) {
@@ -745,8 +817,9 @@ export function highest(source, length) {
745
817
  * @returns Lowest value series (na on the first `length - 1` bars)
746
818
  *
747
819
  * @remarks
748
- * As in PineScript: an `na` value ends the window, so only the values after the last `na`
749
- * count, and a bar whose own value is `na` gives `na`.
820
+ * As in PineScript: `na` values are skipped. The result is the lowest of the last `length` non-na values (of all
821
+ * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
822
+ * value was received.
750
823
  */
751
824
  export function lowest(source, length) {
752
825
  return extremeValue(source, length, true);
@@ -860,6 +933,7 @@ function monotonic(source, length, step) {
860
933
  * ```
861
934
  */
862
935
  export function rising(source, length) {
936
+ checkPositiveLength('rising', length, 0);
863
937
  return monotonic(source, length, gt);
864
938
  }
865
939
  /**
@@ -884,6 +958,7 @@ export function rising(source, length) {
884
958
  * ```
885
959
  */
886
960
  export function falling(source, length) {
961
+ checkPositiveLength('falling', length, 0);
887
962
  return monotonic(source, length, lt);
888
963
  }
889
964
  /**
@@ -908,6 +983,7 @@ export function falling(source, length) {
908
983
  * ```
909
984
  */
910
985
  export function roc(source, length) {
986
+ checkPositiveLength('roc', length, 0);
911
987
  const result = [];
912
988
  for (let i = 0; i < source.length; i++) {
913
989
  if (i < length) {
@@ -947,6 +1023,7 @@ export function roc(source, length) {
947
1023
  * ```
948
1024
  */
949
1025
  export function mom(source, length) {
1026
+ checkPositiveLength('mom', length, 0);
950
1027
  const result = [];
951
1028
  for (let i = 0; i < source.length; i++) {
952
1029
  if (i < length) {
@@ -985,6 +1062,7 @@ export function mom(source, length) {
985
1062
  * ```
986
1063
  */
987
1064
  export function dev(source, length) {
1065
+ checkPositiveLength('dev', length, 0);
988
1066
  const len = Math.floor(length);
989
1067
  return sma(source, len).map((mean, i) => {
990
1068
  if (i < len - 1 || Number.isNaN(mean))
@@ -1031,6 +1109,7 @@ function varianceOf(source, length, biased, name) {
1031
1109
  return biased ? result : result.map((v, i) => (Math.floor(length[i]) > 1 ? v : NaN));
1032
1110
  }
1033
1111
  const len = Math.floor(length);
1112
+ checkSumLength(name, len, 0);
1034
1113
  // PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
1035
1114
  if ((biased ? len : len - 1) <= 0)
1036
1115
  return source.map(() => NaN);
@@ -1058,6 +1137,7 @@ function varianceOf(source, length, biased, name) {
1058
1137
  * ```
1059
1138
  */
1060
1139
  export function median(source, length) {
1140
+ checkPositiveLength('median', length, 0);
1061
1141
  const len = Math.floor(length);
1062
1142
  const n = source.length;
1063
1143
  const out = new Array(n).fill(NaN);
@@ -1146,6 +1226,7 @@ export function swma(source) {
1146
1226
  * ```
1147
1227
  */
1148
1228
  export function vwma(source, length, volume) {
1229
+ checkPositiveLength('vwma->sma', length, 0);
1149
1230
  if (!volume) {
1150
1231
  throw new Error('ta.vwma() requires volume series. ' +
1151
1232
  'Either pass it explicitly or use createContext({ chart: { ..., volume } }) for implicit data.');
@@ -1193,6 +1274,7 @@ export function vwma(source, length, volume) {
1193
1274
  * ```
1194
1275
  */
1195
1276
  export function linreg(source, length, offset = 0) {
1277
+ checkPositiveLength('linreg', length, 0);
1196
1278
  const result = [];
1197
1279
  // Floor the length to match PineScript's auto-truncation of float to int
1198
1280
  const len = Math.floor(length);
@@ -1260,6 +1342,7 @@ export function linreg(source, length, offset = 0) {
1260
1342
  * ```
1261
1343
  */
1262
1344
  export function correlation(source1, source2, length) {
1345
+ checkPositiveLength('correlation', length, 0);
1263
1346
  // PineScript: from compensated running sums, cov = sma(x * y) - sma(x) * sma(y), r = cov / sqrt(var(x) * var(y))
1264
1347
  // (biased variances); each running sum skips its own na values
1265
1348
  const product = Array.from({ length: source1.length }, (_, i) => {
@@ -1299,8 +1382,8 @@ export function correlation(source1, source2, length) {
1299
1382
  *
1300
1383
  * PineScript rules:
1301
1384
  * - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
1302
- * - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
1303
- * `<=` the current value
1385
+ * - a value from bar `length` on; an `na` value in the window counts as not `<=` the current value, and an `na`
1386
+ * current value gives 0 (#177)
1304
1387
  * - the comparison uses the 1e-10 tolerance
1305
1388
  *
1306
1389
  * @example
@@ -1311,14 +1394,15 @@ export function correlation(source1, source2, length) {
1311
1394
  * ```
1312
1395
  */
1313
1396
  export function percentrank(source, length) {
1397
+ checkPositiveLength('percentrank', length, 0);
1314
1398
  const result = [];
1315
1399
  for (let i = 0; i < source.length; i++) {
1316
1400
  const currentValue = source[i];
1317
- if (i < length || Number.isNaN(currentValue)) {
1401
+ if (i < length) {
1318
1402
  result.push(NaN);
1319
1403
  continue;
1320
1404
  }
1321
- // le() is false for an na value in the window
1405
+ // le() is false for an na value in the window, and for all of them when the current value is na (0)
1322
1406
  let countLessOrEqual = 0;
1323
1407
  for (let j = 1; j <= length; j++) {
1324
1408
  if (le(source[i - j], currentValue))
@@ -1357,6 +1441,7 @@ export function percentrank(source, length) {
1357
1441
  * \`\`\`
1358
1442
  */
1359
1443
  export function cci(source, length) {
1444
+ checkPositiveLength('cci', length, 0);
1360
1445
  const smaValues = sma(source, length);
1361
1446
  const devValues = dev(source, length);
1362
1447
  let last = 0;
@@ -1394,6 +1479,7 @@ export function cci(source, length) {
1394
1479
  * \`\`\`
1395
1480
  */
1396
1481
  export function stoch(source, high, low, length) {
1482
+ checkPositiveLength('stoch->highest', length, 0);
1397
1483
  const result = [];
1398
1484
  const lowestValues = lowest(low, length);
1399
1485
  const highestValues = highest(high, length);
@@ -1443,6 +1529,7 @@ export function stoch(source, high, low, length) {
1443
1529
  * ```
1444
1530
  */
1445
1531
  export function mfi(source, length, volume) {
1532
+ checkPositiveLength('mfi', length, 0);
1446
1533
  if (!volume) {
1447
1534
  throw new Error('ta.mfi() requires volume series. ' +
1448
1535
  'Either pass it explicitly or use createContext({ chart: { ..., volume } }) for implicit data.');
@@ -1498,6 +1585,7 @@ export function mfi(source, length, volume) {
1498
1585
  * ```
1499
1586
  */
1500
1587
  export function hma(source, length) {
1588
+ checkPositiveLength('hma', length, 0);
1501
1589
  const halfLength = Math.floor(length / 2);
1502
1590
  const sqrtLength = Math.floor(Math.sqrt(length));
1503
1591
  const wmaHalf = wma(source, halfLength);
@@ -1613,37 +1701,27 @@ export function sar(start, inc, max, high, low, close) {
1613
1701
  /**
1614
1702
  * Pivot detection as PineScript computes it.
1615
1703
  *
1616
- * The value of the pivot bar is returned `rightbars` bars later, on the bar where the pivot is
1617
- * confirmed, so no future bar is used. A pivot high may equal values on its left but must be higher
1618
- * than every value on its right (a pivot low: equal on the left, lower on the right). An `na`
1619
- * neighbour ends the check on its side; an `na` pivot value gives no pivot.
1704
+ * The function keeps the non-na values it receives. On each bar it takes the last `leftbars + rightbars + 1` of them
1705
+ * (all of them when there are fewer) and finds the highest (the newest one on equal values; the lowest for a pivot
1706
+ * low): there is a pivot when that value is on the bar `rightbars` bars back, and its value is returned on this bar,
1707
+ * 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
1708
+ * right. `na` before bar `leftbars + rightbars`.
1620
1709
  */
1621
1710
  function pivot(source, leftbars, rightbars, isHigh) {
1622
- // "beats" = the neighbour prevents the pivot
1623
- const beatsLeft = isHigh ? (x, v) => x > v : (x, v) => x < v;
1624
- const beatsRight = isHigh ? (x, v) => x >= v : (x, v) => x <= v;
1625
- return source.map((_, i) => {
1626
- const center = i - rightbars;
1627
- if (center - leftbars < 0)
1628
- return NaN;
1629
- const v = source[center];
1630
- if (Number.isNaN(v))
1711
+ const { values, bars, last } = keptValues(source);
1712
+ return Array.from(source, (_, i) => {
1713
+ // a series length: the lengths of the current bar
1714
+ const left = Math.trunc(typeof leftbars === 'number' ? leftbars : leftbars[i]);
1715
+ const right = Math.trunc(typeof rightbars === 'number' ? rightbars : rightbars[i]);
1716
+ const k = last[i];
1717
+ if (!(i >= left + right) || k < 0)
1631
1718
  return NaN;
1632
- for (let j = 1; j <= leftbars; j++) {
1633
- const x = source[center - j];
1634
- if (Number.isNaN(x))
1635
- break;
1636
- if (beatsLeft(x, v))
1637
- return NaN;
1638
- }
1639
- for (let j = 1; j <= rightbars; j++) {
1640
- const x = source[center + j];
1641
- if (Number.isNaN(x))
1642
- break;
1643
- if (beatsRight(x, v))
1644
- return NaN;
1719
+ let best = k;
1720
+ for (let j = k - 1; j >= 0 && j >= k - left - right; j--) {
1721
+ if (isHigh ? values[j] > values[best] : values[j] < values[best])
1722
+ best = j;
1645
1723
  }
1646
- return v;
1724
+ return bars[best] === i - right ? values[best] : NaN;
1647
1725
  });
1648
1726
  }
1649
1727
  /**
@@ -1660,7 +1738,10 @@ function pivot(source, leftbars, rightbars, isHigh) {
1660
1738
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1661
1739
  * index is `i - rightbars`
1662
1740
  * - A pivot high may equal values on its left but must be higher than all values on its right
1663
- * - An `na` neighbour ends the check on its side
1741
+ * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the highest of
1742
+ * them must be on the bar `rightbars` bars back
1743
+ * - `leftbars` / `rightbars` can be arrays (a length per bar, PineScript series length), also for `ta.pivotlow`:
1744
+ * each bar uses its own lengths
1664
1745
  *
1665
1746
  * @example
1666
1747
  * ```typescript
@@ -1691,7 +1772,8 @@ export function pivothigh(sourceOrLeftbars, leftbarsOrRightbars, rightbars, high
1691
1772
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1692
1773
  * index is `i - rightbars`
1693
1774
  * - A pivot low may equal values on its left but must be lower than all values on its right
1694
- * - An `na` neighbour ends the check on its side
1775
+ * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the lowest of
1776
+ * them must be on the bar `rightbars` bars back
1695
1777
  *
1696
1778
  * @example
1697
1779
  * ```typescript
@@ -1861,6 +1943,7 @@ export function dmi(diLength, adxSmoothing, high, low, close) {
1861
1943
  * ```
1862
1944
  */
1863
1945
  export function tsi(source, shortLength, longLength) {
1946
+ checkPositiveLength('tsi->ema', shortLength, 0);
1864
1947
  const momentum = [];
1865
1948
  // Calculate momentum (price change)
1866
1949
  for (let i = 0; i < source.length; i++) {
@@ -1906,6 +1989,7 @@ export function tsi(source, shortLength, longLength) {
1906
1989
  * ```
1907
1990
  */
1908
1991
  export function cmo(source, length) {
1992
+ checkPositiveLength('cmo', length, 0);
1909
1993
  // PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
1910
1994
  // sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
1911
1995
  // math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
@@ -1946,6 +2030,7 @@ export function cmo(source, length) {
1946
2030
  * ```
1947
2031
  */
1948
2032
  export function kc(source, length, mult, useTrueRange = true, high, low, close) {
2033
+ checkPositiveLength('kc', length, 0);
1949
2034
  // Calculate middle band (EMA of source)
1950
2035
  const middle = ema(source, length);
1951
2036
  // Calculate range
@@ -2000,6 +2085,7 @@ export function kc(source, length, mult, useTrueRange = true, high, low, close)
2000
2085
  * ```
2001
2086
  */
2002
2087
  export function bbw(source, length, mult) {
2088
+ checkPositiveLength('bbw', length, 0);
2003
2089
  const [basis, upper, lower] = bb(source, length, mult);
2004
2090
  const result = [];
2005
2091
  for (let i = 0; i < source.length; i++) {
@@ -2041,6 +2127,7 @@ export function bbw(source, length, mult) {
2041
2127
  * ```
2042
2128
  */
2043
2129
  export function wpr(high, low, close, length = 14) {
2130
+ checkPositiveLength('wpr', length, 0);
2044
2131
  const highs = highest(high, length);
2045
2132
  const lows = lowest(low, length);
2046
2133
  return Array.from({ length: close.length }, (_, i) => {
@@ -2105,6 +2192,7 @@ export function vwap(source, volume, anchor, stdev_mult) {
2105
2192
  * ```
2106
2193
  */
2107
2194
  export function alma(source, length = 9, offset = 0.85, sigma = 6, floor = false) {
2195
+ checkPositiveLength('alma', length, 0);
2108
2196
  const result = [];
2109
2197
  const m = floor ? Math.floor(offset * (length - 1)) : offset * (length - 1);
2110
2198
  const s = length / sigma;
@@ -2183,6 +2271,7 @@ export function kcw(source, length = 20, mult = 2, useTrueRange = true, high, lo
2183
2271
  * values), and a bar whose own value is `na` still gets a result.
2184
2272
  */
2185
2273
  export function range(source, length) {
2274
+ checkPositiveLength('range', length, 0);
2186
2275
  return source.map((_, i) => {
2187
2276
  const values = lastValues(source, i, length);
2188
2277
  return values ? Math.max(...values) - Math.min(...values) : NaN;
@@ -2193,8 +2282,9 @@ export function range(source, length) {
2193
2282
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2194
2283
  *
2195
2284
  * @remarks
2196
- * As in PineScript: on equal values the oldest bar wins; an `na` value ends the window;
2197
- * 0 when the window has no value.
2285
+ * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2286
+ * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2287
+ * `length - 1` and while no value was received; a fractional length is truncated.
2198
2288
  */
2199
2289
  export function highestbars(source, length) {
2200
2290
  return extremeOffset(source, length, false);
@@ -2204,8 +2294,9 @@ export function highestbars(source, length) {
2204
2294
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2205
2295
  *
2206
2296
  * @remarks
2207
- * As in PineScript: on equal values the oldest bar wins; an `na` value ends the window;
2208
- * 0 when the window has no value.
2297
+ * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2298
+ * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2299
+ * `length - 1` and while no value was received; a fractional length is truncated.
2209
2300
  */
2210
2301
  export function lowestbars(source, length) {
2211
2302
  return extremeOffset(source, length, true);
@@ -2263,6 +2354,7 @@ export function min(source) {
2263
2354
  * ```
2264
2355
  */
2265
2356
  export function cog(source, length = 10) {
2357
+ checkPositiveLength('cog', length, 0);
2266
2358
  const len = Math.floor(length);
2267
2359
  return runningSum(source, len).map((sum, i) => {
2268
2360
  if (i < len - 1 || Number.isNaN(sum) || sum === 0)
@@ -2297,6 +2389,7 @@ export function cog(source, length = 10) {
2297
2389
  * - If no mode exists, returns the smallest value
2298
2390
  */
2299
2391
  export function mode(source, length) {
2392
+ checkPositiveLength('mode', length, 0);
2300
2393
  const result = [];
2301
2394
  const len = Math.floor(length);
2302
2395
  const seen = []; // non-na values so far
@@ -2364,6 +2457,7 @@ export function mode(source, length) {
2364
2457
  * `na` values are before all numbers
2365
2458
  */
2366
2459
  export function percentile_linear_interpolation(source, length, percentage) {
2460
+ checkPositiveLength('percentile_linear_interpolation', length, 0);
2367
2461
  return sortedWindow(source, length, (s) => {
2368
2462
  const pos = (percentage / 100) * length - 0.5;
2369
2463
  if (pos <= 0)
@@ -2430,6 +2524,7 @@ function sortedWindow(source, length, fn) {
2430
2524
  * - `na` values stay in the window, sorted from bar to bar as in `ta.percentile_linear_interpolation`
2431
2525
  */
2432
2526
  export function percentile_nearest_rank(source, length, percentage) {
2527
+ checkPositiveLength('percentile_nearest_rank', length, 0);
2433
2528
  const rank = Math.min(length, Math.max(1, Math.ceil((percentage / 100) * length)));
2434
2529
  return sortedWindow(source, length, (s) => s[rank - 1]);
2435
2530
  }
@@ -2465,6 +2560,7 @@ export function percentile_nearest_rank(source, length, percentage) {
2465
2560
  * where the `na` was
2466
2561
  */
2467
2562
  export function rci(source, length) {
2563
+ checkPositiveLength('rci', length, 0);
2468
2564
  const len = Math.floor(length);
2469
2565
  const result = [];
2470
2566
  // PineScript keeps the values in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start
@@ -2530,187 +2626,135 @@ export function rci(source, length) {
2530
2626
  return result;
2531
2627
  }
2532
2628
  /**
2533
- * Pivot Point Levels
2629
+ * Pivot Point Levels (PineScript `ta.pivot_point_levels`).
2534
2630
  *
2535
- * Calculates pivot point levels using various calculation methods.
2536
- * Returns an array containing: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
2631
+ * Returns 11 series: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5] (one value per bar in each).
2537
2632
  *
2538
2633
  * @param type - Calculation type: "Traditional", "Fibonacci", "Woodie", "Classic", "DM", "Camarilla"
2539
- * @param anchor - Condition that triggers reset of calculations
2540
- * @param developing - If true, pivots recalculate continuously; if false, use last anchor values
2541
- * @param high - High price series (optional, uses context if not provided)
2542
- * @param low - Low price series (optional, uses context if not provided)
2543
- * @param close - Close price series (optional, uses context if not provided)
2544
- * @param open - Open price series (optional, uses context if not provided)
2545
- * @returns Array of 11 pivot levels
2634
+ * @param anchor - Condition that starts a new period (`timeframe.change("W")`...)
2635
+ * @param developing - false: the levels of the previous period, constant until the next anchor; true: the levels of
2636
+ * the current period so far, recalculated on each bar
2637
+ * @param high - High price series
2638
+ * @param low - Low price series
2639
+ * @param close - Close price series
2640
+ * @param open - Open price series (Woodie and DM)
2641
+ * @returns 11 series of levels
2546
2642
  *
2547
2643
  * @example
2548
2644
  * ```typescript
2549
2645
  * const weekChange = [false, false, false, false, true, false, ...]; // Weekly anchor
2550
- * const pivots = ta.pivot_point_levels("Traditional", weekChange, false, high, low, close);
2551
- * // pivots[i]! = [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
2646
+ * const levels = ta.pivot_point_levels("Traditional", weekChange, false, high, low, close, open);
2647
+ * // levels[0]![i] is P on bar i, levels[5]![i] is R3
2552
2648
  * ```
2553
2649
  *
2554
2650
  * @remarks
2555
- * - Woodie type cannot use developing=true (will error in PineScript)
2556
- * - DM type only calculates P, R1, S1 (other levels are NaN)
2557
- * - All calculations follow PineScript v6 specifications
2651
+ * As in PineScript (#177):
2652
+ * - a period runs from an anchor bar to the bar before the next anchor; the bars before the first anchor are a
2653
+ * period too (from bar 0). The fixed levels use the high and low of the previous period and the close of the bar
2654
+ * before the anchor (na with an anchor on bar 0), and are na until the first anchor; the developing levels use
2655
+ * the current period's high and low and the bar's close, from bar 0
2656
+ * - 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),
2657
+ * R5 = P * 4 + (H - 4 * L), S5 = P * 4 - (4 * H - L)
2658
+ * - Fibonacci: P +- 0.382 / 0.618 / 1 * (H - L); R4 to S5 na
2659
+ * - Classic: R3 / S3 = P +- 2 * (H - L), R4 / S4 = P +- 3 * (H - L); R5 / S5 na
2660
+ * - Woodie: P = (H + L + 2 * open of the current period) / 4, R3 = H + 2 * (P - L), S3 = L - 2 * (H - P), R4 / S4 =
2661
+ * R3 + (H - L) / S3 - (H - L); R5 / S5 na. `developing` true throws (RE10128)
2662
+ * - 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,
2663
+ * R1 = x / 2 - L, S1 = x / 2 - H; the other levels na
2664
+ * - Camarilla: C +- (H - L) * 1.1 / 12, / 6, / 4, / 2 for R1 to S4, R5 = (H / L) * C, S5 = 2 * C - R5
2558
2665
  */
2559
2666
  export function pivot_point_levels(type, anchor, developing = false, high, low, close, open) {
2560
- // For simplicity, we need to handle the case where type is a string array
2561
- // But according to the docs, type is "series string", meaning it can change per bar
2562
- // For this implementation, we'll support constant type strings
2563
2667
  const typeStr = typeof type === 'string' ? type : String(type[0]);
2564
- const isDeveloping = typeof developing === 'boolean' ? developing : developing[0];
2565
- // Woodie cannot be developing
2668
+ const isDeveloping = typeof developing === 'boolean' ? developing : Boolean(developing[0]);
2566
2669
  if (typeStr === 'Woodie' && isDeveloping) {
2567
- throw new Error('ta.pivot_point_levels: Woodie type cannot use developing=true');
2568
- }
2569
- // Ensure all series have the same length
2570
- const length = anchor.length;
2571
- if (high && high.length !== length)
2572
- throw new Error('High series length mismatch');
2573
- if (low && low.length !== length)
2574
- throw new Error('Low series length mismatch');
2575
- if (close && close.length !== length)
2576
- throw new Error('Close series length mismatch');
2577
- if (open && open.length !== length)
2578
- throw new Error('Open series length mismatch');
2579
- // Initialize result arrays for all 11 levels
2580
- const results = Array.from({ length: 11 }, () => []);
2581
- // Track the last anchor point data
2582
- let lastH = NaN, lastL = NaN, lastC = NaN, lastO = NaN;
2583
- let lastAnchorIndex = -1;
2584
- for (let i = 0; i < length; i++) {
2585
- // Check if anchor triggered
2586
- if (anchor[i]) {
2587
- lastAnchorIndex = i;
2588
- // Store OHLC at anchor point (these will be used for calculations)
2589
- lastH = high ? high[i] : NaN;
2590
- lastL = low ? low[i] : NaN;
2591
- lastC = close ? close[i] : NaN;
2592
- lastO = open ? open[i] : NaN;
2593
- }
2594
- // Calculate data to use
2595
- let h, l, c, o;
2596
- if (isDeveloping && lastAnchorIndex >= 0) {
2597
- // Developing: use max/min/last since anchor
2598
- h = high ? Math.max(...high.slice(lastAnchorIndex, i + 1).filter(v => !isNaN(v))) : NaN;
2599
- l = low ? Math.min(...low.slice(lastAnchorIndex, i + 1).filter(v => !isNaN(v))) : NaN;
2600
- c = close ? close[i] : NaN;
2601
- o = open && lastAnchorIndex >= 0 ? open[lastAnchorIndex] : NaN;
2602
- }
2603
- else {
2604
- // Not developing: use last anchor values
2605
- h = lastH;
2606
- l = lastL;
2607
- c = lastC;
2608
- o = lastO;
2609
- }
2610
- // Calculate pivot levels based on type
2611
- const levels = calculatePivotLevels(typeStr, h, l, c, o);
2612
- // Push to results
2613
- for (let j = 0; j < 11; j++) {
2614
- results[j].push(levels[j]);
2670
+ throw new Error('Error on bar 0: The `developing` parameter of the `ta.pivot_point_levels()` cannot be `true` when `type` is "Woodie".');
2671
+ }
2672
+ const n = anchor.length;
2673
+ for (const [s, name] of [[high, 'High'], [low, 'Low'], [close, 'Close'], [open, 'Open']]) {
2674
+ if (s && s.length !== n)
2675
+ throw new Error(`${name} series length mismatch`);
2676
+ }
2677
+ const at = (s, i) => (s ? (s[i] ?? NaN) : NaN);
2678
+ const results = Array.from({ length: 11 }, () => new Array(n));
2679
+ // the current period (from bar 0 until the first anchor) and the previous one
2680
+ let curH = NaN, curL = NaN, curO = NaN;
2681
+ let prevH = NaN, prevL = NaN, prevC = NaN, prevO = NaN;
2682
+ let anchored = false;
2683
+ for (let i = 0; i < n; i++) {
2684
+ const h = at(high, i), l = at(low, i);
2685
+ if (i === 0) {
2686
+ // the first period starts on bar 0: an anchor there makes bar 0 its own previous period
2687
+ curH = h;
2688
+ curL = l;
2689
+ curO = at(open, i);
2615
2690
  }
2691
+ if (anchor[i]) {
2692
+ prevH = curH;
2693
+ prevL = curL;
2694
+ prevC = i > 0 ? at(close, i - 1) : NaN;
2695
+ prevO = curO;
2696
+ curH = h;
2697
+ curL = l;
2698
+ curO = at(open, i);
2699
+ anchored = true;
2700
+ }
2701
+ else if (i > 0) {
2702
+ if (h > curH || Number.isNaN(curH))
2703
+ curH = h;
2704
+ if (l < curL || Number.isNaN(curL))
2705
+ curL = l;
2706
+ }
2707
+ const levels = isDeveloping
2708
+ ? pivotLevels(typeStr, curH, curL, at(close, i), curO, at(close, i))
2709
+ : anchored
2710
+ ? pivotLevels(typeStr, prevH, prevL, prevC, prevO, curO)
2711
+ : NA_LEVELS;
2712
+ for (let k = 0; k < 11; k++)
2713
+ results[k][i] = levels[k];
2616
2714
  }
2617
2715
  return results;
2618
2716
  }
2717
+ const NA_LEVELS = new Array(11).fill(NaN);
2619
2718
  /**
2620
- * Helper function to calculate pivot levels
2719
+ * The 11 levels of one bar, in PineScript's operation order (bit for bit). `o` is the open of the period (DM),
2720
+ * `wo` the close term of Woodie (the open of the current period, or the bar's close while developing).
2621
2721
  */
2622
- function calculatePivotLevels(type, h, l, c, o) {
2623
- // Return NaN array if data is insufficient
2624
- if (isNaN(h) || isNaN(l) || isNaN(c)) {
2625
- return Array(11).fill(NaN);
2626
- }
2627
- const levels = Array(11).fill(NaN);
2628
- // Calculate pivot point (P)
2629
- let P;
2722
+ function pivotLevels(type, h, l, c, o, wo) {
2723
+ const N = NaN;
2724
+ const r = h - l;
2630
2725
  switch (type) {
2631
- case 'Traditional':
2632
- case 'Fibonacci':
2633
- case 'Classic':
2634
- P = (h + l + c) / 3;
2635
- break;
2636
- case 'Woodie':
2637
- P = (h + l + 2 * c) / 4;
2638
- break;
2639
- case 'DM':
2640
- P = (h + l + c) / 3;
2641
- break;
2642
- case 'Camarilla':
2643
- P = (h + l + c) / 3;
2644
- break;
2645
- default:
2646
- P = (h + l + c) / 3;
2647
- }
2648
- levels[0] = P; // P is at index 0
2649
- // Calculate resistance and support levels based on type
2650
- switch (type) {
2651
- case 'Traditional':
2652
- case 'Classic':
2653
- levels[1] = 2 * P - l; // R1
2654
- levels[2] = 2 * P - h; // S1
2655
- levels[3] = P + (h - l); // R2
2656
- levels[4] = P - (h - l); // S2
2657
- levels[5] = h + 2 * (P - l); // R3
2658
- levels[6] = l - 2 * (h - P); // S3
2659
- levels[7] = levels[5] + (h - l); // R4
2660
- levels[8] = levels[6] - (h - l); // S4
2661
- levels[9] = levels[7] + (h - l); // R5
2662
- levels[10] = levels[8] - (h - l); // S5
2663
- break;
2664
- case 'Fibonacci':
2665
- levels[1] = P + 0.382 * (h - l); // R1
2666
- levels[2] = P - 0.382 * (h - l); // S1
2667
- levels[3] = P + 0.618 * (h - l); // R2
2668
- levels[4] = P - 0.618 * (h - l); // S2
2669
- levels[5] = P + (h - l); // R3
2670
- levels[6] = P - (h - l); // S3
2671
- levels[7] = levels[5] + 0.618 * (h - l); // R4
2672
- levels[8] = levels[6] - 0.618 * (h - l); // S4
2673
- levels[9] = levels[7] + 0.382 * (h - l); // R5
2674
- levels[10] = levels[8] - 0.382 * (h - l); // S5
2675
- break;
2676
- case 'Woodie':
2677
- levels[1] = 2 * P - l; // R1
2678
- levels[2] = 2 * P - h; // S1
2679
- levels[3] = P + (h - l); // R2
2680
- levels[4] = P - (h - l); // S2
2681
- levels[5] = h + 2 * (P - l); // R3
2682
- levels[6] = l - 2 * (h - P); // S3
2683
- levels[7] = levels[5] + (h - l); // R4
2684
- levels[8] = levels[6] - (h - l); // S4
2685
- levels[9] = levels[7] + (h - l); // R5
2686
- levels[10] = levels[8] - (h - l); // S5
2687
- break;
2726
+ case 'Traditional': {
2727
+ const p = (h + l + c) / 3;
2728
+ 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),
2729
+ p * 3 - (3 * h - l), p * 4 + (h - 4 * l), p * 4 - (4 * h - l)];
2730
+ }
2731
+ case 'Fibonacci': {
2732
+ const p = (h + l + c) / 3;
2733
+ 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];
2734
+ }
2735
+ case 'Classic': {
2736
+ const p = (h + l + c) / 3;
2737
+ 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];
2738
+ }
2739
+ case 'Woodie': {
2740
+ const p = (h + l + 2 * wo) / 4;
2741
+ const r3 = h + 2 * (p - l);
2742
+ const s3 = l - 2 * (h - p);
2743
+ return [p, 2 * p - l, 2 * p - h, p + r, p - r, r3, s3, r3 + r, s3 - r, N, N];
2744
+ }
2688
2745
  case 'DM': {
2689
- // DM (Demark) only calculates P, R1, S1
2690
- const x = h + l + (c * 2) + (isNaN(o) ? c : o);
2691
- const newP = x / (isNaN(o) ? 4 : 5);
2692
- levels[0] = newP;
2693
- levels[1] = x / 2 - l; // R1
2694
- levels[2] = x / 2 - h; // S1
2695
- // R2-S5 remain NaN
2696
- break;
2746
+ const x = c < o ? h + 2 * l + c : c > o ? 2 * h + l + c : h + l + 2 * c;
2747
+ return [x / 4, x / 2 - l, x / 2 - h, N, N, N, N, N, N, N, N];
2697
2748
  }
2698
2749
  case 'Camarilla': {
2699
- const range = h - l;
2700
- levels[1] = c + range * 1.1 / 12; // R1
2701
- levels[2] = c - range * 1.1 / 12; // S1
2702
- levels[3] = c + range * 1.1 / 6; // R2
2703
- levels[4] = c - range * 1.1 / 6; // S2
2704
- levels[5] = c + range * 1.1 / 4; // R3
2705
- levels[6] = c - range * 1.1 / 4; // S3
2706
- levels[7] = c + range * 1.1 / 2; // R4
2707
- levels[8] = c - range * 1.1 / 2; // S4
2708
- levels[9] = h; // R5 (high)
2709
- levels[10] = l; // S5 (low)
2710
- break;
2750
+ const p = (h + l + c) / 3;
2751
+ const r5 = (h / l) * c;
2752
+ 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,
2753
+ c - (r * 1.1) / 4, c + (r * 1.1) / 2, c - (r * 1.1) / 2, r5, 2 * c - r5];
2711
2754
  }
2755
+ default:
2756
+ return NA_LEVELS;
2712
2757
  }
2713
- return levels;
2714
2758
  }
2715
2759
  /**
2716
2760
  * Ichimoku Kinko Hyo (Ichimoku Cloud) - Japanese charting technique for trend identification.