oakscriptjs 0.11.0 → 0.11.2

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 (238) 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.js +6 -6
  6. package/dist/cjs/box/index.js.map +2 -2
  7. package/dist/cjs/callsite/index.d.ts.map +1 -1
  8. package/dist/cjs/callsite/index.js +6 -3
  9. package/dist/cjs/callsite/index.js.map +2 -2
  10. package/dist/cjs/drawing/registry.d.ts +15 -6
  11. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  12. package/dist/cjs/drawing/registry.js +16 -2
  13. package/dist/cjs/drawing/registry.js.map +2 -2
  14. package/dist/cjs/index.d.ts +4 -3
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js +7 -1
  17. package/dist/cjs/index.js.map +2 -2
  18. package/dist/cjs/input.d.ts +9 -1
  19. package/dist/cjs/input.d.ts.map +1 -1
  20. package/dist/cjs/input.js +16 -0
  21. package/dist/cjs/input.js.map +2 -2
  22. package/dist/cjs/label/index.js +3 -3
  23. package/dist/cjs/label/index.js.map +2 -2
  24. package/dist/cjs/line/index.d.ts +10 -27
  25. package/dist/cjs/line/index.d.ts.map +1 -1
  26. package/dist/cjs/line/index.js +10 -27
  27. package/dist/cjs/line/index.js.map +2 -2
  28. package/dist/cjs/linefill/index.d.ts +3 -0
  29. package/dist/cjs/linefill/index.d.ts.map +1 -1
  30. package/dist/cjs/linefill/index.js +15 -1
  31. package/dist/cjs/linefill/index.js.map +3 -3
  32. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js +1 -1
  33. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js.map +2 -2
  34. package/dist/cjs/runtime/index.d.ts +1 -1
  35. package/dist/cjs/runtime/index.d.ts.map +1 -1
  36. package/dist/cjs/runtime/index.js +1 -0
  37. package/dist/cjs/runtime/index.js.map +2 -2
  38. package/dist/cjs/runtime/inputs.d.ts +9 -0
  39. package/dist/cjs/runtime/inputs.d.ts.map +1 -1
  40. package/dist/cjs/runtime/inputs.js +8 -0
  41. package/dist/cjs/runtime/inputs.js.map +2 -2
  42. package/dist/cjs/runtime/types.d.ts +1 -1
  43. package/dist/cjs/runtime/types.d.ts.map +1 -1
  44. package/dist/cjs/runtime/types.js.map +1 -1
  45. package/dist/cjs/script/index.d.ts +66 -13
  46. package/dist/cjs/script/index.d.ts.map +1 -1
  47. package/dist/cjs/script/index.js +139 -35
  48. package/dist/cjs/script/index.js.map +3 -3
  49. package/dist/cjs/security/other.d.ts +92 -0
  50. package/dist/cjs/security/other.d.ts.map +1 -0
  51. package/dist/cjs/security/other.js +164 -0
  52. package/dist/cjs/security/other.js.map +7 -0
  53. package/dist/cjs/session/bars.d.ts +10 -1
  54. package/dist/cjs/session/bars.d.ts.map +1 -1
  55. package/dist/cjs/session/bars.js +16 -2
  56. package/dist/cjs/session/bars.js.map +2 -2
  57. package/dist/cjs/session/calendar.d.ts +4 -1
  58. package/dist/cjs/session/calendar.d.ts.map +1 -1
  59. package/dist/cjs/session/calendar.js +12 -7
  60. package/dist/cjs/session/calendar.js.map +2 -2
  61. package/dist/cjs/session/context.d.ts +22 -5
  62. package/dist/cjs/session/context.d.ts.map +1 -1
  63. package/dist/cjs/session/context.js +67 -6
  64. package/dist/cjs/session/context.js.map +2 -2
  65. package/dist/cjs/str/floatingdecimal.d.ts +31 -0
  66. package/dist/cjs/str/floatingdecimal.d.ts.map +1 -0
  67. package/dist/cjs/str/floatingdecimal.js +298 -0
  68. package/dist/cjs/str/floatingdecimal.js.map +7 -0
  69. package/dist/cjs/str/index.d.ts +4 -3
  70. package/dist/cjs/str/index.d.ts.map +1 -1
  71. package/dist/cjs/str/index.js +1 -1
  72. package/dist/cjs/str/index.js.map +2 -2
  73. package/dist/cjs/str/numberformat.d.ts +6 -4
  74. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  75. package/dist/cjs/str/numberformat.js +3 -2
  76. package/dist/cjs/str/numberformat.js.map +2 -2
  77. package/dist/cjs/ta/index.d.ts +64 -45
  78. package/dist/cjs/ta/index.d.ts.map +1 -1
  79. package/dist/cjs/ta/index.js +140 -126
  80. package/dist/cjs/ta/index.js.map +3 -3
  81. package/dist/cjs/ta/limits.d.ts +11 -0
  82. package/dist/cjs/ta/limits.d.ts.map +1 -1
  83. package/dist/cjs/ta/limits.js +15 -1
  84. package/dist/cjs/ta/limits.js.map +2 -2
  85. package/dist/cjs/ta/running-sum.js +1 -1
  86. package/dist/cjs/ta/running-sum.js.map +2 -2
  87. package/dist/cjs/ta-series.d.ts +1 -1
  88. package/dist/cjs/ta-series.d.ts.map +1 -1
  89. package/dist/cjs/ta-series.js +1 -1
  90. package/dist/cjs/ta-series.js.map +2 -2
  91. package/dist/cjs/table/index.d.ts +10 -3
  92. package/dist/cjs/table/index.d.ts.map +1 -1
  93. package/dist/cjs/table/index.js +11 -1
  94. package/dist/cjs/table/index.js.map +3 -3
  95. package/dist/cjs/time/session.d.ts +9 -0
  96. package/dist/cjs/time/session.d.ts.map +1 -1
  97. package/dist/cjs/time/session.js +42 -14
  98. package/dist/cjs/time/session.js.map +3 -3
  99. package/dist/cjs/time/timezone.d.ts +6 -1
  100. package/dist/cjs/time/timezone.d.ts.map +1 -1
  101. package/dist/cjs/time/timezone.js +2 -2
  102. package/dist/cjs/time/timezone.js.map +2 -2
  103. package/dist/cjs/types/metadata.d.ts +1 -1
  104. package/dist/cjs/types/metadata.d.ts.map +1 -1
  105. package/dist/cjs/types/metadata.js.map +1 -1
  106. package/dist/esm/array/index.d.ts +8 -2
  107. package/dist/esm/array/index.d.ts.map +1 -1
  108. package/dist/esm/array/index.js +12 -3
  109. package/dist/esm/array/index.js.map +1 -1
  110. package/dist/esm/box/index.js +6 -6
  111. package/dist/esm/box/index.js.map +1 -1
  112. package/dist/esm/callsite/index.d.ts.map +1 -1
  113. package/dist/esm/callsite/index.js +6 -3
  114. package/dist/esm/callsite/index.js.map +1 -1
  115. package/dist/esm/drawing/registry.d.ts +15 -6
  116. package/dist/esm/drawing/registry.d.ts.map +1 -1
  117. package/dist/esm/drawing/registry.js +30 -7
  118. package/dist/esm/drawing/registry.js.map +1 -1
  119. package/dist/esm/index.d.ts +4 -3
  120. package/dist/esm/index.d.ts.map +1 -1
  121. package/dist/esm/index.js +8 -5
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/input.d.ts +9 -1
  124. package/dist/esm/input.d.ts.map +1 -1
  125. package/dist/esm/input.js +16 -0
  126. package/dist/esm/input.js.map +1 -1
  127. package/dist/esm/label/index.js +3 -3
  128. package/dist/esm/label/index.js.map +1 -1
  129. package/dist/esm/line/index.d.ts +10 -27
  130. package/dist/esm/line/index.d.ts.map +1 -1
  131. package/dist/esm/line/index.js +20 -64
  132. package/dist/esm/line/index.js.map +1 -1
  133. package/dist/esm/linefill/index.d.ts +3 -0
  134. package/dist/esm/linefill/index.d.ts.map +1 -1
  135. package/dist/esm/linefill/index.js +20 -1
  136. package/dist/esm/linefill/index.js.map +1 -1
  137. package/dist/esm/runtime/adapters/SimpleInputAdapter.js +1 -1
  138. package/dist/esm/runtime/adapters/SimpleInputAdapter.js.map +1 -1
  139. package/dist/esm/runtime/index.d.ts +1 -1
  140. package/dist/esm/runtime/index.d.ts.map +1 -1
  141. package/dist/esm/runtime/index.js +1 -1
  142. package/dist/esm/runtime/index.js.map +1 -1
  143. package/dist/esm/runtime/inputs.d.ts +9 -0
  144. package/dist/esm/runtime/inputs.d.ts.map +1 -1
  145. package/dist/esm/runtime/inputs.js +15 -0
  146. package/dist/esm/runtime/inputs.js.map +1 -1
  147. package/dist/esm/runtime/types.d.ts +1 -1
  148. package/dist/esm/runtime/types.d.ts.map +1 -1
  149. package/dist/esm/script/index.d.ts +66 -13
  150. package/dist/esm/script/index.d.ts.map +1 -1
  151. package/dist/esm/script/index.js +187 -54
  152. package/dist/esm/script/index.js.map +1 -1
  153. package/dist/esm/security/other.d.ts +92 -0
  154. package/dist/esm/security/other.d.ts.map +1 -0
  155. package/dist/esm/security/other.js +211 -0
  156. package/dist/esm/security/other.js.map +1 -0
  157. package/dist/esm/session/bars.d.ts +10 -1
  158. package/dist/esm/session/bars.d.ts.map +1 -1
  159. package/dist/esm/session/bars.js +26 -1
  160. package/dist/esm/session/bars.js.map +1 -1
  161. package/dist/esm/session/calendar.d.ts +4 -1
  162. package/dist/esm/session/calendar.d.ts.map +1 -1
  163. package/dist/esm/session/calendar.js +23 -7
  164. package/dist/esm/session/calendar.js.map +1 -1
  165. package/dist/esm/session/context.d.ts +22 -5
  166. package/dist/esm/session/context.d.ts.map +1 -1
  167. package/dist/esm/session/context.js +86 -10
  168. package/dist/esm/session/context.js.map +1 -1
  169. package/dist/esm/str/floatingdecimal.d.ts +31 -0
  170. package/dist/esm/str/floatingdecimal.d.ts.map +1 -0
  171. package/dist/esm/str/floatingdecimal.js +272 -0
  172. package/dist/esm/str/floatingdecimal.js.map +1 -0
  173. package/dist/esm/str/index.d.ts +4 -3
  174. package/dist/esm/str/index.d.ts.map +1 -1
  175. package/dist/esm/str/index.js +6 -5
  176. package/dist/esm/str/index.js.map +1 -1
  177. package/dist/esm/str/numberformat.d.ts +6 -4
  178. package/dist/esm/str/numberformat.d.ts.map +1 -1
  179. package/dist/esm/str/numberformat.js +9 -5
  180. package/dist/esm/str/numberformat.js.map +1 -1
  181. package/dist/esm/ta/index.d.ts +64 -45
  182. package/dist/esm/ta/index.d.ts.map +1 -1
  183. package/dist/esm/ta/index.js +198 -228
  184. package/dist/esm/ta/index.js.map +1 -1
  185. package/dist/esm/ta/limits.d.ts +11 -0
  186. package/dist/esm/ta/limits.d.ts.map +1 -1
  187. package/dist/esm/ta/limits.js +27 -2
  188. package/dist/esm/ta/limits.js.map +1 -1
  189. package/dist/esm/ta/running-sum.js +1 -1
  190. package/dist/esm/ta/running-sum.js.map +1 -1
  191. package/dist/esm/ta-series.d.ts +1 -1
  192. package/dist/esm/ta-series.d.ts.map +1 -1
  193. package/dist/esm/ta-series.js +2 -1
  194. package/dist/esm/ta-series.js.map +1 -1
  195. package/dist/esm/table/index.d.ts +10 -3
  196. package/dist/esm/table/index.d.ts.map +1 -1
  197. package/dist/esm/table/index.js +23 -3
  198. package/dist/esm/table/index.js.map +1 -1
  199. package/dist/esm/time/session.d.ts +9 -0
  200. package/dist/esm/time/session.d.ts.map +1 -1
  201. package/dist/esm/time/session.js +53 -15
  202. package/dist/esm/time/session.js.map +1 -1
  203. package/dist/esm/time/timezone.d.ts +6 -1
  204. package/dist/esm/time/timezone.d.ts.map +1 -1
  205. package/dist/esm/time/timezone.js +8 -2
  206. package/dist/esm/time/timezone.js.map +1 -1
  207. package/dist/esm/types/metadata.d.ts +1 -1
  208. package/dist/esm/types/metadata.d.ts.map +1 -1
  209. package/package.json +1 -1
  210. package/src/array/index.ts +11 -3
  211. package/src/box/index.ts +6 -6
  212. package/src/callsite/index.ts +6 -3
  213. package/src/drawing/registry.ts +30 -7
  214. package/src/index.ts +16 -3
  215. package/src/input.ts +18 -1
  216. package/src/label/index.ts +3 -3
  217. package/src/line/index.ts +20 -68
  218. package/src/linefill/index.ts +20 -1
  219. package/src/runtime/adapters/SimpleInputAdapter.ts +1 -1
  220. package/src/runtime/index.ts +1 -0
  221. package/src/runtime/inputs.ts +16 -0
  222. package/src/runtime/types.ts +1 -1
  223. package/src/script/index.ts +262 -58
  224. package/src/security/other.ts +269 -0
  225. package/src/session/bars.ts +24 -1
  226. package/src/session/calendar.ts +26 -7
  227. package/src/session/context.ts +104 -10
  228. package/src/str/floatingdecimal.ts +268 -0
  229. package/src/str/index.ts +6 -5
  230. package/src/str/numberformat.ts +11 -6
  231. package/src/ta/index.ts +200 -225
  232. package/src/ta/limits.ts +28 -1
  233. package/src/ta/running-sum.ts +1 -1
  234. package/src/ta-series.ts +3 -2
  235. package/src/table/index.ts +22 -3
  236. package/src/time/session.ts +58 -16
  237. package/src/time/timezone.ts +9 -2
  238. package/src/types/metadata.ts +1 -1
package/src/ta/index.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  import type { series_float, series_bool, series_int, int, Source, simple_int, simple_float, simple_bool } from '../types/index.js';
12
12
  import { eq, ge, gt, le, lt } from '../compare/index.js';
13
13
  import { runningSum, runningSumSeries, runningVariance, runningVarianceSeries } from './running-sum.js';
14
- import { checkSumLength, checkWindowLength } from './limits.js';
14
+ import { checkPositiveLength, checkSumLength, checkWindowLength } from './limits.js';
15
15
 
16
16
  /**
17
17
  * Simple Moving Average - returns the moving average (sum of last y values divided by y).
@@ -68,6 +68,7 @@ export function sma(source: Source, length: simple_int | ArrayLike<number>): ser
68
68
  * ```
69
69
  */
70
70
  export function ema(source: Source, length: simple_int): series_float {
71
+ checkPositiveLength('ema', length, 0);
71
72
  const result: series_float = [];
72
73
  const len = Math.floor(length);
73
74
  const multiplier = 2 / (len + 1);
@@ -133,7 +134,25 @@ export function ema(source: Source, length: simple_int): series_float {
133
134
  * const oversold = rsi14.map(v => v < 30);
134
135
  * ```
135
136
  */
136
- 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)`: -Infinity when `x == -y` (#182), which `na()` and `nz()` treat as na while comparisons
142
+ * see it (`r < 30` is true). Versions 5 and 6 have the length form only.
143
+ */
144
+ export function rsi(x: Source, y: Source): series_float;
145
+ export function rsi(source: Source, length: simple_int | Source): series_float {
146
+ if (typeof length !== 'number') {
147
+ return Array.from(source, (xv, i) => {
148
+ const x = xv ?? NaN;
149
+ const y = length[i] ?? NaN;
150
+ if (eq(y, 0)) return 100;
151
+ if (eq(x, 0)) return 0;
152
+ return 100 - 100 / (1 + x / y);
153
+ });
154
+ }
155
+ checkPositiveLength('rsi', length, 0, 'y');
137
156
  const result: series_float = [];
138
157
 
139
158
  // Calculate price changes
@@ -274,14 +293,14 @@ export function stdev(
274
293
  }
275
294
 
276
295
  /**
277
- * The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist.
278
- * `isNa` tells which values are skipped (NaN by default).
296
+ * The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist. +/-Infinity
297
+ * counts as na (#183).
279
298
  */
280
299
  function lastValues(source: Source, i: number, length: number): number[] | null {
281
300
  const values: number[] = [];
282
301
  for (let j = i; j >= 0 && values.length < length; j--) {
283
302
  const x = source[j]!;
284
- if (!Number.isNaN(x)) values.push(x);
303
+ if (Number.isFinite(x)) values.push(x);
285
304
  }
286
305
  return values.length === length ? values : null;
287
306
  }
@@ -342,7 +361,7 @@ function windowExtremes(
342
361
  /**
343
362
  * The non-na values of `source` with their bars, and for each bar the position of its last non-na value (-1 when
344
363
  * there is none yet). PineScript's ta.highest / ta.lowest / ta.highestbars / ta.lowestbars / ta.pivothigh /
345
- * ta.pivotlow keep the non-na values they receive and nothing else.
364
+ * ta.pivotlow keep the non-na values they receive and nothing else; +/-Infinity is skipped like na (#183).
346
365
  */
347
366
  function keptValues(source: Source): { values: number[]; bars: number[]; last: Int32Array } {
348
367
  const values: number[] = [];
@@ -350,7 +369,7 @@ function keptValues(source: Source): { values: number[]; bars: number[]; last: I
350
369
  const last = new Int32Array(source.length);
351
370
  for (let i = 0; i < source.length; i++) {
352
371
  const x = source[i]!;
353
- if (!Number.isNaN(x)) {
372
+ if (Number.isFinite(x)) {
354
373
  values.push(x);
355
374
  bars.push(i);
356
375
  }
@@ -506,6 +525,7 @@ export function crossunder(series1: Source, series2: Source): series_bool {
506
525
  * ```
507
526
  */
508
527
  export function change(source: Source, length: simple_int = 1): series_float {
528
+ checkPositiveLength('change', length, 0);
509
529
  const result: series_float = [];
510
530
 
511
531
  for (let i = 0; i < source.length; i++) {
@@ -618,6 +638,7 @@ export function tr(
618
638
  * ```
619
639
  */
620
640
  export function atr(length: simple_int, high?: Source, low?: Source, close?: Source): series_float {
641
+ checkPositiveLength('atr->rma', length, 0);
621
642
  if (!high || !low || !close) {
622
643
  throw new Error(
623
644
  'ta.atr() requires high, low, and close series. ' +
@@ -763,6 +784,7 @@ export function supertrend(
763
784
  * ```
764
785
  */
765
786
  export function rma(source: Source, length: simple_int): series_float {
787
+ checkPositiveLength('rma', length, 0);
766
788
  const result: series_float = [];
767
789
  // Floor the length to match PineScript's auto-truncation of float to int
768
790
  const len = Math.floor(length);
@@ -846,6 +868,7 @@ export function wma(source: Source, length: simple_int | ArrayLike<number>): ser
846
868
  const result: series_float = [];
847
869
  // Floor the length to match PineScript's auto-truncation of float to int
848
870
  const fixed = typeof length === 'number' ? Math.floor(length) : NaN;
871
+ if (typeof length === 'number') checkPositiveLength('wma', fixed, 0);
849
872
  // PineScript: na on a bar with an na source (also +/-Infinity) and until `len` non-na values were received;
850
873
  // otherwise the last `len` bars, each na replaced by the previous non-na value
851
874
  const filled: number[] = [];
@@ -861,6 +884,7 @@ export function wma(source: Source, length: simple_int | ArrayLike<number>): ser
861
884
  filled.push(last);
862
885
  // a series length: the window of this bar's length (the result of ta.wma with that fixed length)
863
886
  const len = typeof length === 'number' ? fixed : Math.floor(length[i]!);
887
+ if (typeof length !== 'number') checkPositiveLength('wma', len, i);
864
888
  if (isNa || !(len >= 1) || count < len) {
865
889
  result.push(NaN);
866
890
  continue;
@@ -886,9 +910,9 @@ export function wma(source: Source, length: simple_int | ArrayLike<number>): ser
886
910
  * @returns Highest value series (na on the first `length - 1` bars)
887
911
  *
888
912
  * @remarks
889
- * As in PineScript: `na` values are skipped. The result is the highest of the last `length` non-na values (of all
890
- * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
891
- * value was received. A fractional length is truncated (7.5 is 7 bars).
913
+ * As in PineScript: `na` values (also +/-Infinity, #183) are skipped. The result is the highest of the last `length`
914
+ * non-na values (of all of them when there are fewer), also on a bar whose own value is `na`; `na` before bar
915
+ * `length - 1` and while no value was received. A fractional length is truncated (7.5 is 7 bars).
892
916
  * `length` can be an array (a length per bar, PineScript series length), also for `ta.lowest`, `ta.highestbars`
893
917
  * and `ta.lowestbars`: each bar uses its own length.
894
918
  * A length above 15,000 throws the PineScript runtime error (also `ta.lowest`, `ta.highestbars`, `ta.lowestbars`).
@@ -906,9 +930,9 @@ export function highest(source: Source, length: simple_int | ArrayLike<number>):
906
930
  * @returns Lowest value series (na on the first `length - 1` bars)
907
931
  *
908
932
  * @remarks
909
- * As in PineScript: `na` values are skipped. The result is the lowest of the last `length` non-na values (of all
910
- * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
911
- * value was received.
933
+ * As in PineScript: `na` values (also +/-Infinity, #183) are skipped. The result is the lowest of the last `length`
934
+ * non-na values (of all of them when there are fewer), also on a bar whose own value is `na`; `na` before bar
935
+ * `length - 1` and while no value was received.
912
936
  */
913
937
  export function lowest(source: Source, length: simple_int | ArrayLike<number>): series_float {
914
938
  return extremeValue(source, length, true);
@@ -994,7 +1018,7 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
994
1018
  for (let i = 0; i < source.length; i++) {
995
1019
  const v = source[i]!;
996
1020
  const prev = i > 0 ? source[i - 1]! : NaN;
997
- if (!Number.isNaN(v) && !Number.isNaN(prev)) count = step(v, prev) ? count + 1 : 0;
1021
+ if (Number.isFinite(v) && Number.isFinite(prev)) count = step(v, prev) ? count + 1 : 0;
998
1022
  result.push(count >= length);
999
1023
  }
1000
1024
  return result;
@@ -1011,8 +1035,8 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
1011
1035
  * PineScript rules:
1012
1036
  * - a counter of rises from the previous bar (a rise is larger than the 1e-10 tolerance); a step that is not a rise
1013
1037
  * resets it to 0
1014
- * - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
1015
- * result of the bar before the `na`)
1038
+ * - a step from or to an `na` (or +/-Infinity) bar leaves the counter unchanged (so an `na` bar, and the bar after
1039
+ * it, keep the result of the bar before the `na`)
1016
1040
  * - true when the counter is at least `length` (false at the start)
1017
1041
  *
1018
1042
  * @example
@@ -1022,6 +1046,7 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
1022
1046
  * ```
1023
1047
  */
1024
1048
  export function rising(source: Source, length: simple_int): series_bool {
1049
+ checkPositiveLength('rising', length, 0);
1025
1050
  return monotonic(source, length, gt);
1026
1051
  }
1027
1052
 
@@ -1036,8 +1061,8 @@ export function rising(source: Source, length: simple_int): series_bool {
1036
1061
  * PineScript rules:
1037
1062
  * - a counter of falls from the previous bar (a fall is larger than the 1e-10 tolerance); a step that is not a fall
1038
1063
  * resets it to 0
1039
- * - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
1040
- * result of the bar before the `na`)
1064
+ * - a step from or to an `na` (or +/-Infinity) bar leaves the counter unchanged (so an `na` bar, and the bar after
1065
+ * it, keep the result of the bar before the `na`)
1041
1066
  * - true when the counter is at least `length` (false at the start)
1042
1067
  *
1043
1068
  * @example
@@ -1047,6 +1072,7 @@ export function rising(source: Source, length: simple_int): series_bool {
1047
1072
  * ```
1048
1073
  */
1049
1074
  export function falling(source: Source, length: simple_int): series_bool {
1075
+ checkPositiveLength('falling', length, 0);
1050
1076
  return monotonic(source, length, lt);
1051
1077
  }
1052
1078
 
@@ -1072,6 +1098,7 @@ export function falling(source: Source, length: simple_int): series_bool {
1072
1098
  * ```
1073
1099
  */
1074
1100
  export function roc(source: Source, length: simple_int): series_float {
1101
+ checkPositiveLength('roc', length, 0);
1075
1102
  const result: series_float = [];
1076
1103
 
1077
1104
  for (let i = 0; i < source.length; i++) {
@@ -1112,6 +1139,7 @@ export function roc(source: Source, length: simple_int): series_float {
1112
1139
  * ```
1113
1140
  */
1114
1141
  export function mom(source: Source, length: simple_int): series_float {
1142
+ checkPositiveLength('mom', length, 0);
1115
1143
  const result: series_float = [];
1116
1144
 
1117
1145
  for (let i = 0; i < source.length; i++) {
@@ -1151,6 +1179,7 @@ export function mom(source: Source, length: simple_int): series_float {
1151
1179
  * ```
1152
1180
  */
1153
1181
  export function dev(source: Source, length: simple_int): series_float {
1182
+ checkPositiveLength('dev', length, 0);
1154
1183
  const len = Math.floor(length);
1155
1184
  return sma(source, len).map((mean, i) => {
1156
1185
  if (i < len - 1 || Number.isNaN(mean)) return NaN;
@@ -1222,7 +1251,7 @@ function varianceOf(
1222
1251
  * @remarks
1223
1252
  * - Returns the middle value when values are sorted
1224
1253
  * - For an even length, returns the average of the two middle values
1225
- * - As in PineScript: `na` values are skipped, the window holds the last `length` non-na values
1254
+ * - As in PineScript: `na` values (also +/-Infinity) are skipped, the window holds the last `length` non-na values
1226
1255
  * (going back as many bars as needed), and a bar whose own value is `na` still gets a result
1227
1256
  * - Returns NaN until `length` non-na values exist
1228
1257
  * - More robust to outliers than mean (SMA)
@@ -1234,6 +1263,7 @@ function varianceOf(
1234
1263
  * ```
1235
1264
  */
1236
1265
  export function median(source: Source, length: simple_int): series_float {
1266
+ checkPositiveLength('median', length, 0);
1237
1267
  const len = Math.floor(length);
1238
1268
  const n = source.length;
1239
1269
  const out: series_float = new Array<number>(n).fill(NaN);
@@ -1257,7 +1287,7 @@ export function median(source: Source, length: simple_int): series_float {
1257
1287
  const mid = Math.floor(len / 2);
1258
1288
  for (let i = 0; i < n; i++) {
1259
1289
  const x = source[i]!;
1260
- if (!Number.isNaN(x)) {
1290
+ if (Number.isFinite(x)) {
1261
1291
  kept[count++] = x;
1262
1292
  sorted.splice(bound(x, false), 0, x); // before the equal values: the newest first
1263
1293
  // the value leaving the window is the oldest, so the last of its equal values
@@ -1321,6 +1351,7 @@ export function swma(source: Source): series_float {
1321
1351
  * ```
1322
1352
  */
1323
1353
  export function vwma(source: Source, length: simple_int, volume?: Source): series_float {
1354
+ checkPositiveLength('vwma->sma', length, 0);
1324
1355
  if (!volume) {
1325
1356
  throw new Error(
1326
1357
  'ta.vwma() requires volume series. ' +
@@ -1374,6 +1405,7 @@ export function vwma(source: Source, length: simple_int, volume?: Source): serie
1374
1405
  * ```
1375
1406
  */
1376
1407
  export function linreg(source: Source, length: simple_int, offset: simple_int = 0): series_float {
1408
+ checkPositiveLength('linreg', length, 0);
1377
1409
  const result: series_float = [];
1378
1410
  // Floor the length to match PineScript's auto-truncation of float to int
1379
1411
  const len = Math.floor(length);
@@ -1443,6 +1475,7 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
1443
1475
  * ```
1444
1476
  */
1445
1477
  export function correlation(source1: Source, source2: Source, length: simple_int): series_float {
1478
+ checkPositiveLength('correlation', length, 0);
1446
1479
  // PineScript: from compensated running sums, cov = sma(x * y) - sma(x) * sma(y), r = cov / sqrt(var(x) * var(y))
1447
1480
  // (biased variances); each running sum skips its own na values
1448
1481
  const product = Array.from({ length: source1.length }, (_, i) => {
@@ -1481,8 +1514,8 @@ export function correlation(source1: Source, source2: Source, length: simple_int
1481
1514
  *
1482
1515
  * PineScript rules:
1483
1516
  * - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
1484
- * - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
1485
- * `<=` 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)
1486
1519
  * - the comparison uses the 1e-10 tolerance
1487
1520
  *
1488
1521
  * @example
@@ -1493,15 +1526,16 @@ export function correlation(source1: Source, source2: Source, length: simple_int
1493
1526
  * ```
1494
1527
  */
1495
1528
  export function percentrank(source: Source, length: simple_int): series_float {
1529
+ checkPositiveLength('percentrank', length, 0);
1496
1530
  const result: series_float = [];
1497
1531
 
1498
1532
  for (let i = 0; i < source.length; i++) {
1499
1533
  const currentValue = source[i]!;
1500
- if (i < length || Number.isNaN(currentValue)) {
1534
+ if (i < length) {
1501
1535
  result.push(NaN);
1502
1536
  continue;
1503
1537
  }
1504
- // 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)
1505
1539
  let countLessOrEqual = 0;
1506
1540
  for (let j = 1; j <= length; j++) {
1507
1541
  if (le(source[i - j]!, currentValue)) countLessOrEqual++;
@@ -1541,6 +1575,7 @@ export function percentrank(source: Source, length: simple_int): series_float {
1541
1575
  * \`\`\`
1542
1576
  */
1543
1577
  export function cci(source: Source, length: simple_int): series_float {
1578
+ checkPositiveLength('cci', length, 0);
1544
1579
  const smaValues = sma(source, length);
1545
1580
  const devValues = dev(source, length);
1546
1581
  let last = 0;
@@ -1566,8 +1601,9 @@ export function cci(source: Source, length: simple_int): series_float {
1566
1601
  * - Measures where close is relative to the high-low range
1567
1602
  * - Values above 80 typically indicate overbought
1568
1603
  * - Values below 20 typically indicate oversold
1569
- * - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range is 0, the
1570
- * result is the previous result of the call (\`na\` until a first value)
1604
+ * - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range and
1605
+ * \`source - lowest\` are both 0, the result is the previous result of the call (\`na\` until a first value); a zero
1606
+ * range with another source value gives +/-Infinity (plotted as \`na\`)
1571
1607
  *
1572
1608
  * @example
1573
1609
  * \`\`\`typescript
@@ -1577,17 +1613,18 @@ export function cci(source: Source, length: simple_int): series_float {
1577
1613
  * \`\`\`
1578
1614
  */
1579
1615
  export function stoch(source: Source, high: Source, low: Source, length: simple_int): series_float {
1616
+ checkPositiveLength('stoch->highest', length, 0);
1580
1617
  const result: series_float = [];
1581
1618
  const lowestValues = lowest(low, length);
1582
1619
  const highestValues = highest(high, length);
1583
1620
 
1584
- // PineScript: an na input or a zero range keeps the previous result
1621
+ // PineScript: an na (NaN) result keeps the previous one: an na input, or 0 / 0 on a zero range; a non-zero value
1622
+ // over a zero range gives +/-Infinity (#183)
1585
1623
  let previous = NaN;
1586
1624
  for (let i = 0; i < source.length; i++) {
1587
- const value = source[i]!;
1588
1625
  const lo = lowestValues[i]!;
1589
- const range = highestValues[i]! - lo;
1590
- if (!Number.isNaN(value) && !Number.isNaN(range) && range !== 0) previous = (100 * (value - lo)) / range;
1626
+ const k = (100 * (source[i]! - lo)) / (highestValues[i]! - lo);
1627
+ if (!Number.isNaN(k)) previous = k;
1591
1628
  result.push(previous);
1592
1629
  }
1593
1630
 
@@ -1629,6 +1666,7 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
1629
1666
  * ```
1630
1667
  */
1631
1668
  export function mfi(source: Source, length: simple_int, volume?: Source): series_float {
1669
+ checkPositiveLength('mfi', length, 0);
1632
1670
  if (!volume) {
1633
1671
  throw new Error(
1634
1672
  'ta.mfi() requires volume series. ' +
@@ -1687,6 +1725,7 @@ export function mfi(source: Source, length: simple_int, volume?: Source): series
1687
1725
  * ```
1688
1726
  */
1689
1727
  export function hma(source: Source, length: simple_int): series_float {
1728
+ checkPositiveLength('hma', length, 0);
1690
1729
  const halfLength = Math.floor(length / 2);
1691
1730
  const sqrtLength = Math.floor(Math.sqrt(length));
1692
1731
 
@@ -1860,8 +1899,8 @@ function pivot(source: Source, leftbars: PivotLength, rightbars: PivotLength, is
1860
1899
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1861
1900
  * index is `i - rightbars`
1862
1901
  * - A pivot high may equal values on its left but must be higher than all values on its right
1863
- * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the highest of
1864
- * them must be on the bar `rightbars` bars back
1902
+ * - `na` values (also +/-Infinity) are skipped: the window is the last `leftbars + rightbars + 1` non-na values,
1903
+ * and the highest of them must be on the bar `rightbars` bars back
1865
1904
  * - `leftbars` / `rightbars` can be arrays (a length per bar, PineScript series length), also for `ta.pivotlow`:
1866
1905
  * each bar uses its own lengths
1867
1906
  *
@@ -1900,8 +1939,8 @@ export function pivothigh(
1900
1939
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1901
1940
  * index is `i - rightbars`
1902
1941
  * - A pivot low may equal values on its left but must be lower than all values on its right
1903
- * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the lowest of
1904
- * them must be on the bar `rightbars` bars back
1942
+ * - `na` values (also +/-Infinity) are skipped: the window is the last `leftbars + rightbars + 1` non-na values,
1943
+ * and the lowest of them must be on the bar `rightbars` bars back
1905
1944
  *
1906
1945
  * @example
1907
1946
  * ```typescript
@@ -2093,6 +2132,7 @@ export function dmi(
2093
2132
  * ```
2094
2133
  */
2095
2134
  export function tsi(source: Source, shortLength: simple_int, longLength: simple_int): series_float {
2135
+ checkPositiveLength('tsi->ema', shortLength, 0);
2096
2136
  const momentum: series_float = [];
2097
2137
 
2098
2138
  // Calculate momentum (price change)
@@ -2142,6 +2182,7 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
2142
2182
  * ```
2143
2183
  */
2144
2184
  export function cmo(source: Source, length: simple_int): series_float {
2185
+ checkPositiveLength('cmo', length, 0);
2145
2186
  // PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
2146
2187
  // sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
2147
2188
  // math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
@@ -2191,6 +2232,7 @@ export function kc(
2191
2232
  low?: Source,
2192
2233
  close?: Source
2193
2234
  ): [series_float, series_float, series_float] {
2235
+ checkPositiveLength('kc', length, 0);
2194
2236
  // Calculate middle band (EMA of source)
2195
2237
  const middle = ema(source, length);
2196
2238
 
@@ -2250,6 +2292,7 @@ export function kc(
2250
2292
  * ```
2251
2293
  */
2252
2294
  export function bbw(source: Source, length: simple_int, mult: simple_float): series_float {
2295
+ checkPositiveLength('bbw', length, 0);
2253
2296
  const [basis, upper, lower] = bb(source, length, mult);
2254
2297
  const result: series_float = [];
2255
2298
 
@@ -2293,6 +2336,7 @@ export function bbw(source: Source, length: simple_int, mult: simple_float): ser
2293
2336
  * ```
2294
2337
  */
2295
2338
  export function wpr(high: Source, low: Source, close: Source, length: simple_int = 14): series_float {
2339
+ checkPositiveLength('wpr', length, 0);
2296
2340
  const highs = highest(high, length);
2297
2341
  const lows = lowest(low, length);
2298
2342
  return Array.from({ length: close.length }, (_, i) => {
@@ -2393,6 +2437,7 @@ export function alma(
2393
2437
  sigma: simple_float = 6,
2394
2438
  floor: boolean = false
2395
2439
  ): series_float {
2440
+ checkPositiveLength('alma', length, 0);
2396
2441
  const result: series_float = [];
2397
2442
  const m = floor ? Math.floor(offset * (length - 1)) : offset * (length - 1);
2398
2443
  const s = length / sigma;
@@ -2485,10 +2530,11 @@ export function kcw(
2485
2530
  * (PineScript `ta.range(source, length)`).
2486
2531
  *
2487
2532
  * @remarks
2488
- * As in PineScript: `na` values are skipped (the window holds the last `length` non-na
2533
+ * As in PineScript: `na` values (also +/-Infinity) are skipped (the window holds the last `length` non-na
2489
2534
  * values), and a bar whose own value is `na` still gets a result.
2490
2535
  */
2491
2536
  export function range(source: Source, length: simple_int): series_float {
2537
+ checkPositiveLength('range', length, 0);
2492
2538
  return source.map((_, i) => {
2493
2539
  const values = lastValues(source, i, length);
2494
2540
  return values ? Math.max(...values) - Math.min(...values) : NaN;
@@ -2500,9 +2546,9 @@ export function range(source: Source, length: simple_int): series_float {
2500
2546
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2501
2547
  *
2502
2548
  * @remarks
2503
- * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2504
- * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2505
- * `length - 1` and while no value was received; a fractional length is truncated.
2549
+ * As in PineScript: the best of the last `length` non-na values (+/-Infinity skipped), the newest one on equal
2550
+ * values; the offset is the distance in bars to its bar, so it is below `-(length - 1)` when `na` values were
2551
+ * skipped. `na` before bar `length - 1` and while no value was received; a fractional length is truncated.
2506
2552
  */
2507
2553
  export function highestbars(source: Source, length: simple_int | ArrayLike<number>): series_int {
2508
2554
  return extremeOffset(source, length, false);
@@ -2513,9 +2559,9 @@ export function highestbars(source: Source, length: simple_int | ArrayLike<numbe
2513
2559
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2514
2560
  *
2515
2561
  * @remarks
2516
- * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2517
- * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2518
- * `length - 1` and while no value was received; a fractional length is truncated.
2562
+ * As in PineScript: the best of the last `length` non-na values (+/-Infinity skipped), the newest one on equal
2563
+ * values; the offset is the distance in bars to its bar, so it is below `-(length - 1)` when `na` values were
2564
+ * skipped. `na` before bar `length - 1` and while no value was received; a fractional length is truncated.
2519
2565
  */
2520
2566
  export function lowestbars(source: Source, length: simple_int | ArrayLike<number>): series_int {
2521
2567
  return extremeOffset(source, length, true);
@@ -2525,13 +2571,13 @@ export function lowestbars(source: Source, length: simple_int | ArrayLike<number
2525
2571
  * All-time highest value of `source` up to each bar (PineScript `ta.max(source)`).
2526
2572
  *
2527
2573
  * @remarks
2528
- * As in PineScript: `na` values are skipped, the value carries over `na` bars, and the
2574
+ * As in PineScript: `na` values (also +/-Infinity) are skipped, the value carries over `na` bars, and the
2529
2575
  * result is `na` until the first non-na value.
2530
2576
  */
2531
2577
  export function max(source: Source): series_float {
2532
2578
  let best = NaN;
2533
2579
  return source.map((x) => {
2534
- if (!Number.isNaN(x) && !(x <= best)) best = x;
2580
+ if (Number.isFinite(x) && !(x <= best)) best = x;
2535
2581
  return best;
2536
2582
  });
2537
2583
  }
@@ -2540,13 +2586,13 @@ export function max(source: Source): series_float {
2540
2586
  * All-time lowest value of `source` up to each bar (PineScript `ta.min(source)`).
2541
2587
  *
2542
2588
  * @remarks
2543
- * As in PineScript: `na` values are skipped, the value carries over `na` bars, and the
2589
+ * As in PineScript: `na` values (also +/-Infinity) are skipped, the value carries over `na` bars, and the
2544
2590
  * result is `na` until the first non-na value.
2545
2591
  */
2546
2592
  export function min(source: Source): series_float {
2547
2593
  let best = NaN;
2548
2594
  return source.map((x) => {
2549
- if (!Number.isNaN(x) && !(x >= best)) best = x;
2595
+ if (Number.isFinite(x) && !(x >= best)) best = x;
2550
2596
  return best;
2551
2597
  });
2552
2598
  }
@@ -2574,6 +2620,7 @@ export function min(source: Source): series_float {
2574
2620
  * ```
2575
2621
  */
2576
2622
  export function cog(source: Source, length: simple_int = 10): series_float {
2623
+ checkPositiveLength('cog', length, 0);
2577
2624
  const len = Math.floor(length);
2578
2625
  return runningSum(source, len).map((sum, i) => {
2579
2626
  if (i < len - 1 || Number.isNaN(sum) || sum === 0) return NaN;
@@ -2601,19 +2648,20 @@ export function cog(source: Source, length: simple_int = 10): series_float {
2601
2648
  *
2602
2649
  * @remarks
2603
2650
  * PineScript rules:
2604
- * - the window is the last `length` non-`na` values (an `na` bar does not enter it, and gets the mode of the
2605
- * window it keeps)
2651
+ * - the window is the last `length` non-`na` values (an `na` or +/-Infinity bar does not enter it, and gets the mode
2652
+ * of the window it keeps)
2606
2653
  * - NaN until `length` non-`na` values exist
2607
2654
  * - If no mode exists, returns the smallest value
2608
2655
  */
2609
2656
  export function mode(source: Source, length: simple_int): series_float {
2657
+ checkPositiveLength('mode', length, 0);
2610
2658
  const result: series_float = [];
2611
2659
  const len = Math.floor(length);
2612
2660
  const seen: number[] = []; // non-na values so far
2613
2661
 
2614
2662
  for (let i = 0; i < source.length; i++) {
2615
2663
  const v = source[i]!;
2616
- if (!Number.isNaN(v)) seen.push(v);
2664
+ if (Number.isFinite(v)) seen.push(v);
2617
2665
  if (seen.length < len) {
2618
2666
  result.push(NaN);
2619
2667
  continue;
@@ -2684,6 +2732,7 @@ export function percentile_linear_interpolation(
2684
2732
  length: simple_int,
2685
2733
  percentage: number
2686
2734
  ): series_float {
2735
+ checkPositiveLength('percentile_linear_interpolation', length, 0);
2687
2736
  return sortedWindow(source, length, (s) => {
2688
2737
  const pos = (percentage / 100) * length - 0.5;
2689
2738
  if (pos <= 0) return s[0]!;
@@ -2753,6 +2802,7 @@ export function percentile_nearest_rank(
2753
2802
  length: simple_int,
2754
2803
  percentage: number
2755
2804
  ): series_float {
2805
+ checkPositiveLength('percentile_nearest_rank', length, 0);
2756
2806
  const rank = Math.min(length, Math.max(1, Math.ceil((percentage / 100) * length)));
2757
2807
  return sortedWindow(source, length, (s) => s[rank - 1]!);
2758
2808
  }
@@ -2789,6 +2839,7 @@ export function percentile_nearest_rank(
2789
2839
  * where the `na` was
2790
2840
  */
2791
2841
  export function rci(source: Source, length: simple_int): series_float {
2842
+ checkPositiveLength('rci', length, 0);
2792
2843
  const len = Math.floor(length);
2793
2844
  const result: series_float = [];
2794
2845
  // PineScript keeps the values in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start
@@ -2852,31 +2903,42 @@ export function rci(source: Source, length: simple_int): series_float {
2852
2903
  }
2853
2904
 
2854
2905
  /**
2855
- * Pivot Point Levels
2906
+ * Pivot Point Levels (PineScript `ta.pivot_point_levels`).
2856
2907
  *
2857
- * Calculates pivot point levels using various calculation methods.
2858
- * Returns an array containing: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
2908
+ * Returns 11 series: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5] (one value per bar in each).
2859
2909
  *
2860
2910
  * @param type - Calculation type: "Traditional", "Fibonacci", "Woodie", "Classic", "DM", "Camarilla"
2861
- * @param anchor - Condition that triggers reset of calculations
2862
- * @param developing - If true, pivots recalculate continuously; if false, use last anchor values
2863
- * @param high - High price series (optional, uses context if not provided)
2864
- * @param low - Low price series (optional, uses context if not provided)
2865
- * @param close - Close price series (optional, uses context if not provided)
2866
- * @param open - Open price series (optional, uses context if not provided)
2867
- * @returns Array of 11 pivot levels
2911
+ * @param anchor - Condition that starts a new period (`timeframe.change("W")`...)
2912
+ * @param developing - false: the levels of the previous period, constant until the next anchor; true: the levels of
2913
+ * the current period so far, recalculated on each bar
2914
+ * @param high - High price series
2915
+ * @param low - Low price series
2916
+ * @param close - Close price series
2917
+ * @param open - Open price series (Woodie and DM)
2918
+ * @returns 11 series of levels
2868
2919
  *
2869
2920
  * @example
2870
2921
  * ```typescript
2871
2922
  * const weekChange = [false, false, false, false, true, false, ...]; // Weekly anchor
2872
- * const pivots = ta.pivot_point_levels("Traditional", weekChange, false, high, low, close);
2873
- * // pivots[i]! = [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
2923
+ * const levels = ta.pivot_point_levels("Traditional", weekChange, false, high, low, close, open);
2924
+ * // levels[0]![i] is P on bar i, levels[5]![i] is R3
2874
2925
  * ```
2875
2926
  *
2876
2927
  * @remarks
2877
- * - Woodie type cannot use developing=true (will error in PineScript)
2878
- * - DM type only calculates P, R1, S1 (other levels are NaN)
2879
- * - All calculations follow PineScript v6 specifications
2928
+ * As in PineScript (#177):
2929
+ * - a period runs from an anchor bar to the bar before the next anchor; the bars before the first anchor are a
2930
+ * period too (from bar 0). The fixed levels use the high and low of the previous period and the close of the bar
2931
+ * before the anchor (na with an anchor on bar 0), and are na until the first anchor; the developing levels use
2932
+ * the current period's high and low and the bar's close, from bar 0
2933
+ * - 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),
2934
+ * R5 = P * 4 + (H - 4 * L), S5 = P * 4 - (4 * H - L)
2935
+ * - Fibonacci: P +- 0.382 / 0.618 / 1 * (H - L); R4 to S5 na
2936
+ * - Classic: R3 / S3 = P +- 2 * (H - L), R4 / S4 = P +- 3 * (H - L); R5 / S5 na
2937
+ * - Woodie: P = (H + L + 2 * open of the current period) / 4, R3 = H + 2 * (P - L), S3 = L - 2 * (H - P), R4 / S4 =
2938
+ * R3 + (H - L) / S3 - (H - L); R5 / S5 na. `developing` true throws (RE10128)
2939
+ * - 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,
2940
+ * R1 = x / 2 - L, S1 = x / 2 - H; the other levels na
2941
+ * - Camarilla: C +- (H - L) * 1.1 / 12, / 6, / 4, / 2 for R1 to S4, R5 = (H / L) * C, S5 = 2 * C - R5
2880
2942
  */
2881
2943
  export function pivot_point_levels(
2882
2944
  type: series_float | string,
@@ -2887,183 +2949,96 @@ export function pivot_point_levels(
2887
2949
  close?: Source,
2888
2950
  open?: Source
2889
2951
  ): series_float[] {
2890
- // For simplicity, we need to handle the case where type is a string array
2891
- // But according to the docs, type is "series string", meaning it can change per bar
2892
- // For this implementation, we'll support constant type strings
2893
-
2894
2952
  const typeStr = typeof type === 'string' ? type : String(type[0]);
2895
- const isDeveloping = typeof developing === 'boolean' ? developing : developing[0];
2896
-
2897
- // Woodie cannot be developing
2953
+ const isDeveloping = typeof developing === 'boolean' ? developing : Boolean(developing[0]);
2898
2954
  if (typeStr === 'Woodie' && isDeveloping) {
2899
- throw new Error('ta.pivot_point_levels: Woodie type cannot use developing=true');
2955
+ throw new Error(
2956
+ 'Error on bar 0: The `developing` parameter of the `ta.pivot_point_levels()` cannot be `true` when `type` is "Woodie".'
2957
+ );
2900
2958
  }
2901
-
2902
- // Ensure all series have the same length
2903
- const length = anchor.length;
2904
- if (high && high.length !== length) throw new Error('High series length mismatch');
2905
- if (low && low.length !== length) throw new Error('Low series length mismatch');
2906
- if (close && close.length !== length) throw new Error('Close series length mismatch');
2907
- if (open && open.length !== length) throw new Error('Open series length mismatch');
2908
-
2909
- // Initialize result arrays for all 11 levels
2910
- const results: series_float[] = Array.from({ length: 11 }, () => []);
2911
-
2912
- // Track the last anchor point data
2913
- let lastH = NaN, lastL = NaN, lastC = NaN, lastO = NaN;
2914
- let lastAnchorIndex = -1;
2915
-
2916
- for (let i = 0; i < length; i++) {
2917
- // Check if anchor triggered
2918
- if (anchor[i]!) {
2919
- lastAnchorIndex = i;
2920
- // Store OHLC at anchor point (these will be used for calculations)
2921
- lastH = high ? high[i]! : NaN;
2922
- lastL = low ? low[i]! : NaN;
2923
- lastC = close ? close[i]! : NaN;
2924
- lastO = open ? open[i]! : NaN;
2925
- }
2926
-
2927
- // Calculate data to use
2928
- let h: number, l: number, c: number, o: number;
2929
-
2930
- if (isDeveloping && lastAnchorIndex >= 0) {
2931
- // Developing: use max/min/last since anchor
2932
- h = high ? Math.max(...high.slice(lastAnchorIndex, i + 1).filter(v => !isNaN(v))) : NaN;
2933
- l = low ? Math.min(...low.slice(lastAnchorIndex, i + 1).filter(v => !isNaN(v))) : NaN;
2934
- c = close ? close[i]! : NaN;
2935
- o = open && lastAnchorIndex >= 0 ? open[lastAnchorIndex]! : NaN;
2936
- } else {
2937
- // Not developing: use last anchor values
2938
- h = lastH;
2939
- l = lastL;
2940
- c = lastC;
2941
- o = lastO;
2942
- }
2943
-
2944
- // Calculate pivot levels based on type
2945
- const levels = calculatePivotLevels(typeStr, h, l, c, o);
2946
-
2947
- // Push to results
2948
- for (let j = 0; j < 11; j++) {
2949
- results[j]!.push(levels[j]!);
2950
- }
2959
+ const n = anchor.length;
2960
+ for (const [s, name] of [[high, 'High'], [low, 'Low'], [close, 'Close'], [open, 'Open']] as const) {
2961
+ if (s && s.length !== n) throw new Error(`${name} series length mismatch`);
2962
+ }
2963
+ const at = (s: Source | undefined, i: number): number => (s ? (s[i] ?? NaN) : NaN);
2964
+ const results: series_float[] = Array.from({ length: 11 }, () => new Array<number>(n));
2965
+ // the current period (from bar 0 until the first anchor) and the previous one
2966
+ let curH = NaN, curL = NaN, curO = NaN;
2967
+ let prevH = NaN, prevL = NaN, prevC = NaN, prevO = NaN;
2968
+ let anchored = false;
2969
+ for (let i = 0; i < n; i++) {
2970
+ const h = at(high, i), l = at(low, i);
2971
+ if (i === 0) {
2972
+ // the first period starts on bar 0: an anchor there makes bar 0 its own previous period
2973
+ curH = h;
2974
+ curL = l;
2975
+ curO = at(open, i);
2976
+ }
2977
+ if (anchor[i]) {
2978
+ prevH = curH;
2979
+ prevL = curL;
2980
+ prevC = i > 0 ? at(close, i - 1) : NaN;
2981
+ prevO = curO;
2982
+ curH = h;
2983
+ curL = l;
2984
+ curO = at(open, i);
2985
+ anchored = true;
2986
+ } else if (i > 0) {
2987
+ if (h > curH || Number.isNaN(curH)) curH = h;
2988
+ if (l < curL || Number.isNaN(curL)) curL = l;
2989
+ }
2990
+ const levels = isDeveloping
2991
+ ? pivotLevels(typeStr, curH, curL, at(close, i), curO, at(close, i))
2992
+ : anchored
2993
+ ? pivotLevels(typeStr, prevH, prevL, prevC, prevO, curO)
2994
+ : NA_LEVELS;
2995
+ for (let k = 0; k < 11; k++) results[k]![i] = levels[k]!;
2951
2996
  }
2952
-
2953
2997
  return results;
2954
2998
  }
2955
2999
 
3000
+ const NA_LEVELS: readonly number[] = new Array<number>(11).fill(NaN);
3001
+
2956
3002
  /**
2957
- * Helper function to calculate pivot levels
3003
+ * The 11 levels of one bar, in PineScript's operation order (bit for bit). `o` is the open of the period (DM),
3004
+ * `wo` the close term of Woodie (the open of the current period, or the bar's close while developing).
2958
3005
  */
2959
- function calculatePivotLevels(
2960
- type: string,
2961
- h: number,
2962
- l: number,
2963
- c: number,
2964
- o: number
2965
- ): number[] {
2966
- // Return NaN array if data is insufficient
2967
- if (isNaN(h) || isNaN(l) || isNaN(c)) {
2968
- return Array(11).fill(NaN);
2969
- }
2970
-
2971
- const levels: number[] = Array(11).fill(NaN);
2972
-
2973
- // Calculate pivot point (P)
2974
- let P: number;
2975
-
2976
- switch (type) {
2977
- case 'Traditional':
2978
- case 'Fibonacci':
2979
- case 'Classic':
2980
- P = (h + l + c) / 3;
2981
- break;
2982
- case 'Woodie':
2983
- P = (h + l + 2 * c) / 4;
2984
- break;
2985
- case 'DM':
2986
- P = (h + l + c) / 3;
2987
- break;
2988
- case 'Camarilla':
2989
- P = (h + l + c) / 3;
2990
- break;
2991
- default:
2992
- P = (h + l + c) / 3;
2993
- }
2994
-
2995
- levels[0] = P; // P is at index 0
2996
-
2997
- // Calculate resistance and support levels based on type
3006
+ function pivotLevels(type: string, h: number, l: number, c: number, o: number, wo: number): readonly number[] {
3007
+ const N = NaN;
3008
+ const r = h - l;
2998
3009
  switch (type) {
2999
- case 'Traditional':
3000
- case 'Classic':
3001
- levels[1] = 2 * P - l; // R1
3002
- levels[2] = 2 * P - h; // S1
3003
- levels[3] = P + (h - l); // R2
3004
- levels[4] = P - (h - l); // S2
3005
- levels[5] = h + 2 * (P - l); // R3
3006
- levels[6] = l - 2 * (h - P); // S3
3007
- levels[7] = levels[5] + (h - l); // R4
3008
- levels[8] = levels[6] - (h - l); // S4
3009
- levels[9] = levels[7] + (h - l); // R5
3010
- levels[10] = levels[8] - (h - l); // S5
3011
- break;
3012
-
3013
- case 'Fibonacci':
3014
- levels[1] = P + 0.382 * (h - l); // R1
3015
- levels[2] = P - 0.382 * (h - l); // S1
3016
- levels[3] = P + 0.618 * (h - l); // R2
3017
- levels[4] = P - 0.618 * (h - l); // S2
3018
- levels[5] = P + (h - l); // R3
3019
- levels[6] = P - (h - l); // S3
3020
- levels[7] = levels[5] + 0.618 * (h - l); // R4
3021
- levels[8] = levels[6] - 0.618 * (h - l); // S4
3022
- levels[9] = levels[7] + 0.382 * (h - l); // R5
3023
- levels[10] = levels[8] - 0.382 * (h - l); // S5
3024
- break;
3025
-
3026
- case 'Woodie':
3027
- levels[1] = 2 * P - l; // R1
3028
- levels[2] = 2 * P - h; // S1
3029
- levels[3] = P + (h - l); // R2
3030
- levels[4] = P - (h - l); // S2
3031
- levels[5] = h + 2 * (P - l); // R3
3032
- levels[6] = l - 2 * (h - P); // S3
3033
- levels[7] = levels[5] + (h - l); // R4
3034
- levels[8] = levels[6] - (h - l); // S4
3035
- levels[9] = levels[7] + (h - l); // R5
3036
- levels[10] = levels[8] - (h - l); // S5
3037
- break;
3038
-
3010
+ case 'Traditional': {
3011
+ const p = (h + l + c) / 3;
3012
+ 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),
3013
+ p * 3 - (3 * h - l), p * 4 + (h - 4 * l), p * 4 - (4 * h - l)];
3014
+ }
3015
+ case 'Fibonacci': {
3016
+ const p = (h + l + c) / 3;
3017
+ 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];
3018
+ }
3019
+ case 'Classic': {
3020
+ const p = (h + l + c) / 3;
3021
+ 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];
3022
+ }
3023
+ case 'Woodie': {
3024
+ const p = (h + l + 2 * wo) / 4;
3025
+ const r3 = h + 2 * (p - l);
3026
+ const s3 = l - 2 * (h - p);
3027
+ return [p, 2 * p - l, 2 * p - h, p + r, p - r, r3, s3, r3 + r, s3 - r, N, N];
3028
+ }
3039
3029
  case 'DM': {
3040
- // DM (Demark) only calculates P, R1, S1
3041
- const x = h + l + (c * 2) + (isNaN(o) ? c : o);
3042
- const newP = x / (isNaN(o) ? 4 : 5);
3043
- levels[0] = newP;
3044
- levels[1] = x / 2 - l; // R1
3045
- levels[2] = x / 2 - h; // S1
3046
- // R2-S5 remain NaN
3047
- break;
3030
+ const x = c < o ? h + 2 * l + c : c > o ? 2 * h + l + c : h + l + 2 * c;
3031
+ return [x / 4, x / 2 - l, x / 2 - h, N, N, N, N, N, N, N, N];
3048
3032
  }
3049
-
3050
3033
  case 'Camarilla': {
3051
- const range = h - l;
3052
- levels[1] = c + range * 1.1 / 12; // R1
3053
- levels[2] = c - range * 1.1 / 12; // S1
3054
- levels[3] = c + range * 1.1 / 6; // R2
3055
- levels[4] = c - range * 1.1 / 6; // S2
3056
- levels[5] = c + range * 1.1 / 4; // R3
3057
- levels[6] = c - range * 1.1 / 4; // S3
3058
- levels[7] = c + range * 1.1 / 2; // R4
3059
- levels[8] = c - range * 1.1 / 2; // S4
3060
- levels[9] = h; // R5 (high)
3061
- levels[10] = l; // S5 (low)
3062
- break;
3034
+ const p = (h + l + c) / 3;
3035
+ const r5 = (h / l) * c;
3036
+ 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,
3037
+ c - (r * 1.1) / 4, c + (r * 1.1) / 2, c - (r * 1.1) / 2, r5, 2 * c - r5];
3063
3038
  }
3039
+ default:
3040
+ return NA_LEVELS;
3064
3041
  }
3065
-
3066
- return levels;
3067
3042
  }
3068
3043
 
3069
3044
  /**