oakscriptjs 0.8.1 → 0.9.0

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 (544) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +31 -9
  3. package/dist/cjs/array/index.d.ts +701 -0
  4. package/dist/cjs/array/index.d.ts.map +1 -0
  5. package/dist/cjs/array/index.js +392 -0
  6. package/dist/cjs/array/index.js.map +7 -0
  7. package/dist/cjs/box/index.d.ts +444 -0
  8. package/dist/cjs/box/index.d.ts.map +1 -0
  9. package/dist/cjs/box/index.js +246 -0
  10. package/dist/cjs/box/index.js.map +7 -0
  11. package/dist/cjs/callsite/index.d.ts +61 -0
  12. package/dist/cjs/callsite/index.d.ts.map +1 -0
  13. package/dist/cjs/callsite/index.js +65 -0
  14. package/dist/cjs/callsite/index.js.map +7 -0
  15. package/dist/cjs/chartpoint/index.d.ts +90 -0
  16. package/dist/cjs/chartpoint/index.d.ts.map +1 -0
  17. package/dist/cjs/chartpoint/index.js +56 -0
  18. package/dist/cjs/chartpoint/index.js.map +7 -0
  19. package/dist/cjs/color/index.d.ts +285 -0
  20. package/dist/cjs/color/index.d.ts.map +1 -0
  21. package/dist/cjs/color/index.js +143 -0
  22. package/dist/cjs/color/index.js.map +7 -0
  23. package/dist/cjs/compare/index.d.ts +26 -0
  24. package/dist/cjs/compare/index.d.ts.map +1 -0
  25. package/dist/cjs/compare/index.js +50 -0
  26. package/dist/cjs/compare/index.js.map +7 -0
  27. package/dist/cjs/drawing/registry.d.ts +42 -0
  28. package/dist/cjs/drawing/registry.d.ts.map +1 -0
  29. package/dist/cjs/drawing/registry.js +87 -0
  30. package/dist/cjs/drawing/registry.js.map +7 -0
  31. package/dist/cjs/index.d.ts +85 -0
  32. package/dist/cjs/index.d.ts.map +1 -0
  33. package/dist/cjs/index.js +184 -0
  34. package/dist/cjs/index.js.map +7 -0
  35. package/dist/cjs/indicator.d.ts +117 -0
  36. package/dist/cjs/indicator.d.ts.map +1 -0
  37. package/dist/cjs/indicator.js +74 -0
  38. package/dist/cjs/indicator.js.map +7 -0
  39. package/dist/cjs/input.d.ts +196 -0
  40. package/dist/cjs/input.d.ts.map +1 -0
  41. package/dist/cjs/input.js +197 -0
  42. package/dist/cjs/input.js.map +7 -0
  43. package/dist/cjs/label/index.d.ts +303 -0
  44. package/dist/cjs/label/index.d.ts.map +1 -0
  45. package/dist/cjs/label/index.js +190 -0
  46. package/dist/cjs/label/index.js.map +7 -0
  47. package/dist/cjs/lib/index.d.ts +8 -0
  48. package/dist/cjs/lib/index.d.ts.map +1 -0
  49. package/dist/cjs/lib/index.js +19 -0
  50. package/dist/cjs/lib/index.js.map +7 -0
  51. package/dist/{lib → cjs/lib}/zigzag/index.d.ts +4 -1
  52. package/dist/cjs/lib/zigzag/index.d.ts.map +1 -0
  53. package/dist/cjs/lib/zigzag/index.js +27 -0
  54. package/dist/cjs/lib/zigzag/index.js.map +7 -0
  55. package/dist/cjs/lib/zigzag/zigzag.d.ts +100 -0
  56. package/dist/cjs/lib/zigzag/zigzag.d.ts.map +1 -0
  57. package/dist/cjs/lib/zigzag/zigzag.js +207 -0
  58. package/dist/cjs/lib/zigzag/zigzag.js.map +7 -0
  59. package/dist/cjs/line/index.d.ts +342 -0
  60. package/dist/cjs/line/index.d.ts.map +1 -0
  61. package/dist/cjs/line/index.js +193 -0
  62. package/dist/cjs/line/index.js.map +7 -0
  63. package/dist/cjs/linefill/index.d.ts +124 -0
  64. package/dist/cjs/linefill/index.d.ts.map +1 -0
  65. package/dist/cjs/linefill/index.js +56 -0
  66. package/dist/cjs/linefill/index.js.map +7 -0
  67. package/dist/cjs/map/index.d.ts +69 -0
  68. package/dist/cjs/map/index.d.ts.map +1 -0
  69. package/dist/cjs/map/index.js +80 -0
  70. package/dist/cjs/map/index.js.map +7 -0
  71. package/dist/cjs/math/index.d.ts +560 -0
  72. package/dist/cjs/math/index.d.ts.map +1 -0
  73. package/dist/cjs/math/index.js +441 -0
  74. package/dist/cjs/math/index.js.map +7 -0
  75. package/dist/cjs/matrix/index.d.ts +1231 -0
  76. package/dist/cjs/matrix/index.d.ts.map +1 -0
  77. package/dist/cjs/matrix/index.js +1200 -0
  78. package/dist/cjs/matrix/index.js.map +7 -0
  79. package/dist/cjs/package.json +1 -0
  80. package/dist/cjs/plot.d.ts +76 -0
  81. package/dist/cjs/plot.d.ts.map +1 -0
  82. package/dist/cjs/plot.js +51 -0
  83. package/dist/cjs/plot.js.map +7 -0
  84. package/dist/cjs/polyline/index.d.ts +115 -0
  85. package/dist/cjs/polyline/index.d.ts.map +1 -0
  86. package/dist/cjs/polyline/index.js +76 -0
  87. package/dist/cjs/polyline/index.js.map +7 -0
  88. package/dist/cjs/runtime/adapters/LightweightChartsAdapter.d.ts +86 -0
  89. package/dist/cjs/runtime/adapters/LightweightChartsAdapter.d.ts.map +1 -0
  90. package/dist/cjs/runtime/adapters/LightweightChartsAdapter.js +141 -0
  91. package/dist/cjs/runtime/adapters/LightweightChartsAdapter.js.map +7 -0
  92. package/dist/cjs/runtime/adapters/SimpleInputAdapter.d.ts +59 -0
  93. package/dist/cjs/runtime/adapters/SimpleInputAdapter.d.ts.map +1 -0
  94. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js +117 -0
  95. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js.map +7 -0
  96. package/dist/{runtime → cjs/runtime}/index.d.ts +12 -5
  97. package/dist/cjs/runtime/index.d.ts.map +1 -0
  98. package/dist/cjs/runtime/index.js +49 -0
  99. package/dist/cjs/runtime/index.js.map +7 -0
  100. package/dist/cjs/runtime/inputs.d.ts +91 -0
  101. package/dist/cjs/runtime/inputs.d.ts.map +1 -0
  102. package/dist/cjs/runtime/inputs.js +262 -0
  103. package/dist/cjs/runtime/inputs.js.map +7 -0
  104. package/dist/cjs/runtime/runtime.d.ts +70 -0
  105. package/dist/cjs/runtime/runtime.d.ts.map +1 -0
  106. package/dist/cjs/runtime/runtime.js +148 -0
  107. package/dist/cjs/runtime/runtime.js.map +7 -0
  108. package/dist/cjs/runtime/series.d.ts +362 -0
  109. package/dist/cjs/runtime/series.d.ts.map +1 -0
  110. package/dist/cjs/runtime/series.js +560 -0
  111. package/dist/cjs/runtime/series.js.map +7 -0
  112. package/dist/cjs/runtime/types.d.ts +282 -0
  113. package/dist/cjs/runtime/types.d.ts.map +1 -0
  114. package/dist/cjs/runtime/types.js +17 -0
  115. package/dist/cjs/runtime/types.js.map +7 -0
  116. package/dist/cjs/script/index.d.ts +987 -0
  117. package/dist/cjs/script/index.d.ts.map +1 -0
  118. package/dist/cjs/script/index.js +1169 -0
  119. package/dist/cjs/script/index.js.map +7 -0
  120. package/dist/cjs/security/resample.d.ts +63 -0
  121. package/dist/cjs/security/resample.d.ts.map +1 -0
  122. package/dist/cjs/security/resample.js +84 -0
  123. package/dist/cjs/security/resample.js.map +7 -0
  124. package/dist/cjs/session/bars.d.ts +80 -0
  125. package/dist/cjs/session/bars.d.ts.map +1 -0
  126. package/dist/cjs/session/bars.js +219 -0
  127. package/dist/cjs/session/bars.js.map +7 -0
  128. package/dist/cjs/session/calendar.d.ts +70 -0
  129. package/dist/cjs/session/calendar.d.ts.map +1 -0
  130. package/dist/cjs/session/calendar.js +164 -0
  131. package/dist/cjs/session/calendar.js.map +7 -0
  132. package/dist/cjs/str/dateformat.d.ts +15 -0
  133. package/dist/cjs/str/dateformat.d.ts.map +1 -0
  134. package/dist/cjs/str/dateformat.js +149 -0
  135. package/dist/cjs/str/dateformat.js.map +7 -0
  136. package/dist/cjs/str/index.d.ts +459 -0
  137. package/dist/cjs/str/index.d.ts.map +1 -0
  138. package/dist/cjs/str/index.js +143 -0
  139. package/dist/cjs/str/index.js.map +7 -0
  140. package/dist/cjs/str/messageformat.d.ts +14 -0
  141. package/dist/cjs/str/messageformat.d.ts.map +1 -0
  142. package/dist/cjs/str/messageformat.js +114 -0
  143. package/dist/cjs/str/messageformat.js.map +7 -0
  144. package/dist/cjs/str/numberformat.d.ts +19 -0
  145. package/dist/cjs/str/numberformat.d.ts.map +1 -0
  146. package/dist/cjs/str/numberformat.js +170 -0
  147. package/dist/cjs/str/numberformat.js.map +7 -0
  148. package/dist/{strategy → cjs/strategy}/index.d.ts +50 -0
  149. package/dist/cjs/strategy/index.d.ts.map +1 -0
  150. package/dist/cjs/strategy/index.js +92 -0
  151. package/dist/cjs/strategy/index.js.map +7 -0
  152. package/dist/cjs/ta/index.d.ts +1397 -0
  153. package/dist/cjs/ta/index.d.ts.map +1 -0
  154. package/dist/cjs/ta/index.js +1715 -0
  155. package/dist/cjs/ta/index.js.map +7 -0
  156. package/dist/cjs/ta/running-sum.d.ts +30 -0
  157. package/dist/cjs/ta/running-sum.d.ts.map +1 -0
  158. package/dist/cjs/ta/running-sum.js +99 -0
  159. package/dist/cjs/ta/running-sum.js.map +7 -0
  160. package/dist/cjs/ta-series.d.ts +558 -0
  161. package/dist/cjs/ta-series.d.ts.map +1 -0
  162. package/dist/cjs/ta-series.js +606 -0
  163. package/dist/cjs/ta-series.js.map +7 -0
  164. package/dist/cjs/text/index.d.ts +14 -0
  165. package/dist/cjs/text/index.d.ts.map +1 -0
  166. package/dist/cjs/text/index.js +29 -0
  167. package/dist/cjs/text/index.js.map +7 -0
  168. package/dist/cjs/time/datestring.d.ts +23 -0
  169. package/dist/cjs/time/datestring.d.ts.map +1 -0
  170. package/dist/cjs/time/datestring.js +106 -0
  171. package/dist/cjs/time/datestring.js.map +7 -0
  172. package/dist/cjs/time/index.d.ts +89 -0
  173. package/dist/cjs/time/index.d.ts.map +1 -0
  174. package/dist/cjs/time/index.js +85 -0
  175. package/dist/cjs/time/index.js.map +7 -0
  176. package/dist/cjs/time/session.d.ts +17 -0
  177. package/dist/cjs/time/session.d.ts.map +1 -0
  178. package/dist/cjs/time/session.js +65 -0
  179. package/dist/cjs/time/session.js.map +7 -0
  180. package/dist/cjs/time/timezone.d.ts +40 -0
  181. package/dist/cjs/time/timezone.d.ts.map +1 -0
  182. package/dist/cjs/time/timezone.js +125 -0
  183. package/dist/cjs/time/timezone.js.map +7 -0
  184. package/dist/cjs/timeframe/index.d.ts +73 -0
  185. package/dist/cjs/timeframe/index.d.ts.map +1 -0
  186. package/dist/cjs/timeframe/index.js +85 -0
  187. package/dist/cjs/timeframe/index.js.map +7 -0
  188. package/dist/cjs/types/index.d.ts +343 -0
  189. package/dist/cjs/types/index.d.ts.map +1 -0
  190. package/dist/cjs/types/index.js +32 -0
  191. package/dist/cjs/types/index.js.map +7 -0
  192. package/dist/cjs/types/metadata.d.ts +300 -0
  193. package/dist/cjs/types/metadata.d.ts.map +1 -0
  194. package/dist/cjs/types/metadata.js +17 -0
  195. package/dist/cjs/types/metadata.js.map +7 -0
  196. package/dist/cjs/utils/index.d.ts +167 -0
  197. package/dist/cjs/utils/index.d.ts.map +1 -0
  198. package/dist/cjs/utils/index.js +209 -0
  199. package/dist/cjs/utils/index.js.map +7 -0
  200. package/dist/esm/array/index.d.ts +701 -0
  201. package/dist/esm/array/index.d.ts.map +1 -0
  202. package/dist/esm/array/index.js +983 -0
  203. package/dist/esm/array/index.js.map +1 -0
  204. package/dist/esm/box/index.d.ts +444 -0
  205. package/dist/esm/box/index.d.ts.map +1 -0
  206. package/dist/esm/box/index.js +602 -0
  207. package/dist/esm/box/index.js.map +1 -0
  208. package/dist/esm/callsite/index.d.ts +61 -0
  209. package/dist/esm/callsite/index.d.ts.map +1 -0
  210. package/dist/esm/callsite/index.js +97 -0
  211. package/dist/esm/callsite/index.js.map +1 -0
  212. package/dist/esm/chartpoint/index.d.ts +90 -0
  213. package/dist/esm/chartpoint/index.d.ts.map +1 -0
  214. package/dist/esm/chartpoint/index.js +113 -0
  215. package/dist/esm/chartpoint/index.js.map +1 -0
  216. package/dist/esm/color/index.d.ts +285 -0
  217. package/dist/esm/color/index.d.ts.map +1 -0
  218. package/dist/esm/color/index.js +373 -0
  219. package/dist/esm/color/index.js.map +1 -0
  220. package/dist/esm/compare/index.d.ts +26 -0
  221. package/dist/esm/compare/index.d.ts.map +1 -0
  222. package/dist/esm/compare/index.js +39 -0
  223. package/dist/esm/compare/index.js.map +1 -0
  224. package/dist/esm/drawing/registry.d.ts +42 -0
  225. package/dist/esm/drawing/registry.d.ts.map +1 -0
  226. package/dist/esm/drawing/registry.js +89 -0
  227. package/dist/esm/drawing/registry.js.map +1 -0
  228. package/dist/esm/index.d.ts +85 -0
  229. package/dist/esm/index.d.ts.map +1 -0
  230. package/dist/esm/index.js +96 -0
  231. package/dist/esm/index.js.map +1 -0
  232. package/dist/esm/indicator.d.ts +117 -0
  233. package/dist/esm/indicator.d.ts.map +1 -0
  234. package/dist/esm/indicator.js +84 -0
  235. package/dist/esm/indicator.js.map +1 -0
  236. package/dist/esm/input.d.ts +196 -0
  237. package/dist/esm/input.d.ts.map +1 -0
  238. package/dist/esm/input.js +183 -0
  239. package/dist/esm/input.js.map +1 -0
  240. package/dist/esm/label/index.d.ts +303 -0
  241. package/dist/esm/label/index.d.ts.map +1 -0
  242. package/dist/esm/label/index.js +417 -0
  243. package/dist/esm/label/index.js.map +1 -0
  244. package/dist/esm/lib/index.d.ts +8 -0
  245. package/dist/esm/lib/index.d.ts.map +1 -0
  246. package/dist/esm/lib/index.js +8 -0
  247. package/dist/esm/lib/index.js.map +1 -0
  248. package/dist/esm/lib/zigzag/index.d.ts +5 -0
  249. package/dist/esm/lib/zigzag/index.d.ts.map +1 -0
  250. package/dist/esm/lib/zigzag/index.js +5 -0
  251. package/dist/esm/lib/zigzag/index.js.map +1 -0
  252. package/dist/esm/lib/zigzag/zigzag.d.ts +100 -0
  253. package/dist/esm/lib/zigzag/zigzag.d.ts.map +1 -0
  254. package/dist/esm/lib/zigzag/zigzag.js +229 -0
  255. package/dist/esm/lib/zigzag/zigzag.js.map +1 -0
  256. package/dist/esm/line/index.d.ts +342 -0
  257. package/dist/esm/line/index.d.ts.map +1 -0
  258. package/dist/esm/line/index.js +470 -0
  259. package/dist/esm/line/index.js.map +1 -0
  260. package/dist/esm/linefill/index.d.ts +124 -0
  261. package/dist/esm/linefill/index.d.ts.map +1 -0
  262. package/dist/esm/linefill/index.js +144 -0
  263. package/dist/esm/linefill/index.js.map +1 -0
  264. package/dist/esm/map/index.d.ts +69 -0
  265. package/dist/esm/map/index.d.ts.map +1 -0
  266. package/dist/esm/map/index.js +106 -0
  267. package/dist/esm/map/index.js.map +1 -0
  268. package/dist/esm/math/index.d.ts +560 -0
  269. package/dist/esm/math/index.d.ts.map +1 -0
  270. package/dist/esm/math/index.js +495 -0
  271. package/dist/esm/math/index.js.map +1 -0
  272. package/dist/esm/matrix/index.d.ts +1231 -0
  273. package/dist/esm/matrix/index.d.ts.map +1 -0
  274. package/dist/esm/matrix/index.js +2450 -0
  275. package/dist/esm/matrix/index.js.map +1 -0
  276. package/dist/esm/plot.d.ts +76 -0
  277. package/dist/esm/plot.d.ts.map +1 -0
  278. package/dist/esm/plot.js +63 -0
  279. package/dist/esm/plot.js.map +1 -0
  280. package/dist/esm/polyline/index.d.ts +115 -0
  281. package/dist/esm/polyline/index.d.ts.map +1 -0
  282. package/dist/esm/polyline/index.js +157 -0
  283. package/dist/esm/polyline/index.js.map +1 -0
  284. package/dist/esm/runtime/adapters/LightweightChartsAdapter.d.ts +86 -0
  285. package/dist/esm/runtime/adapters/LightweightChartsAdapter.d.ts.map +1 -0
  286. package/dist/esm/runtime/adapters/LightweightChartsAdapter.js +140 -0
  287. package/dist/esm/runtime/adapters/LightweightChartsAdapter.js.map +1 -0
  288. package/dist/esm/runtime/adapters/SimpleInputAdapter.d.ts +59 -0
  289. package/dist/esm/runtime/adapters/SimpleInputAdapter.d.ts.map +1 -0
  290. package/dist/esm/runtime/adapters/SimpleInputAdapter.js +115 -0
  291. package/dist/esm/runtime/adapters/SimpleInputAdapter.js.map +1 -0
  292. package/dist/esm/runtime/index.d.ts +13 -0
  293. package/dist/esm/runtime/index.d.ts.map +1 -0
  294. package/dist/esm/runtime/index.js +14 -0
  295. package/dist/esm/runtime/index.js.map +1 -0
  296. package/dist/esm/runtime/inputs.d.ts +91 -0
  297. package/dist/esm/runtime/inputs.d.ts.map +1 -0
  298. package/dist/esm/runtime/inputs.js +334 -0
  299. package/dist/esm/runtime/inputs.js.map +1 -0
  300. package/dist/esm/runtime/runtime.d.ts +70 -0
  301. package/dist/esm/runtime/runtime.d.ts.map +1 -0
  302. package/dist/esm/runtime/runtime.js +193 -0
  303. package/dist/esm/runtime/runtime.js.map +1 -0
  304. package/dist/esm/runtime/series.d.ts +362 -0
  305. package/dist/esm/runtime/series.d.ts.map +1 -0
  306. package/dist/esm/runtime/series.js +583 -0
  307. package/dist/esm/runtime/series.js.map +1 -0
  308. package/dist/esm/runtime/types.d.ts +282 -0
  309. package/dist/esm/runtime/types.d.ts.map +1 -0
  310. package/dist/esm/runtime/types.js +7 -0
  311. package/dist/esm/runtime/types.js.map +1 -0
  312. package/dist/esm/script/index.d.ts +987 -0
  313. package/dist/esm/script/index.d.ts.map +1 -0
  314. package/dist/esm/script/index.js +1289 -0
  315. package/dist/esm/script/index.js.map +1 -0
  316. package/dist/esm/security/resample.d.ts +63 -0
  317. package/dist/esm/security/resample.d.ts.map +1 -0
  318. package/dist/esm/security/resample.js +110 -0
  319. package/dist/esm/security/resample.js.map +1 -0
  320. package/dist/esm/session/bars.d.ts +80 -0
  321. package/dist/esm/session/bars.d.ts.map +1 -0
  322. package/dist/esm/session/bars.js +232 -0
  323. package/dist/esm/session/bars.js.map +1 -0
  324. package/dist/esm/session/calendar.d.ts +70 -0
  325. package/dist/esm/session/calendar.d.ts.map +1 -0
  326. package/dist/esm/session/calendar.js +172 -0
  327. package/dist/esm/session/calendar.js.map +1 -0
  328. package/dist/esm/str/dateformat.d.ts +15 -0
  329. package/dist/esm/str/dateformat.d.ts.map +1 -0
  330. package/dist/esm/str/dateformat.js +136 -0
  331. package/dist/esm/str/dateformat.js.map +1 -0
  332. package/dist/esm/str/index.d.ts +459 -0
  333. package/dist/esm/str/index.d.ts.map +1 -0
  334. package/dist/esm/str/index.js +540 -0
  335. package/dist/esm/str/index.js.map +1 -0
  336. package/dist/esm/str/messageformat.d.ts +14 -0
  337. package/dist/esm/str/messageformat.d.ts.map +1 -0
  338. package/dist/esm/str/messageformat.js +111 -0
  339. package/dist/esm/str/messageformat.js.map +1 -0
  340. package/dist/esm/str/numberformat.d.ts +19 -0
  341. package/dist/esm/str/numberformat.d.ts.map +1 -0
  342. package/dist/esm/str/numberformat.js +189 -0
  343. package/dist/esm/str/numberformat.js.map +1 -0
  344. package/dist/esm/strategy/index.d.ts +245 -0
  345. package/dist/esm/strategy/index.d.ts.map +1 -0
  346. package/dist/esm/strategy/index.js +84 -0
  347. package/dist/esm/strategy/index.js.map +1 -0
  348. package/dist/esm/ta/index.d.ts +1397 -0
  349. package/dist/esm/ta/index.d.ts.map +1 -0
  350. package/dist/esm/ta/index.js +3227 -0
  351. package/dist/esm/ta/index.js.map +1 -0
  352. package/dist/esm/ta/running-sum.d.ts +30 -0
  353. package/dist/esm/ta/running-sum.d.ts.map +1 -0
  354. package/dist/esm/ta/running-sum.js +113 -0
  355. package/dist/esm/ta/running-sum.js.map +1 -0
  356. package/dist/esm/ta-series.d.ts +558 -0
  357. package/dist/esm/ta-series.d.ts.map +1 -0
  358. package/dist/esm/ta-series.js +910 -0
  359. package/dist/esm/ta-series.js.map +1 -0
  360. package/dist/esm/text/index.d.ts +14 -0
  361. package/dist/esm/text/index.d.ts.map +1 -0
  362. package/dist/esm/text/index.js +14 -0
  363. package/dist/esm/text/index.js.map +1 -0
  364. package/dist/esm/time/datestring.d.ts +23 -0
  365. package/dist/esm/time/datestring.d.ts.map +1 -0
  366. package/dist/esm/time/datestring.js +94 -0
  367. package/dist/esm/time/datestring.js.map +1 -0
  368. package/dist/esm/time/index.d.ts +89 -0
  369. package/dist/esm/time/index.d.ts.map +1 -0
  370. package/dist/esm/time/index.js +108 -0
  371. package/dist/esm/time/index.js.map +1 -0
  372. package/dist/esm/time/session.d.ts +17 -0
  373. package/dist/esm/time/session.d.ts.map +1 -0
  374. package/dist/esm/time/session.js +69 -0
  375. package/dist/esm/time/session.js.map +1 -0
  376. package/dist/esm/time/timezone.d.ts +40 -0
  377. package/dist/esm/time/timezone.d.ts.map +1 -0
  378. package/dist/esm/time/timezone.js +144 -0
  379. package/dist/esm/time/timezone.js.map +1 -0
  380. package/dist/esm/timeframe/index.d.ts +73 -0
  381. package/dist/esm/timeframe/index.d.ts.map +1 -0
  382. package/dist/esm/timeframe/index.js +131 -0
  383. package/dist/esm/timeframe/index.js.map +1 -0
  384. package/dist/esm/types/index.d.ts +343 -0
  385. package/dist/esm/types/index.d.ts.map +1 -0
  386. package/dist/esm/types/index.js +17 -0
  387. package/dist/esm/types/index.js.map +1 -0
  388. package/dist/esm/types/metadata.d.ts +300 -0
  389. package/dist/esm/types/metadata.d.ts.map +1 -0
  390. package/dist/esm/types/metadata.js +7 -0
  391. package/dist/esm/types/metadata.js.map +1 -0
  392. package/dist/esm/utils/index.d.ts +167 -0
  393. package/dist/esm/utils/index.d.ts.map +1 -0
  394. package/dist/esm/utils/index.js +295 -0
  395. package/dist/esm/utils/index.js.map +1 -0
  396. package/package.json +53 -23
  397. package/src/array/index.ts +1056 -0
  398. package/src/box/index.ts +680 -0
  399. package/src/callsite/index.ts +104 -0
  400. package/src/chartpoint/index.ts +126 -0
  401. package/src/color/index.ts +411 -0
  402. package/src/compare/index.ts +46 -0
  403. package/src/drawing/registry.ts +106 -0
  404. package/src/index.ts +201 -0
  405. package/src/indicator.ts +185 -0
  406. package/src/input.ts +280 -0
  407. package/src/label/index.ts +490 -0
  408. package/src/lib/index.ts +8 -0
  409. package/src/lib/zigzag/index.ts +13 -0
  410. package/src/lib/zigzag/zigzag.ts +318 -0
  411. package/src/line/index.ts +530 -0
  412. package/src/linefill/index.ts +152 -0
  413. package/src/map/index.ts +115 -0
  414. package/src/math/index.ts +1071 -0
  415. package/src/matrix/index.ts +2697 -0
  416. package/src/plot.ts +121 -0
  417. package/src/polyline/index.ts +184 -0
  418. package/src/runtime/adapters/LightweightChartsAdapter.ts +201 -0
  419. package/src/runtime/adapters/SimpleInputAdapter.ts +132 -0
  420. package/src/runtime/index.ts +52 -0
  421. package/src/runtime/inputs.ts +377 -0
  422. package/src/runtime/runtime.ts +241 -0
  423. package/src/runtime/series.ts +644 -0
  424. package/src/runtime/types.ts +292 -0
  425. package/src/script/index.ts +1865 -0
  426. package/src/security/resample.ts +119 -0
  427. package/src/session/bars.ts +262 -0
  428. package/src/session/calendar.ts +199 -0
  429. package/src/str/dateformat.ts +104 -0
  430. package/src/str/index.ts +557 -0
  431. package/src/str/messageformat.ts +109 -0
  432. package/src/str/numberformat.ts +197 -0
  433. package/src/strategy/index.ts +281 -0
  434. package/src/ta/index.ts +3580 -0
  435. package/src/ta/running-sum.ts +117 -0
  436. package/src/ta-series.ts +1036 -0
  437. package/src/text/index.ts +14 -0
  438. package/src/time/datestring.ts +96 -0
  439. package/src/time/index.ts +161 -0
  440. package/src/time/session.ts +79 -0
  441. package/src/time/timezone.ts +180 -0
  442. package/src/timeframe/index.ts +144 -0
  443. package/src/types/index.ts +398 -0
  444. package/src/types/metadata.ts +334 -0
  445. package/src/utils/index.ts +334 -0
  446. package/dist/array/index.d.ts +0 -57
  447. package/dist/array/index.d.ts.map +0 -1
  448. package/dist/box/index.d.ts +0 -37
  449. package/dist/box/index.d.ts.map +0 -1
  450. package/dist/callsite/index.d.ts +0 -7
  451. package/dist/callsite/index.d.ts.map +0 -1
  452. package/dist/chartpoint/index.d.ts +0 -8
  453. package/dist/chartpoint/index.d.ts.map +0 -1
  454. package/dist/color/index.d.ts +0 -28
  455. package/dist/color/index.d.ts.map +0 -1
  456. package/dist/compare/index.d.ts +0 -8
  457. package/dist/compare/index.d.ts.map +0 -1
  458. package/dist/drawing/registry.d.ts +0 -19
  459. package/dist/drawing/registry.d.ts.map +0 -1
  460. package/dist/index.cjs +0 -7979
  461. package/dist/index.d.ts +0 -64
  462. package/dist/index.d.ts.map +0 -1
  463. package/dist/index.mjs +0 -7962
  464. package/dist/indicator.d.ts +0 -42
  465. package/dist/indicator.d.ts.map +0 -1
  466. package/dist/input.d.ts +0 -47
  467. package/dist/input.d.ts.map +0 -1
  468. package/dist/label/index.d.ts +0 -30
  469. package/dist/label/index.d.ts.map +0 -1
  470. package/dist/lib/index.d.ts +0 -2
  471. package/dist/lib/index.d.ts.map +0 -1
  472. package/dist/lib/zigzag/index.d.ts.map +0 -1
  473. package/dist/lib/zigzag/zigzag.d.ts +0 -47
  474. package/dist/lib/zigzag/zigzag.d.ts.map +0 -1
  475. package/dist/line/index.d.ts +0 -30
  476. package/dist/line/index.d.ts.map +0 -1
  477. package/dist/linefill/index.d.ts +0 -10
  478. package/dist/linefill/index.d.ts.map +0 -1
  479. package/dist/map/index.d.ts +0 -14
  480. package/dist/map/index.d.ts.map +0 -1
  481. package/dist/math/index.d.ts +0 -53
  482. package/dist/math/index.d.ts.map +0 -1
  483. package/dist/matrix/index.d.ts +0 -52
  484. package/dist/matrix/index.d.ts.map +0 -1
  485. package/dist/plot.d.ts +0 -20
  486. package/dist/plot.d.ts.map +0 -1
  487. package/dist/polyline/index.d.ts +0 -11
  488. package/dist/polyline/index.d.ts.map +0 -1
  489. package/dist/runtime/adapters/LightweightChartsAdapter.d.ts +0 -15
  490. package/dist/runtime/adapters/LightweightChartsAdapter.d.ts.map +0 -1
  491. package/dist/runtime/adapters/SimpleInputAdapter.d.ts +0 -16
  492. package/dist/runtime/adapters/SimpleInputAdapter.d.ts.map +0 -1
  493. package/dist/runtime/index.cjs +0 -600
  494. package/dist/runtime/index.d.ts.map +0 -1
  495. package/dist/runtime/index.mjs +0 -577
  496. package/dist/runtime/inputs.d.ts +0 -13
  497. package/dist/runtime/inputs.d.ts.map +0 -1
  498. package/dist/runtime/runtime.d.ts +0 -16
  499. package/dist/runtime/runtime.d.ts.map +0 -1
  500. package/dist/runtime/series.d.ts +0 -60
  501. package/dist/runtime/series.d.ts.map +0 -1
  502. package/dist/runtime/types.d.ts +0 -122
  503. package/dist/runtime/types.d.ts.map +0 -1
  504. package/dist/script/index.cjs +0 -6372
  505. package/dist/script/index.d.ts +0 -709
  506. package/dist/script/index.d.ts.map +0 -1
  507. package/dist/script/index.mjs +0 -6355
  508. package/dist/security/resample.d.ts +0 -13
  509. package/dist/security/resample.d.ts.map +0 -1
  510. package/dist/session/bars.d.ts +0 -34
  511. package/dist/session/bars.d.ts.map +0 -1
  512. package/dist/session/calendar.d.ts +0 -31
  513. package/dist/session/calendar.d.ts.map +0 -1
  514. package/dist/str/dateformat.d.ts +0 -2
  515. package/dist/str/dateformat.d.ts.map +0 -1
  516. package/dist/str/index.d.ts +0 -24
  517. package/dist/str/index.d.ts.map +0 -1
  518. package/dist/str/messageformat.d.ts +0 -2
  519. package/dist/str/messageformat.d.ts.map +0 -1
  520. package/dist/str/numberformat.d.ts +0 -3
  521. package/dist/str/numberformat.d.ts.map +0 -1
  522. package/dist/strategy/index.d.ts.map +0 -1
  523. package/dist/ta/index.d.ts +0 -72
  524. package/dist/ta/index.d.ts.map +0 -1
  525. package/dist/ta-series.d.ts +0 -143
  526. package/dist/ta-series.d.ts.map +0 -1
  527. package/dist/text/index.d.ts +0 -4
  528. package/dist/text/index.d.ts.map +0 -1
  529. package/dist/time/datestring.d.ts +0 -2
  530. package/dist/time/datestring.d.ts.map +0 -1
  531. package/dist/time/index.d.ts +0 -15
  532. package/dist/time/index.d.ts.map +0 -1
  533. package/dist/time/session.d.ts +0 -2
  534. package/dist/time/session.d.ts.map +0 -1
  535. package/dist/time/timezone.d.ts +0 -13
  536. package/dist/time/timezone.d.ts.map +0 -1
  537. package/dist/timeframe/index.d.ts +0 -17
  538. package/dist/timeframe/index.d.ts.map +0 -1
  539. package/dist/types/index.d.ts +0 -181
  540. package/dist/types/index.d.ts.map +0 -1
  541. package/dist/types/metadata.d.ts +0 -131
  542. package/dist/types/metadata.d.ts.map +0 -1
  543. package/dist/utils/index.d.ts +0 -26
  544. package/dist/utils/index.d.ts.map +0 -1
@@ -0,0 +1,3580 @@
1
+ /**
2
+ * Technical Analysis (ta) namespace
3
+ * Mirrors PineScript's ta.* functions for technical analysis indicators and calculations.
4
+ *
5
+ * @remarks
6
+ * All technical analysis functions in this namespace follow PineScript v6 API specifications.
7
+ *
8
+ * @version 6
9
+ */
10
+
11
+ import type { series_float, series_bool, series_int, int, Source, simple_int, simple_float, simple_bool } from '../types/index.js';
12
+ import { eq, ge, gt, le, lt } from '../compare/index.js';
13
+ import { runningSum, runningVariance } from './running-sum.js';
14
+
15
+ /**
16
+ * Simple Moving Average - returns the moving average (sum of last y values divided by y).
17
+ *
18
+ * @param source - Series of values to process
19
+ * @param length - Number of bars (length)
20
+ * @returns Simple moving average of source for length bars back
21
+ *
22
+ * @remarks
23
+ * - `na` values (also +/-Infinity) in the source series are ignored: the mean of the last `length` non-`na`
24
+ * values, and a bar with an `na` source keeps the previous result (as in PineScript)
25
+ * - Returns NaN until `length` non-`na` values are available
26
+ * - The sum is a compensated running sum, as in PineScript, so the result can differ from the exact window mean in
27
+ * the last bits (bit for bit with PineScript)
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const closePrices = [10, 11, 12, 13, 14];
32
+ * const sma5 = ta.sma(closePrices, 5); // Returns: [NaN, NaN, NaN, NaN, 12]
33
+ * ```
34
+ */
35
+ export function sma(source: Source, length: simple_int): series_float {
36
+ // Floor the length to match PineScript's auto-truncation of float to int
37
+ const len = Math.floor(length);
38
+ return runningSum(source, len).map((sum) => sum / len);
39
+ }
40
+
41
+ /** Mean of the last `length` bars; na when the window holds an na value (used by ta.dev). */
42
+ function strictWindowMean(source: Source, length: simple_int): series_float {
43
+ const len = Math.floor(length);
44
+ return Array.from({ length: source.length }, (_, i) => {
45
+ if (i < len - 1) return NaN;
46
+ let sum = 0;
47
+ for (let j = 0; j < len; j++) sum += source[i - j]!;
48
+ return sum / len;
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Exponential Moving Average - returns the exponentially weighted moving average.
54
+ *
55
+ * @param source - Series of values to process
56
+ * @param length - Number of bars (length)
57
+ * @returns Exponential moving average of source with alpha = 2 / (length + 1)
58
+ *
59
+ * @remarks
60
+ * - In EMA, weighting factors decrease exponentially
61
+ * - Formula: `EMA = alpha * source + (1 - alpha) * EMA[1]`, where `alpha = 2 / (length + 1)`
62
+ * - `na` values (also +/-Infinity) in the source series are ignored
63
+ * - The function calculates on the `length` quantity of non-`na` values
64
+ * - May cause indicator repainting
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const closePrices = [10, 11, 12, 13, 14];
69
+ * const ema5 = ta.ema(closePrices, 5);
70
+ * ```
71
+ */
72
+ export function ema(source: Source, length: simple_int): series_float {
73
+ const result: series_float = [];
74
+ const len = Math.floor(length);
75
+ const multiplier = 2 / (len + 1);
76
+
77
+ // SMA seed: slide past leading NaN, then collect `len` valid values.
78
+ // An infinite value is na, as in PineScript.
79
+ let firstValidIndex = -1;
80
+ let validCount = 0;
81
+ let initSum = 0;
82
+
83
+ for (let i = 0; i < source.length; i++) {
84
+ const val = source[i];
85
+ if (val !== undefined && Number.isFinite(val)) {
86
+ initSum += val;
87
+ validCount++;
88
+ if (validCount === len) {
89
+ firstValidIndex = i;
90
+ break;
91
+ }
92
+ }
93
+ }
94
+
95
+ let emaValue = validCount > 0 ? initSum / validCount : NaN;
96
+ const emaInitialized = firstValidIndex >= 0;
97
+
98
+ for (let i = 0; i < source.length; i++) {
99
+ if (!emaInitialized || i < firstValidIndex) {
100
+ result.push(NaN);
101
+ } else if (i === firstValidIndex) {
102
+ result.push(emaValue);
103
+ } else {
104
+ const val = source[i];
105
+ if (val !== undefined && Number.isFinite(val)) {
106
+ emaValue = (val - emaValue) * multiplier + emaValue;
107
+ result.push(emaValue);
108
+ } else {
109
+ // na source: na on this bar; the next bar continues from the last value (as in PineScript)
110
+ result.push(NaN);
111
+ }
112
+ }
113
+ }
114
+
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * Relative Strength Index - momentum oscillator measuring speed and magnitude of price changes.
120
+ *
121
+ * @param source - Series of values to process
122
+ * @param length - Number of bars (length)
123
+ * @returns RSI series (values range from 0 to 100)
124
+ *
125
+ * @remarks
126
+ * - RSI values above 70 typically indicate overbought conditions
127
+ * - RSI values below 30 typically indicate oversold conditions
128
+ * - Uses `ta.rma()` (Relative Moving Average) for smoothing, matching PineScript v6
129
+ * - Formula: RSI = 100 - (100 / (1 + RS)), where RS = Average Gain / Average Loss
130
+ * - Average Gain and Average Loss are calculated using RMA (alpha = 1 / length)
131
+ * - `na` values in the source series are ignored
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const rsi14 = ta.rsi(closePrices, 14);
136
+ * // Identify overbought/oversold conditions
137
+ * const overbought = rsi14.map(v => v > 70);
138
+ * const oversold = rsi14.map(v => v < 30);
139
+ * ```
140
+ */
141
+ export function rsi(source: Source, length: simple_int): series_float {
142
+ const result: series_float = [];
143
+
144
+ // Calculate price changes
145
+ const changes: number[] = [];
146
+ for (let i = 1; i < source.length; i++) {
147
+ changes.push(source[i]! - source[i - 1]!);
148
+ }
149
+
150
+ // Separate gains and losses; a change from or to na is na (skipped by rma), not a 0 gain / loss
151
+ const gains: number[] = changes.map(c => (Number.isNaN(c) ? NaN : c > 0 ? c : 0));
152
+ const losses: number[] = changes.map(c => (Number.isNaN(c) ? NaN : c < 0 ? -c : 0));
153
+
154
+ // Calculate average gains and losses using RMA (not SMA)
155
+ const avgGains = rma(gains, length);
156
+ const avgLosses = rma(losses, length);
157
+
158
+ result.push(NaN); // First value is NaN
159
+
160
+ for (let i = 0; i < avgGains.length; i++) {
161
+ if (avgLosses[i]! === 0) {
162
+ result.push(100);
163
+ } else {
164
+ const rs = avgGains[i]! / avgLosses[i]!;
165
+ result.push(100 - (100 / (1 + rs)));
166
+ }
167
+ }
168
+
169
+ return result;
170
+ }
171
+
172
+ /**
173
+ * Moving Average Convergence Divergence - trend-following momentum indicator showing relationship
174
+ * between two moving averages.
175
+ *
176
+ * @param source - Series of values to process
177
+ * @param fastLength - Fast EMA length (typically 12)
178
+ * @param slowLength - Slow EMA length (typically 26)
179
+ * @param signalLength - Signal line EMA length (typically 9)
180
+ * @returns Tuple of [macdLine, signalLine, histogram]
181
+ *
182
+ * @remarks
183
+ * - MACD Line = Fast EMA - Slow EMA
184
+ * - Signal Line = EMA of MACD Line
185
+ * - Histogram = MACD Line - Signal Line
186
+ * - Crossovers between MACD and signal line indicate potential buy/sell signals
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * const [macdLine, signal, histogram] = ta.macd(closePrices, 12, 26, 9);
191
+ * ```
192
+ */
193
+ export function macd(
194
+ source: Source,
195
+ fastLength: simple_int,
196
+ slowLength: simple_int,
197
+ signalLength: simple_int
198
+ ): [series_float, series_float, series_float] {
199
+ const fastEma = ema(source, fastLength);
200
+ const slowEma = ema(source, slowLength);
201
+
202
+ const macdLine: series_float = [];
203
+ for (let i = 0; i < source.length; i++) {
204
+ macdLine.push(fastEma[i]! - slowEma[i]!);
205
+ }
206
+
207
+ const signalLine = ema(macdLine, signalLength);
208
+
209
+ const histogram: series_float = [];
210
+ for (let i = 0; i < source.length; i++) {
211
+ histogram.push(macdLine[i]! - signalLine[i]!);
212
+ }
213
+
214
+ return [macdLine, signalLine, histogram];
215
+ }
216
+
217
+ /**
218
+ * Bollinger Bands - a technical analysis tool defined by lines plotted two standard deviations
219
+ * away from a simple moving average.
220
+ *
221
+ * @param series - Series of values to process
222
+ * @param length - Number of bars (length)
223
+ * @param mult - Standard deviation factor
224
+ * @returns Tuple of [middle, upper, lower] bands
225
+ *
226
+ * @remarks
227
+ * - Middle band is the SMA of the source
228
+ * - Upper band = middle + (mult * standard deviation)
229
+ * - Lower band = middle - (mult * standard deviation)
230
+ * - `na` values in the source series are ignored
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * const [middle, upper, lower] = ta.bb(closePrices, 20, 2);
235
+ * ```
236
+ */
237
+ export function bb(
238
+ series: Source,
239
+ length: simple_int,
240
+ mult: simple_float
241
+ ): [series_float, series_float, series_float] {
242
+ const basis = sma(series, length);
243
+ const dev = stdev(series, length);
244
+
245
+ const upper: series_float = [];
246
+ const lower: series_float = [];
247
+
248
+ for (let i = 0; i < series.length; i++) {
249
+ upper.push(basis[i]! + mult * dev[i]!);
250
+ lower.push(basis[i]! - mult * dev[i]!);
251
+ }
252
+
253
+ return [basis, upper, lower];
254
+ }
255
+
256
+ /**
257
+ * Standard deviation over the last `length` non-na values.
258
+ *
259
+ * @param source - Series of values to process
260
+ * @param length - Number of values
261
+ * @param biased - true (default): biased estimate (divides by `length`); false: unbiased (by `length - 1`)
262
+ * @returns Standard deviation series (na until `length` non-na values exist)
263
+ *
264
+ * @remarks
265
+ * As in PineScript: `na` values (also +/-Infinity) are skipped, the window holds the last
266
+ * `length` non-na values, and a bar whose own value is `na` keeps the previous result.
267
+ * The result is the square root of {@link variance} (0 when the variance is not positive), computed from
268
+ * compensated running sums of the values and of their squares, bit for bit as PineScript.
269
+ */
270
+ export function stdev(source: Source, length: simple_int, biased: simple_bool = true): series_float {
271
+ return variance(source, length, biased).map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
272
+ }
273
+
274
+ /**
275
+ * The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist.
276
+ * `isNa` tells which values are skipped (NaN by default).
277
+ */
278
+ function lastValues(source: Source, i: number, length: number): number[] | null {
279
+ const values: number[] = [];
280
+ for (let j = i; j >= 0 && values.length < length; j--) {
281
+ const x = source[j]!;
282
+ if (!Number.isNaN(x)) values.push(x);
283
+ }
284
+ return values.length === length ? values : null;
285
+ }
286
+
287
+ /**
288
+ * The extreme of the window of `length` bars ending at each bar, in one pass (monotonic deque): the bar index of
289
+ * the oldest highest (or lowest) value, or -1 when the window is empty. As PineScript does for ta.highest /
290
+ * ta.lowest / ta.highestbars / ta.lowestbars, the window stops at the first `na` value going back, so a bar whose
291
+ * own value is `na` has an empty window. `zeros[i]` tells whether the window holds a +0 (highest) or a -0
292
+ * (lowest), the zero that `Math.max` / `Math.min` return when the extreme is 0.
293
+ */
294
+ function windowExtremes(source: Source, length: number, lowest: boolean): { at: Int32Array; zeros: Uint8Array } {
295
+ const n = source.length;
296
+ const span = length > 0 ? Math.ceil(length) : 0; // the bars j = 0, 1... with j < length
297
+ const at = new Int32Array(n).fill(-1);
298
+ const zeros = new Uint8Array(n);
299
+ const queue = new Int32Array(n); // bar indices; values from best to worst, the oldest first among equal values
300
+ const preferredZero = lowest ? -0 : 0;
301
+ let head = 0;
302
+ let tail = 0;
303
+ let zeroCount = 0; // preferred zeros in the queue
304
+ let lastNa = -1;
305
+ const isPreferredZero = (k: number) => Object.is(source[k], preferredZero);
306
+ for (let i = 0; i < n; i++) {
307
+ const x = source[i]!;
308
+ if (Number.isNaN(x)) {
309
+ lastNa = i;
310
+ head = tail = 0;
311
+ zeroCount = 0;
312
+ continue;
313
+ }
314
+ // drop the values that the new one beats; equal values stay (the oldest wins ties)
315
+ while (tail > head && (lowest ? source[queue[tail - 1]!]! > x : source[queue[tail - 1]!]! < x)) {
316
+ if (isPreferredZero(queue[--tail]!)) zeroCount--;
317
+ }
318
+ queue[tail++] = i;
319
+ if (isPreferredZero(i)) zeroCount++;
320
+ const start = Math.max(i - span + 1, lastNa + 1);
321
+ while (head < tail && queue[head]! < start) {
322
+ if (isPreferredZero(queue[head++]!)) zeroCount--;
323
+ }
324
+ if (head < tail) {
325
+ at[i] = queue[head]!;
326
+ zeros[i] = zeroCount > 0 ? 1 : 0;
327
+ }
328
+ }
329
+ return { at, zeros };
330
+ }
331
+
332
+ /** Offset of the best value of the window; on ties the oldest bar wins. 0 when the window is empty. */
333
+ function extremeOffset(source: Source, length: simple_int, lowest: boolean): series_int {
334
+ const { at } = windowExtremes(source, length, lowest);
335
+ // -(i - at): -0 when the extreme is the current bar, as the offset -j of the window scan
336
+ return source.map((_, i) => (i < length - 1 ? NaN : at[i]! < 0 ? 0 : -(i - at[i]!)));
337
+ }
338
+
339
+ /** Best value of the window (`na` when the window is empty), with the zero sign of `Math.max` / `Math.min`. */
340
+ function extremeValue(source: Source, length: simple_int, lowest: boolean): series_float {
341
+ const { at, zeros } = windowExtremes(source, length, lowest);
342
+ return source.map((_, i) => {
343
+ if (i < length - 1 || at[i]! < 0) return NaN;
344
+ const v = source[at[i]!]!;
345
+ return v === 0 && zeros[i] ? (lowest ? -0 : 0) : v;
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Crossover - returns true when series1 crosses over series2 (moves from below to above).
351
+ *
352
+ * @param series1 - First series
353
+ * @param series2 - Second series
354
+ * @returns Boolean series (true at crossover points)
355
+ *
356
+ * @remarks
357
+ * - True when: series1[i]! > series2[i]! AND series1[i-1] <= series2[i-1]
358
+ * - First value is always false (no previous value to compare)
359
+ * - Useful for detecting bullish signals (e.g., fast MA crossing over slow MA)
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * const crossUp = ta.crossover(fastMA, slowMA);
364
+ * ```
365
+ */
366
+ export function crossover(series1: Source, series2: Source): series_bool {
367
+ const result: series_bool = [];
368
+
369
+ for (let i = 0; i < series1.length; i++) {
370
+ if (i === 0) {
371
+ result.push(false);
372
+ } else {
373
+ result.push(series1[i]! > series2[i]! && series1[i - 1]! <= series2[i - 1]!);
374
+ }
375
+ }
376
+
377
+ return result;
378
+ }
379
+
380
+ /**
381
+ * Crossunder - returns true when series1 crosses under series2 (moves from above to below).
382
+ *
383
+ * @param series1 - First series
384
+ * @param series2 - Second series
385
+ * @returns Boolean series (true at crossunder points)
386
+ *
387
+ * @remarks
388
+ * - True when: series1[i]! < series2[i]! AND series1[i-1] >= series2[i-1]
389
+ * - First value is always false (no previous value to compare)
390
+ * - Useful for detecting bearish signals (e.g., fast MA crossing under slow MA)
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * const crossDown = ta.crossunder(fastMA, slowMA);
395
+ * ```
396
+ */
397
+ export function crossunder(series1: Source, series2: Source): series_bool {
398
+ const result: series_bool = [];
399
+
400
+ for (let i = 0; i < series1.length; i++) {
401
+ if (i === 0) {
402
+ result.push(false);
403
+ } else {
404
+ result.push(series1[i]! < series2[i]! && series1[i - 1]! >= series2[i - 1]!);
405
+ }
406
+ }
407
+
408
+ return result;
409
+ }
410
+
411
+ /**
412
+ * Change - calculates the difference between the current value and its value length bars ago.
413
+ *
414
+ * @param source - Series of values to process
415
+ * @param length - Number of bars back (default: 1)
416
+ * @returns Change series (source[i]! - source[i - length])
417
+ *
418
+ * @remarks
419
+ * - Returns NaN for the first `length` values
420
+ * - Default length is 1 (difference from previous bar)
421
+ * - Positive values indicate increase, negative values indicate decrease
422
+ *
423
+ * @example
424
+ * ```typescript
425
+ * const change1 = ta.change(closePrices); // Daily change
426
+ * const change5 = ta.change(closePrices, 5); // 5-day change
427
+ * ```
428
+ */
429
+ export function change(source: Source, length: simple_int = 1): series_float {
430
+ const result: series_float = [];
431
+
432
+ for (let i = 0; i < source.length; i++) {
433
+ if (i < length) {
434
+ result.push(NaN);
435
+ } else {
436
+ result.push(source[i]! - source[i - length]!);
437
+ }
438
+ }
439
+
440
+ return result;
441
+ }
442
+
443
+ /**
444
+ * True Range - measures market volatility by calculating the greatest of three price ranges.
445
+ *
446
+ * @param handle_na - Defines how the function calculates when previous close is na (default: false)
447
+ * @param high - High price series (required when not using context API)
448
+ * @param low - Low price series (required when not using context API)
449
+ * @param close - Close price series (required when not using context API)
450
+ * @returns True range series
451
+ *
452
+ * @remarks
453
+ * - **PineScript v6 signature**: `ta.tr(handle_na?)` - uses implicit chart data
454
+ * - **JavaScript signature**: Requires explicit `high`, `low`, `close` OR use `createContext()`
455
+ * - True Range = max(high - low, abs(high - close[1]), abs(low - close[1]))
456
+ * - When `handle_na` is true: returns `high - low` if previous close is na
457
+ * - When `handle_na` is false: returns na if previous close is na
458
+ * - Used as a component in ATR calculations
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * // Direct call with explicit data
463
+ * const trueRange = ta.tr(false, high, low, close);
464
+ *
465
+ * // Or use context API for cleaner syntax
466
+ * const { ta } = createContext({ chart: { high, low, close } });
467
+ * const trueRange = ta.tr(); // Matches PineScript!
468
+ * ```
469
+ */
470
+ export function tr(
471
+ handle_na: simple_bool = false,
472
+ high?: Source,
473
+ low?: Source,
474
+ close?: Source
475
+ ): series_float {
476
+ if (!high || !low || !close) {
477
+ throw new Error(
478
+ 'ta.tr() requires high, low, and close series. ' +
479
+ 'Either pass them explicitly or use createContext({ chart: { high, low, close } }) for implicit data.'
480
+ );
481
+ }
482
+
483
+ const result: series_float = [];
484
+
485
+ for (let i = 0; i < high.length; i++) {
486
+ if (i === 0) {
487
+ // First bar: the previous close is na
488
+ result.push(handle_na ? high[i]! - low[i]! : NaN);
489
+ } else {
490
+ const prevClose = close[i - 1]!;
491
+
492
+ // Handle na previous close based on handle_na parameter
493
+ if (isNaN(prevClose)) {
494
+ if (handle_na) {
495
+ result.push(high[i]! - low[i]!);
496
+ } else {
497
+ result.push(NaN);
498
+ }
499
+ } else {
500
+ const tr = Math.max(
501
+ high[i]! - low[i]!,
502
+ Math.abs(high[i]! - prevClose),
503
+ Math.abs(low[i]! - prevClose)
504
+ );
505
+ result.push(tr);
506
+ }
507
+ }
508
+ }
509
+
510
+ return result;
511
+ }
512
+
513
+ /**
514
+ * Average True Range - returns the RMA (Relative Moving Average) of true range.
515
+ *
516
+ * @param length - Number of bars (length)
517
+ * @param high - High price series (required when not using context API)
518
+ * @param low - Low price series (required when not using context API)
519
+ * @param close - Close price series (required when not using context API)
520
+ * @returns Average true range series
521
+ *
522
+ * @remarks
523
+ * - **PineScript v6 signature**: `ta.atr(length)` - uses implicit chart data
524
+ * - **JavaScript signature**: Requires explicit `high`, `low`, `close` OR use `createContext()`
525
+ * - Uses `ta.rma()` (Relative Moving Average) for smoothing, matching PineScript v6
526
+ * - True range is max(high - low, abs(high - close[1]), abs(low - close[1]))
527
+ * - ATR is a measure of volatility, higher values indicate greater volatility
528
+ * - `na` values in the source series are ignored
529
+ * - The function calculates on the `length` quantity of non-`na` values
530
+ *
531
+ * @example
532
+ * ```typescript
533
+ * // Direct call with explicit data
534
+ * const atr14 = ta.atr(14, high, low, close);
535
+ *
536
+ * // Or use context API for cleaner syntax
537
+ * const { ta } = createContext({ chart: { high, low, close } });
538
+ * const atr14 = ta.atr(14); // Matches PineScript!
539
+ * ```
540
+ */
541
+ export function atr(length: simple_int, high?: Source, low?: Source, close?: Source): series_float {
542
+ if (!high || !low || !close) {
543
+ throw new Error(
544
+ 'ta.atr() requires high, low, and close series. ' +
545
+ 'Either pass them explicitly or use createContext({ chart: { high, low, close } }) for implicit data.'
546
+ );
547
+ }
548
+
549
+ // PineScript: ta.atr uses ta.tr(true), so bar 0 is high - low
550
+ const trueRange = tr(true, high, low, close);
551
+ return rma(trueRange, length);
552
+ }
553
+
554
+ /**
555
+ * SuperTrend Indicator - a trend-following indicator that helps identify trend direction.
556
+ *
557
+ * @param factor - The multiplier by which the ATR will get multiplied (series int/float)
558
+ * @param atrPeriod - Length of ATR (simple int)
559
+ * @param high - High price series (required when not using context API)
560
+ * @param low - Low price series (required when not using context API)
561
+ * @param close - Close price series (required when not using context API)
562
+ * @param wicks - Whether to use wicks for trend reversal (NOT in PineScript v6 API, default: false)
563
+ * @returns Tuple of [supertrend, direction] where direction is 1 (downtrend) or -1 (uptrend)
564
+ *
565
+ * @remarks
566
+ * - **PineScript v6 signature**: `ta.supertrend(factor, atrPeriod)` - uses implicit chart data
567
+ * - **JavaScript signature**: Requires explicit `high`, `low`, `close` OR use `createContext()`
568
+ * - The `wicks` parameter is NOT part of the official PineScript v6 API
569
+ * - Direction: 1 = downtrend (red), -1 = uptrend (green)
570
+ * - Uses hl2 (average of high and low) as the source
571
+ * - SuperTrend helps identify the current market trend and potential reversal points
572
+ *
573
+ * @example
574
+ * ```typescript
575
+ * // Direct call with explicit data
576
+ * const [supertrend, direction] = ta.supertrend(3, 10, high, low, close);
577
+ * // Plot uptrend when direction < 0
578
+ * // Plot downtrend when direction > 0
579
+ *
580
+ * // Or use context API for cleaner syntax
581
+ * const { ta } = createContext({ chart: { high, low, close } });
582
+ * const [supertrend, direction] = ta.supertrend(3, 10); // Matches PineScript!
583
+ * ```
584
+ */
585
+ export function supertrend(
586
+ factor: simple_float,
587
+ atrPeriod: simple_int,
588
+ high?: Source,
589
+ low?: Source,
590
+ close?: Source,
591
+ wicks: simple_bool = false
592
+ ): [series_float, series_int] {
593
+ if (!high || !low || !close) {
594
+ throw new Error(
595
+ 'ta.supertrend() requires high, low, and close series. ' +
596
+ 'Either pass them explicitly or use createContext({ chart: { high, low, close } }) for implicit data.'
597
+ );
598
+ }
599
+ const supertrendValues: series_float = [];
600
+ const directions: series_int = [];
601
+
602
+ // Calculate hl2 (average of high and low)
603
+ const source: series_float = [];
604
+ for (let i = 0; i < high.length; i++) {
605
+ source.push((high[i]! + low[i]!) / 2);
606
+ }
607
+
608
+ // Calculate ATR
609
+ const atrValues = atr(atrPeriod, high, low, close);
610
+
611
+ // Track previous values across iterations
612
+ let prevLowerBand = NaN;
613
+ let prevUpperBand = NaN;
614
+ let prevSuperTrend = NaN;
615
+
616
+ for (let i = 0; i < source.length; i++) {
617
+ const atrValue = atrValues[i]! * factor;
618
+
619
+ // Skip calculation if ATR is not available yet
620
+ if (isNaN(atrValue)) {
621
+ supertrendValues.push(NaN);
622
+ directions.push(1);
623
+ continue;
624
+ }
625
+
626
+ // Calculate initial bands
627
+ let upperBand = source[i]! + atrValue;
628
+ let lowerBand = source[i]! - atrValue;
629
+
630
+ // Determine which price to use for comparison
631
+ const highPrice = wicks ? high[i]! : close[i]!;
632
+ const lowPrice = wicks ? low[i]! : close[i]!;
633
+ const prevLowPrice = i > 0 ? (wicks ? low[i - 1]! : close[i - 1]!) : 0;
634
+ const prevHighPrice = i > 0 ? (wicks ? high[i - 1]! : close[i - 1]!) : 0;
635
+
636
+ // Update bands conditionally (trailing behavior) - only if previous bands are valid
637
+ if (i > 0 && !isNaN(prevLowerBand) && !isNaN(prevUpperBand)) {
638
+ lowerBand = (lowerBand > prevLowerBand || prevLowPrice < prevLowerBand) ? lowerBand : prevLowerBand;
639
+ upperBand = (upperBand < prevUpperBand || prevHighPrice > prevUpperBand) ? upperBand : prevUpperBand;
640
+ }
641
+
642
+ // Determine trend direction
643
+ let currentDirection: int;
644
+ if (isNaN(prevSuperTrend)) {
645
+ // Initial direction when we don't have previous supertrend
646
+ currentDirection = 1;
647
+ } else if (prevSuperTrend === prevUpperBand) {
648
+ // Was in downtrend (following upper band)
649
+ currentDirection = highPrice > upperBand ? -1 : 1;
650
+ } else {
651
+ // Was in uptrend (following lower band)
652
+ currentDirection = lowPrice < lowerBand ? 1 : -1;
653
+ }
654
+
655
+ // Calculate supertrend value based on direction
656
+ const superTrendValue = currentDirection === -1 ? lowerBand : upperBand;
657
+
658
+ supertrendValues.push(superTrendValue);
659
+ directions.push(currentDirection);
660
+
661
+ // Update previous values for next iteration
662
+ prevLowerBand = lowerBand;
663
+ prevUpperBand = upperBand;
664
+ prevSuperTrend = superTrendValue;
665
+ }
666
+
667
+ return [supertrendValues, directions];
668
+ }
669
+ /**
670
+ * Relative Moving Average (RMA) - exponentially weighted moving average with alpha = 1 / length.
671
+ *
672
+ * @param source - Series of values to process
673
+ * @param length - Number of bars (length)
674
+ * @returns RMA of source for length bars back
675
+ *
676
+ * @remarks
677
+ * - Moving average used in RSI calculation
678
+ * - Alpha = 1 / length (different from EMA which uses alpha = 2 / (length + 1))
679
+ * - First value is initialized with SMA, then uses exponential smoothing
680
+ * - Formula: `RMA = (source + (length - 1) * RMA[1]) / length`, evaluated in this order as in PineScript
681
+ * (equal to `alpha * source + (1 - alpha) * RMA[1]`, but bit for bit)
682
+ * - `na` values (also +/-Infinity) in the source series are ignored
683
+ * - The function calculates on the `length` quantity of non-`na` values
684
+ *
685
+ * @example
686
+ * ```typescript
687
+ * const rma14 = ta.rma(closePrices, 14);
688
+ * // Used internally by RSI:
689
+ * // avgGain = ta.rma(gains, 14);
690
+ * // avgLoss = ta.rma(losses, 14);
691
+ * ```
692
+ */
693
+ export function rma(source: Source, length: simple_int): series_float {
694
+ const result: series_float = [];
695
+ // Floor the length to match PineScript's auto-truncation of float to int
696
+ const len = Math.floor(length);
697
+
698
+ // Find the first index where we have enough non-na values for SMA initialization.
699
+ // An infinite value is na, as in PineScript.
700
+ let firstValidIndex = -1;
701
+ let validCount = 0;
702
+ let initSum = 0;
703
+
704
+ for (let i = 0; i < source.length; i++) {
705
+ const val = source[i];
706
+ if (val !== undefined && Number.isFinite(val)) {
707
+ initSum += val;
708
+ validCount++;
709
+ if (validCount === len) {
710
+ firstValidIndex = i;
711
+ break;
712
+ }
713
+ }
714
+ }
715
+
716
+ // Initialize RMA value with SMA of first `length` non-NaN values
717
+ let rmaValue = validCount > 0 ? initSum / validCount : NaN;
718
+ const rmaInitialized = firstValidIndex >= 0;
719
+
720
+ for (let i = 0; i < source.length; i++) {
721
+ if (!rmaInitialized || i < firstValidIndex) {
722
+ // Not enough data yet for RMA
723
+ result.push(NaN);
724
+ } else if (i === firstValidIndex) {
725
+ // First valid RMA value
726
+ result.push(rmaValue);
727
+ } else {
728
+ const val = source[i];
729
+ if (val !== undefined && Number.isFinite(val)) {
730
+ // PineScript evaluates (source + (length - 1) * RMA[1]) / length; the form
731
+ // alpha * source + (1 - alpha) * RMA[1] differs in the last bits
732
+ rmaValue = (val + (len - 1) * rmaValue) / len;
733
+ result.push(rmaValue);
734
+ } else {
735
+ // na source: na on this bar; the next bar continues from the last value (as in PineScript)
736
+ result.push(NaN);
737
+ }
738
+ }
739
+ }
740
+
741
+ return result;
742
+ }
743
+
744
+ /**
745
+ * Weighted Moving Average (WMA) - moving average with linearly decreasing weights.
746
+ *
747
+ * @param source - Series of values to process
748
+ * @param length - Number of bars (length)
749
+ * @returns WMA of source for length bars back
750
+ *
751
+ * @remarks
752
+ * - Weighting factors decrease in arithmetical progression
753
+ * - Most recent value has weight `length`, previous has `length-1`, etc.
754
+ * - Formula: `sum(source[i]! * (length - i)) / sum(length - i)` for i = 0 to length-1
755
+ * - As in PineScript: na on a bar whose source is `na` (also +/-Infinity) and until `length` non-`na` values were
756
+ * received; an `na` value inside the window is replaced by the previous non-`na` value
757
+ * - More responsive to recent price changes than SMA
758
+ *
759
+ * @example
760
+ * ```typescript
761
+ * const wma20 = ta.wma(closePrices, 20);
762
+ * // WMA gives more weight to recent prices
763
+ * ```
764
+ */
765
+ export function wma(source: Source, length: simple_int): series_float {
766
+ const result: series_float = [];
767
+ // Floor the length to match PineScript's auto-truncation of float to int
768
+ const len = Math.floor(length);
769
+ // PineScript: na on a bar with an na source (also +/-Infinity) and until `len` non-na values were received;
770
+ // otherwise the last `len` bars, each na replaced by the previous non-na value
771
+ const filled: number[] = [];
772
+ let last = NaN;
773
+ let count = 0;
774
+ for (let i = 0; i < source.length; i++) {
775
+ const v = source[i];
776
+ const isNa = v === undefined || v === null || !Number.isFinite(v);
777
+ if (!isNa) {
778
+ last = v;
779
+ count++;
780
+ }
781
+ filled.push(last);
782
+ if (isNa || count < len) {
783
+ result.push(NaN);
784
+ continue;
785
+ }
786
+ // Summed from the oldest bar (weight 1) to the newest (weight len), as PineScript (bit for bit)
787
+ let sum = 0;
788
+ let weightSum = 0;
789
+ for (let j = len - 1; j >= 0; j--) {
790
+ const weight = len - j;
791
+ sum += filled[i - j]! * weight;
792
+ weightSum += weight;
793
+ }
794
+ result.push(sum / weightSum);
795
+ }
796
+ return result;
797
+ }
798
+
799
+ /**
800
+ * Highest value over the last `length` bars.
801
+ *
802
+ * @param source - Series of values to process
803
+ * @param length - Number of bars
804
+ * @returns Highest value series (na on the first `length - 1` bars)
805
+ *
806
+ * @remarks
807
+ * As in PineScript: an `na` value ends the window, so only the values after the last `na`
808
+ * count, and a bar whose own value is `na` gives `na`.
809
+ * PineScript `ta.highest(length)` (source = high) is `ta.highest(length)` in the script API.
810
+ */
811
+ export function highest(source: Source, length: simple_int): series_float {
812
+ return extremeValue(source, length, false);
813
+ }
814
+
815
+ /**
816
+ * Lowest value over the last `length` bars.
817
+ *
818
+ * @param source - Series of values to process
819
+ * @param length - Number of bars
820
+ * @returns Lowest value series (na on the first `length - 1` bars)
821
+ *
822
+ * @remarks
823
+ * As in PineScript: an `na` value ends the window, so only the values after the last `na`
824
+ * count, and a bar whose own value is `na` gives `na`.
825
+ */
826
+ export function lowest(source: Source, length: simple_int): series_float {
827
+ return extremeValue(source, length, true);
828
+ }
829
+
830
+ /**
831
+ * Cumulative Sum - returns the total sum of all elements from the beginning.
832
+ *
833
+ * @param source - Series of values to process
834
+ * @returns Series containing the cumulative sum at each bar
835
+ *
836
+ * @remarks
837
+ * - Returns the running total of all values from index 0 to current index
838
+ * - As in PineScript: an `na` value adds nothing to the sum,
839
+ * and the result is `na` on that bar only; the sum continues on the next bars
840
+ * - Useful for calculating total volume, total trades, etc.
841
+ *
842
+ * @example
843
+ * ```typescript
844
+ * const cumulativeVolume = ta.cum(volume);
845
+ * const totalGains = ta.cum(gains);
846
+ * ```
847
+ */
848
+ export function cum(source: Source): series_float {
849
+ let sum = 0;
850
+ return source.map((v) => {
851
+ // PineScript: an na value (also +/-Infinity) gives na on its bar and is not added
852
+ if (!Number.isFinite(v)) return NaN;
853
+ sum += v;
854
+ return sum;
855
+ });
856
+ }
857
+
858
+ /**
859
+ * Cross - returns true when two series cross each other (either direction).
860
+ *
861
+ * @param source1 - First series
862
+ * @param source2 - Second series
863
+ * @returns Boolean series (true at cross points)
864
+ *
865
+ * @remarks
866
+ * - True when: (source1[i]! > source2[i]! AND source1[i-1] <= source2[i-1]) OR
867
+ * (source1[i]! < source2[i]! AND source1[i-1] >= source2[i-1])
868
+ * - First value is always false (no previous value to compare)
869
+ * - Detects any crossing (either over or under)
870
+ * - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
871
+ *
872
+ * @example
873
+ * ```typescript
874
+ * const crossed = ta.cross(fastMA, slowMA);
875
+ * // Detect any MA crossover
876
+ * ```
877
+ */
878
+ export function cross(source1: Source, source2: Source): series_bool {
879
+ const result: series_bool = [];
880
+
881
+ for (let i = 0; i < source1.length; i++) {
882
+ if (i === 0) {
883
+ result.push(false);
884
+ } else {
885
+ const crossedUp = source1[i]! > source2[i]! && source1[i - 1]! <= source2[i - 1]!;
886
+ const crossedDown = source1[i]! < source2[i]! && source1[i - 1]! >= source2[i - 1]!;
887
+ result.push(crossedUp || crossedDown);
888
+ }
889
+ }
890
+
891
+ return result;
892
+ }
893
+
894
+ /**
895
+ * ta.rising / ta.falling: true when each of the `length` steps between the last `length + 1` non-na values (up to
896
+ * the current bar) passes `step(newer, older)`; false when there are fewer non-na values.
897
+ *
898
+ * @internal
899
+ */
900
+ function monotonic(source: Source, length: simple_int, step: (newer: number, older: number) => boolean): series_bool {
901
+ const result: series_bool = [];
902
+ const values: number[] = []; // non-na values so far
903
+ for (let i = 0; i < source.length; i++) {
904
+ const v = source[i]!;
905
+ if (!Number.isNaN(v)) values.push(v);
906
+ const n = values.length;
907
+ let ok = n > length;
908
+ for (let j = 1; ok && j <= length; j++) ok = step(values[n - j]!, values[n - j - 1]!);
909
+ result.push(ok);
910
+ }
911
+ return result;
912
+ }
913
+
914
+ /**
915
+ * Rising - returns true if source is rising for length bars.
916
+ *
917
+ * @param source - Series of values to process
918
+ * @param length - Number of bars (length)
919
+ * @returns Boolean series (true when rising)
920
+ *
921
+ * @remarks
922
+ * PineScript rules:
923
+ * - each of the `length` steps is a rise larger than the 1e-10 tolerance
924
+ * - `na` values are skipped: the steps are between the last `length + 1` non-`na` values up to the current bar
925
+ * (on an `na` bar, the result of the last non-`na` bar)
926
+ * - false while there are fewer than `length + 1` non-`na` values
927
+ *
928
+ * @example
929
+ * ```typescript
930
+ * const isRising = ta.rising(close, 3);
931
+ * // Detect upward momentum
932
+ * ```
933
+ */
934
+ export function rising(source: Source, length: simple_int): series_bool {
935
+ return monotonic(source, length, gt);
936
+ }
937
+
938
+ /**
939
+ * Falling - returns true if source is falling for length bars.
940
+ *
941
+ * @param source - Series of values to process
942
+ * @param length - Number of bars (length)
943
+ * @returns Boolean series (true when falling)
944
+ *
945
+ * @remarks
946
+ * PineScript rules:
947
+ * - each of the `length` steps is a fall larger than the 1e-10 tolerance
948
+ * - `na` values are skipped: the steps are between the last `length + 1` non-`na` values up to the current bar
949
+ * (on an `na` bar, the result of the last non-`na` bar)
950
+ * - false while there are fewer than `length + 1` non-`na` values
951
+ *
952
+ * @example
953
+ * ```typescript
954
+ * const isFalling = ta.falling(close, 3);
955
+ * // Detect downward momentum
956
+ * ```
957
+ */
958
+ export function falling(source: Source, length: simple_int): series_bool {
959
+ return monotonic(source, length, lt);
960
+ }
961
+
962
+ /**
963
+ * Rate of Change (ROC) - percentage change between current value and value length bars ago.
964
+ *
965
+ * @param source - Series of values to process
966
+ * @param length - Number of bars (length)
967
+ * @returns ROC series as percentage
968
+ *
969
+ * @remarks
970
+ * - Formula: `100 * change(source, length) / source[length]`
971
+ * - Equivalent to: `100 * (source - source[length]) / source[length]`
972
+ * - Returns percentage change, e.g., 5.0 means 5% increase
973
+ * - `na` values in the source series are included in calculations and will produce an `na` result
974
+ * - Useful for momentum analysis and trend strength measurement
975
+ *
976
+ * @example
977
+ * ```typescript
978
+ * const roc10 = ta.roc(closePrices, 10);
979
+ * // Positive ROC indicates upward momentum
980
+ * // Negative ROC indicates downward momentum
981
+ * ```
982
+ */
983
+ export function roc(source: Source, length: simple_int): series_float {
984
+ const result: series_float = [];
985
+
986
+ for (let i = 0; i < source.length; i++) {
987
+ if (i < length) {
988
+ result.push(NaN);
989
+ } else {
990
+ const oldValue = source[i - length]!;
991
+ if (oldValue === 0 || isNaN(oldValue) || isNaN(source[i]!)) {
992
+ result.push(NaN);
993
+ } else {
994
+ const changeValue = source[i]! - oldValue;
995
+ result.push((100 * changeValue) / oldValue);
996
+ }
997
+ }
998
+ }
999
+
1000
+ return result;
1001
+ }
1002
+
1003
+ /**
1004
+ * Momentum (MOM) - difference between current value and value length bars ago.
1005
+ *
1006
+ * @param source - Series of values to process
1007
+ * @param length - Offset from current bar to previous bar
1008
+ * @returns Momentum series
1009
+ *
1010
+ * @remarks
1011
+ * - Formula: `source - source[length]`
1012
+ * - Equivalent to `ta.change(source, length)`
1013
+ * - Positive momentum indicates upward movement
1014
+ * - Negative momentum indicates downward movement
1015
+ * - `na` values in the source series are included in calculations and will produce an `na` result
1016
+ *
1017
+ * @example
1018
+ * ```typescript
1019
+ * const mom10 = ta.mom(closePrices, 10);
1020
+ * // Measures raw price momentum over 10 bars
1021
+ * ```
1022
+ */
1023
+ export function mom(source: Source, length: simple_int): series_float {
1024
+ const result: series_float = [];
1025
+
1026
+ for (let i = 0; i < source.length; i++) {
1027
+ if (i < length) {
1028
+ result.push(NaN);
1029
+ } else {
1030
+ if (isNaN(source[i]!) || isNaN(source[i - length]!)) {
1031
+ result.push(NaN);
1032
+ } else {
1033
+ result.push(source[i]! - source[i - length]!);
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ return result;
1039
+ }
1040
+
1041
+ /**
1042
+ * Mean Absolute Deviation - measure of difference between series and its SMA.
1043
+ *
1044
+ * @param source - Series of values to process
1045
+ * @param length - Number of bars (length)
1046
+ * @returns Mean absolute deviation series
1047
+ *
1048
+ * @remarks
1049
+ * - Measures average absolute distance from the mean
1050
+ * - Formula: `sum(abs(source[i]! - sma)) / length` for i in 0 to length-1
1051
+ * - Less sensitive to outliers than standard deviation
1052
+ * - `na` values in the source series are ignored
1053
+ * - The function calculates on the `length` quantity of non-`na` values
1054
+ *
1055
+ * @example
1056
+ * ```typescript
1057
+ * const dev10 = ta.dev(closePrices, 10);
1058
+ * // Measures volatility using mean absolute deviation
1059
+ * ```
1060
+ */
1061
+ export function dev(source: Source, length: simple_int): series_float {
1062
+ const result: series_float = [];
1063
+ // PineScript: na when the window of `length` bars holds an na value
1064
+ const meanValues = strictWindowMean(source, length);
1065
+
1066
+ for (let i = 0; i < source.length; i++) {
1067
+ if (i < length - 1 || isNaN(meanValues[i]!)) {
1068
+ result.push(NaN);
1069
+ } else {
1070
+ let sum = 0;
1071
+ for (let j = 0; j < length; j++) {
1072
+ if (!isNaN(source[i - j]!)) {
1073
+ sum += Math.abs(source[i - j]! - meanValues[i]!);
1074
+ }
1075
+ }
1076
+ result.push(sum / length);
1077
+ }
1078
+ }
1079
+
1080
+ return result;
1081
+ }
1082
+
1083
+ /**
1084
+ * Variance - expectation of squared deviation from mean.
1085
+ *
1086
+ * @param source - Series of values to process
1087
+ * @param length - Number of bars (length)
1088
+ * @param biased - Use biased (true) or unbiased (false) estimate (default: true)
1089
+ * @returns Variance series
1090
+ *
1091
+ * @remarks
1092
+ * - Measures how far values are spread out from their mean
1093
+ * - If `biased` is true: divides by `length` (population variance)
1094
+ * - If `biased` is false: divides by `length - 1` (sample variance)
1095
+ * - Formula (biased): `sum((source[i]! - mean)^2) / length`
1096
+ * - Formula (unbiased): `sum((source[i]! - mean)^2) / (length - 1)`
1097
+ * - `na` values in the source series are ignored
1098
+ * - The function calculates on the `length` quantity of non-`na` values
1099
+ * - Relationship: `stdev = sqrt(variance)`
1100
+ * - Computed as PineScript, bit for bit, from compensated running sums of the values and of their squares:
1101
+ * biased `sumSq / length - mean * mean`, unbiased `sumSq / (length - 1) - mean * sum / (length - 1)`
1102
+ *
1103
+ * @example
1104
+ * ```typescript
1105
+ * const variance20 = ta.variance(closePrices, 20);
1106
+ * const sampleVariance = ta.variance(closePrices, 20, false);
1107
+ * ```
1108
+ */
1109
+ export function variance(source: Source, length: simple_int, biased: simple_bool = true): series_float {
1110
+ const len = Math.floor(length);
1111
+ // PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
1112
+ if ((biased ? len : len - 1) <= 0) return source.map(() => NaN);
1113
+ return runningVariance(source, len, biased);
1114
+ }
1115
+
1116
+ /**
1117
+ * Median - returns the median (middle value) of the series.
1118
+ *
1119
+ * @param source - Series of values to process
1120
+ * @param length - Number of bars (length)
1121
+ * @returns Median series
1122
+ *
1123
+ * @remarks
1124
+ * - Returns the middle value when values are sorted
1125
+ * - For an even length, returns the average of the two middle values
1126
+ * - As in PineScript: `na` values are skipped, the window holds the last `length` non-na values
1127
+ * (going back as many bars as needed), and a bar whose own value is `na` still gets a result
1128
+ * - Returns NaN until `length` non-na values exist
1129
+ * - More robust to outliers than mean (SMA)
1130
+ *
1131
+ * @example
1132
+ * ```typescript
1133
+ * const median20 = ta.median(closePrices, 20);
1134
+ * // Median is less affected by extreme values than SMA
1135
+ * ```
1136
+ */
1137
+ export function median(source: Source, length: simple_int): series_float {
1138
+ const len = Math.floor(length);
1139
+ const n = source.length;
1140
+ const out: series_float = new Array<number>(n).fill(NaN);
1141
+ if (!(len >= 1)) return out;
1142
+ // The last `len` non-na values, sorted; equal values (also +0 / -0) from the newest to the oldest, which is the
1143
+ // order a stable sort of the window gives, so the middle values are the same numbers.
1144
+ const sorted: number[] = [];
1145
+ const kept = new Float64Array(n); // the non-na values in bar order
1146
+ let count = 0;
1147
+ /** First position whose value is >= v (`after`: > v). */
1148
+ const bound = (v: number, after: boolean): number => {
1149
+ let lo = 0;
1150
+ let hi = sorted.length;
1151
+ while (lo < hi) {
1152
+ const m = (lo + hi) >> 1;
1153
+ if (after ? sorted[m]! <= v : sorted[m]! < v) lo = m + 1;
1154
+ else hi = m;
1155
+ }
1156
+ return lo;
1157
+ };
1158
+ const mid = Math.floor(len / 2);
1159
+ for (let i = 0; i < n; i++) {
1160
+ const x = source[i]!;
1161
+ if (!Number.isNaN(x)) {
1162
+ kept[count++] = x;
1163
+ sorted.splice(bound(x, false), 0, x); // before the equal values: the newest first
1164
+ // the value leaving the window is the oldest, so the last of its equal values
1165
+ if (count > len) sorted.splice(bound(kept[count - 1 - len]!, true) - 1, 1);
1166
+ }
1167
+ if (count >= len) out[i] = len % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!;
1168
+ }
1169
+ return out;
1170
+ }
1171
+
1172
+ /**
1173
+ * Symmetrically Weighted Moving Average (SWMA) - fixed length 4 with symmetric weights.
1174
+ *
1175
+ * @param source - Series of values to process
1176
+ * @returns SWMA series
1177
+ *
1178
+ * @remarks
1179
+ * - Fixed length of 4 bars
1180
+ * - Weights: [1/6, 2/6, 2/6, 1/6] (symmetric)
1181
+ * - Formula: `source[3] * 1/6 + source[2] * 2/6 + source[1] * 2/6 + source[0] * 1/6`
1182
+ * - More weight given to middle values
1183
+ * - `na` values in the source series are included in calculations and will produce an `na` result
1184
+ * - Returns NaN for the first 3 bars
1185
+ *
1186
+ * @example
1187
+ * ```typescript
1188
+ * const swma = ta.swma(closePrices);
1189
+ * // Smoothed price with symmetric weighting
1190
+ * ```
1191
+ */
1192
+ export function swma(source: Source): series_float {
1193
+ const result: series_float = [];
1194
+
1195
+ for (let i = 0; i < source.length; i++) {
1196
+ if (i < 3) {
1197
+ result.push(NaN);
1198
+ } else {
1199
+ // Check for any NaN values in the window
1200
+ if (isNaN(source[i]!) || isNaN(source[i - 1]!) || isNaN(source[i - 2]!) || isNaN(source[i - 3]!)) {
1201
+ result.push(NaN);
1202
+ } else {
1203
+ const value =
1204
+ source[i - 3]! * (1 / 6) +
1205
+ source[i - 2]! * (2 / 6) +
1206
+ source[i - 1]! * (2 / 6) +
1207
+ source[i]! * (1 / 6);
1208
+ result.push(value);
1209
+ }
1210
+ }
1211
+ }
1212
+
1213
+ return result;
1214
+ }
1215
+
1216
+ /**
1217
+ * Volume Weighted Moving Average (VWMA) - moving average weighted by volume.
1218
+ *
1219
+ * @param source - Series of values to process
1220
+ * @param length - Number of bars (length)
1221
+ * @param volume - Volume series (required when not using context API)
1222
+ * @returns VWMA series
1223
+ *
1224
+ * @remarks
1225
+ * - **PineScript v6 signature**: `ta.vwma(source, length)` - uses implicit volume data
1226
+ * - **JavaScript signature**: Requires explicit `volume` OR use `createContext()`
1227
+ * - Formula: `sma(source * volume, length) / sma(volume, length)`
1228
+ * - Gives more weight to bars with higher volume
1229
+ * - `na` values in the source series are ignored
1230
+ * - Useful for price analysis considering volume significance
1231
+ *
1232
+ * @example
1233
+ * ```typescript
1234
+ * // Direct call with explicit volume
1235
+ * const vwma20 = ta.vwma(closePrices, 20, volumeData);
1236
+ *
1237
+ * // Or use context API for cleaner syntax
1238
+ * const { ta } = createContext({ chart: { high, low, close, volume } });
1239
+ * const vwma20 = ta.vwma(close, 20); // Matches PineScript!
1240
+ * ```
1241
+ */
1242
+ export function vwma(source: Source, length: simple_int, volume?: Source): series_float {
1243
+ if (!volume) {
1244
+ throw new Error(
1245
+ 'ta.vwma() requires volume series. ' +
1246
+ 'Either pass it explicitly or use createContext({ chart: { ..., volume } }) for implicit data.'
1247
+ );
1248
+ }
1249
+
1250
+ // Calculate source * volume
1251
+ const sourceTimesVolume: series_float = [];
1252
+ for (let i = 0; i < source.length; i++) {
1253
+ sourceTimesVolume.push(source[i]! * volume[i]!);
1254
+ }
1255
+
1256
+ const numerator = sma(sourceTimesVolume, length);
1257
+ const denominator = sma(volume, length);
1258
+
1259
+ const result: series_float = [];
1260
+ for (let i = 0; i < source.length; i++) {
1261
+ if (denominator[i]! === 0 || isNaN(denominator[i]!)) {
1262
+ result.push(NaN);
1263
+ } else {
1264
+ result.push(numerator[i]! / denominator[i]!);
1265
+ }
1266
+ }
1267
+
1268
+ return result;
1269
+ }
1270
+
1271
+ /**
1272
+ * Linear Regression - line that best fits prices using least squares method.
1273
+ *
1274
+ * @param source - Series of values to process
1275
+ * @param length - Number of bars (length)
1276
+ * @param offset - Offset (default: 0)
1277
+ * @returns Linear regression value
1278
+ *
1279
+ * @remarks
1280
+ * - Calculates line of best fit using least squares method
1281
+ * - Formula: `linreg = intercept + slope * (length - 1 - offset)`
1282
+ * - offset=0 gives current fitted value, offset<0 gives future projection
1283
+ * - `na` values in the source series are included in calculations and will produce an `na` result
1284
+ * - Useful for trend detection and prediction
1285
+ *
1286
+ * @example
1287
+ * ```typescript
1288
+ * const linreg20 = ta.linreg(closePrices, 20, 0);
1289
+ * const linregFuture = ta.linreg(closePrices, 20, -5); // Project 5 bars ahead
1290
+ * ```
1291
+ */
1292
+ export function linreg(source: Source, length: simple_int, offset: simple_int = 0): series_float {
1293
+ const result: series_float = [];
1294
+ // Floor the length to match PineScript's auto-truncation of float to int
1295
+ const len = Math.floor(length);
1296
+
1297
+ for (let i = 0; i < source.length; i++) {
1298
+ if (i < len - 1) {
1299
+ result.push(NaN);
1300
+ } else {
1301
+ // Check for NaN values in window
1302
+ let hasNaN = false;
1303
+ for (let j = 0; j < len; j++) {
1304
+ if (isNaN(source[i - j]!)) {
1305
+ hasNaN = true;
1306
+ break;
1307
+ }
1308
+ }
1309
+
1310
+ if (hasNaN) {
1311
+ result.push(NaN);
1312
+ } else {
1313
+ // Calculate least squares regression
1314
+ let sumX = 0;
1315
+ let sumY = 0;
1316
+ let sumXY = 0;
1317
+ let sumX2 = 0;
1318
+
1319
+ for (let j = 0; j < len; j++) {
1320
+ const x = j;
1321
+ const y = source[i - (len - 1 - j)]!;
1322
+ sumX += x;
1323
+ sumY += y;
1324
+ sumXY += x * y;
1325
+ sumX2 += x * x;
1326
+ }
1327
+
1328
+ const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
1329
+ const intercept = (sumY - slope * sumX) / len;
1330
+
1331
+ // Calculate linreg value at offset
1332
+ const x = len - 1 - offset;
1333
+ result.push(intercept + slope * x);
1334
+ }
1335
+ }
1336
+ }
1337
+
1338
+ return result;
1339
+ }
1340
+
1341
+ /**
1342
+ * Correlation Coefficient - measures degree to which two series deviate from their means together.
1343
+ *
1344
+ * @param source1 - First series
1345
+ * @param source2 - Second series
1346
+ * @param length - Number of bars (length)
1347
+ * @returns Correlation coefficient (-1 to +1)
1348
+ *
1349
+ * @remarks
1350
+ * - Returns value between -1 and +1
1351
+ * - +1 = perfect positive correlation
1352
+ * - -1 = perfect negative correlation
1353
+ * - 0 = no correlation
1354
+ * - Measures linear relationship between two series
1355
+ * - `na` values in the source series are ignored
1356
+ * - The function calculates on the `length` quantity of non-`na` values
1357
+ *
1358
+ * @example
1359
+ * ```typescript
1360
+ * const corr = ta.correlation(series1, series2, 20);
1361
+ * // Values close to +1 or -1 indicate strong relationship
1362
+ * ```
1363
+ */
1364
+ export function correlation(source1: Source, source2: Source, length: simple_int): series_float {
1365
+ const result: series_float = [];
1366
+ // the non-NaN pairs of the window, newest first (reused on every bar)
1367
+ const span = length > 0 ? Math.ceil(length) : 0;
1368
+ const a = new Float64Array(span);
1369
+ const b = new Float64Array(span);
1370
+
1371
+ for (let i = 0; i < source1.length; i++) {
1372
+ if (i < length - 1) {
1373
+ result.push(NaN);
1374
+ continue;
1375
+ }
1376
+ let m = 0;
1377
+ for (let j = 0; j < length; j++) {
1378
+ const v1 = source1[i - j]!;
1379
+ const v2 = source2[i - j]!;
1380
+ if (!isNaN(v1) && !isNaN(v2)) {
1381
+ a[m] = v1;
1382
+ b[m] = v2;
1383
+ m++;
1384
+ }
1385
+ }
1386
+ if (m === 0) {
1387
+ result.push(NaN);
1388
+ continue;
1389
+ }
1390
+
1391
+ // Calculate means
1392
+ let sum1 = 0;
1393
+ let sum2 = 0;
1394
+ for (let k = 0; k < m; k++) {
1395
+ sum1 += a[k]!;
1396
+ sum2 += b[k]!;
1397
+ }
1398
+ const mean1 = sum1 / m;
1399
+ const mean2 = sum2 / m;
1400
+
1401
+ // Calculate correlation components
1402
+ let numerator = 0;
1403
+ let sum1Sq = 0;
1404
+ let sum2Sq = 0;
1405
+ for (let k = 0; k < m; k++) {
1406
+ const dev1 = a[k]! - mean1;
1407
+ const dev2 = b[k]! - mean2;
1408
+ numerator += dev1 * dev2;
1409
+ sum1Sq += dev1 * dev1;
1410
+ sum2Sq += dev2 * dev2;
1411
+ }
1412
+
1413
+ const denominator = Math.sqrt(sum1Sq * sum2Sq);
1414
+ result.push(denominator === 0 ? NaN : numerator / denominator);
1415
+ }
1416
+
1417
+ return result;
1418
+ }
1419
+
1420
+ /**
1421
+ * Percent Rank - percentage of how many previous values were less than or equal to current value.
1422
+ *
1423
+ * @param source - Series of values to process
1424
+ * @param length - Number of bars (length)
1425
+ * @returns Percent rank (0 to 100)
1426
+ *
1427
+ * @remarks
1428
+ * - Returns value between 0 and 100
1429
+ * - 0 = current value is lowest in the period
1430
+ * - 100 = current value is highest in the period
1431
+ * - 50 = current value is at median
1432
+ * - Useful for identifying relative strength within a period
1433
+ *
1434
+ * PineScript rules:
1435
+ * - percentrank = count(source[k] <= source, k = 1..length) / length * 100
1436
+ * - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
1437
+ * `<=` the current value
1438
+ * - the comparison uses the 1e-10 tolerance
1439
+ *
1440
+ * @example
1441
+ * ```typescript
1442
+ * const pctrank = ta.percentrank(closePrices, 100);
1443
+ * // Values near 100 indicate recent strength
1444
+ * // Values near 0 indicate recent weakness
1445
+ * ```
1446
+ */
1447
+ export function percentrank(source: Source, length: simple_int): series_float {
1448
+ const result: series_float = [];
1449
+
1450
+ for (let i = 0; i < source.length; i++) {
1451
+ const currentValue = source[i]!;
1452
+ if (i < length || Number.isNaN(currentValue)) {
1453
+ result.push(NaN);
1454
+ continue;
1455
+ }
1456
+ // le() is false for an na value in the window
1457
+ let countLessOrEqual = 0;
1458
+ for (let j = 1; j <= length; j++) {
1459
+ if (le(source[i - j]!, currentValue)) countLessOrEqual++;
1460
+ }
1461
+ result.push((countLessOrEqual / length) * 100);
1462
+ }
1463
+
1464
+ return result;
1465
+ }
1466
+
1467
+ /**
1468
+ * Commodity Channel Index (CCI) - measures deviation from average price.
1469
+ *
1470
+ * @param source - Series of values to process
1471
+ * @param length - Number of bars (length)
1472
+ * @returns CCI series
1473
+ *
1474
+ * @remarks
1475
+ * - CCI = (Typical Price - SMA of TP) / (0.015 * Mean Deviation)
1476
+ * - Typical Price = (High + Low + Close) / 3
1477
+ * - Mean Deviation = Average of absolute differences from mean
1478
+ * - Scaled by 0.015 to provide more readable numbers
1479
+ * - Values above +100 indicate overbought conditions
1480
+ * - Values below -100 indicate oversold conditions
1481
+ * - \`na\` values in the source series are ignored
1482
+ *
1483
+ * @example
1484
+ * \`\`\`typescript
1485
+ * const cci20 = ta.cci(typicalPrice, 20);
1486
+ * // Overbought when cci > 100
1487
+ * // Oversold when cci < -100
1488
+ * \`\`\`
1489
+ */
1490
+ export function cci(source: Source, length: simple_int): series_float {
1491
+ const result: series_float = [];
1492
+ const smaValues = sma(source, length);
1493
+ const devValues = dev(source, length);
1494
+
1495
+ for (let i = 0; i < source.length; i++) {
1496
+ if (isNaN(smaValues[i]!) || isNaN(devValues[i]!) || devValues[i]! === 0) {
1497
+ result.push(NaN);
1498
+ } else {
1499
+ const cci = (source[i]! - smaValues[i]!) / (0.015 * devValues[i]!);
1500
+ result.push(cci);
1501
+ }
1502
+ }
1503
+
1504
+ return result;
1505
+ }
1506
+
1507
+ /**
1508
+ * Stochastic Oscillator - momentum indicator comparing closing price to price range.
1509
+ *
1510
+ * @param source - Source series (typically close)
1511
+ * @param high - High price series
1512
+ * @param low - Low price series
1513
+ * @param length - Number of bars (length)
1514
+ * @returns Stochastic %K series (values range from 0 to 100)
1515
+ *
1516
+ * @remarks
1517
+ * - Formula: 100 * (close - lowest(low, length)) / (highest(high, length) - lowest(low, length))
1518
+ * - Measures where close is relative to the high-low range
1519
+ * - Values above 80 typically indicate overbought
1520
+ * - Values below 20 typically indicate oversold
1521
+ * - Returns NaN when range is zero (high equals low)
1522
+ * - \`na\` values in the source series are ignored
1523
+ *
1524
+ * @example
1525
+ * \`\`\`typescript
1526
+ * const stochK = ta.stoch(close, high, low, 14);
1527
+ * // Smooth with SMA for %D line:
1528
+ * const stochD = ta.sma(stochK, 3);
1529
+ * \`\`\`
1530
+ */
1531
+ export function stoch(source: Source, high: Source, low: Source, length: simple_int): series_float {
1532
+ const result: series_float = [];
1533
+ const lowestValues = lowest(low, length);
1534
+ const highestValues = highest(high, length);
1535
+
1536
+ for (let i = 0; i < source.length; i++) {
1537
+ if (isNaN(lowestValues[i]!) || isNaN(highestValues[i]!)) {
1538
+ result.push(NaN);
1539
+ } else {
1540
+ const range = highestValues[i]! - lowestValues[i]!;
1541
+ if (range === 0) {
1542
+ result.push(NaN);
1543
+ } else {
1544
+ const stochValue = 100 * (source[i]! - lowestValues[i]!) / range;
1545
+ result.push(stochValue);
1546
+ }
1547
+ }
1548
+ }
1549
+
1550
+ return result;
1551
+ }
1552
+
1553
+
1554
+ /**
1555
+ * Money Flow Index (MFI) - volume-weighted RSI measuring buying and selling pressure.
1556
+ *
1557
+ * @param source - Source series (typically hlc3 or close)
1558
+ * @param length - Number of bars (length)
1559
+ * @param volume - Volume series (required when not using context API)
1560
+ * @returns MFI series (values range from 0 to 100)
1561
+ *
1562
+ * @remarks
1563
+ * - **PineScript v6 signature**: `ta.mfi(source, length)` - uses implicit volume data
1564
+ * - **JavaScript signature**: Requires explicit `volume` OR use `createContext()`
1565
+ * - Combines price and volume to identify overbought/oversold conditions
1566
+ * - Formula: 100 - (100 / (1 + Positive Money Flow / Negative Money Flow))
1567
+ * - Values above 80 indicate overbought
1568
+ * - Values below 20 indicate oversold
1569
+ * - `na` values in the source series are ignored
1570
+ *
1571
+ * @example
1572
+ * ```typescript
1573
+ * // Direct call with explicit volume
1574
+ * const mfi14 = ta.mfi(hlc3, 14, volume);
1575
+ *
1576
+ * // Or use context API for cleaner syntax
1577
+ * const { ta } = createContext({ chart: { high, low, close, volume } });
1578
+ * const mfi14 = ta.mfi(hlc3, 14); // Matches PineScript!
1579
+ * ```
1580
+ */
1581
+ export function mfi(source: Source, length: simple_int, volume?: Source): series_float {
1582
+ if (!volume) {
1583
+ throw new Error(
1584
+ 'ta.mfi() requires volume series. ' +
1585
+ 'Either pass it explicitly or use createContext({ chart: { ..., volume } }) for implicit data.'
1586
+ );
1587
+ }
1588
+
1589
+ // PineScript reference:
1590
+ // upper = math.sum(volume * (ta.change(src) <= 0 ? 0 : src), length)
1591
+ // lower = math.sum(volume * (ta.change(src) >= 0 ? 0 : src), length)
1592
+ // A comparison with na is false, so a bar with an na change (bar 0) adds its money flow to both sums.
1593
+ // The comparisons use the 1e-10 tolerance of Pine operators.
1594
+ const upperFlow: number[] = [];
1595
+ const lowerFlow: number[] = [];
1596
+ for (let i = 0; i < source.length; i++) {
1597
+ const change = i === 0 ? NaN : source[i]! - source[i - 1]!;
1598
+ const flow = volume[i]! * source[i]!;
1599
+ upperFlow.push(le(change, 0) ? 0 : flow);
1600
+ lowerFlow.push(ge(change, 0) ? 0 : flow);
1601
+ }
1602
+ // math.sum: the compensated running sum of the last `length` non-na values (as PineScript, bit for bit)
1603
+ const upper = runningSum(upperFlow, length);
1604
+ const lower = runningSum(lowerFlow, length);
1605
+
1606
+ return upper.map((u, i) => {
1607
+ const l = lower[i]!;
1608
+ if (Number.isNaN(u) || Number.isNaN(l)) return NaN;
1609
+ if (l === 0) return 100;
1610
+ return 100 - 100 / (1 + u / l);
1611
+ });
1612
+ }
1613
+
1614
+ /**
1615
+ * Hull Moving Average (HMA) - improved moving average with reduced lag.
1616
+ *
1617
+ * @param source - Series of values to process
1618
+ * @param length - Number of bars (length)
1619
+ * @returns Hull moving average series
1620
+ *
1621
+ * @remarks
1622
+ * - Formula: WMA(2 * WMA(src, len/2) - WMA(src, len), sqrt(len))
1623
+ * - Significantly reduces lag compared to traditional moving averages
1624
+ * - Smoother than WMA while being more responsive
1625
+ * - Created by Alan Hull
1626
+ * - `na` values in the source series are ignored
1627
+ *
1628
+ * @example
1629
+ * ```typescript
1630
+ * const hma20 = ta.hma(closePrices, 20);
1631
+ * // Faster response to price changes than SMA or EMA
1632
+ * ```
1633
+ */
1634
+ export function hma(source: Source, length: simple_int): series_float {
1635
+ const halfLength = Math.floor(length / 2);
1636
+ const sqrtLength = Math.floor(Math.sqrt(length));
1637
+
1638
+ const wmaHalf = wma(source, halfLength);
1639
+ const wmaFull = wma(source, length);
1640
+
1641
+ const diff: series_float = [];
1642
+ for (let i = 0; i < source.length; i++) {
1643
+ diff.push(2 * wmaHalf[i]! - wmaFull[i]!);
1644
+ }
1645
+
1646
+ return wma(diff, sqrtLength);
1647
+ }
1648
+
1649
+ /**
1650
+ * Parabolic SAR (Stop and Reverse) - trend-following indicator.
1651
+ *
1652
+ * @param start - Acceleration factor start value (typically 0.02)
1653
+ * @param inc - Acceleration factor increment (typically 0.02)
1654
+ * @param max - Maximum acceleration factor (typically 0.2)
1655
+ * @param high - High price series (required when not using context API)
1656
+ * @param low - Low price series (required when not using context API)
1657
+ * @param close - Close price series (required when not using context API)
1658
+ * @returns SAR series
1659
+ *
1660
+ * @remarks
1661
+ * - **PineScript v6 signature**: `ta.sar(start, inc, max)` - uses implicit chart data
1662
+ * - **JavaScript signature**: Requires explicit `high`, `low`, `close` OR use `createContext()`
1663
+ * - SAR below price indicates uptrend, above price indicates downtrend
1664
+ * - Created by J. Welles Wilder Jr.
1665
+ *
1666
+ * @example
1667
+ * ```typescript
1668
+ * const sar = ta.sar(0.02, 0.02, 0.2, high, low, close);
1669
+ * ```
1670
+ */
1671
+ export function sar(
1672
+ start: simple_float,
1673
+ inc: simple_float,
1674
+ max: simple_float,
1675
+ high?: Source,
1676
+ low?: Source,
1677
+ close?: Source
1678
+ ): series_float {
1679
+ if (!high || !low || !close) {
1680
+ throw new Error(
1681
+ 'ta.sar() requires high, low, and close series. ' +
1682
+ 'Either pass them explicitly or use createContext({ chart: { high, low, close } }) for implicit data.'
1683
+ );
1684
+ }
1685
+
1686
+ const result: series_float = [];
1687
+ let sarValue = NaN;
1688
+ let extremePoint = NaN;
1689
+ let acceleration = start;
1690
+ let isUpTrend = false;
1691
+ let isFirstTrendBar = false;
1692
+
1693
+ for (let i = 0; i < close.length; i++) {
1694
+ if (i === 0) {
1695
+ result.push(NaN);
1696
+ continue;
1697
+ }
1698
+
1699
+ if (i === 1) {
1700
+ if (close[i]! > close[i - 1]!) {
1701
+ isUpTrend = true;
1702
+ extremePoint = high[i]!;
1703
+ sarValue = low[i - 1]!;
1704
+ } else {
1705
+ isUpTrend = false;
1706
+ extremePoint = low[i]!;
1707
+ sarValue = high[i - 1]!;
1708
+ }
1709
+ isFirstTrendBar = true;
1710
+ acceleration = start;
1711
+ }
1712
+
1713
+ sarValue = sarValue + acceleration * (extremePoint - sarValue);
1714
+
1715
+ if (isUpTrend) {
1716
+ if (sarValue > low[i]!) {
1717
+ isFirstTrendBar = true;
1718
+ isUpTrend = false;
1719
+ sarValue = Math.max(high[i]!, extremePoint);
1720
+ extremePoint = low[i]!;
1721
+ acceleration = start;
1722
+ }
1723
+ } else {
1724
+ if (sarValue < high[i]!) {
1725
+ isFirstTrendBar = true;
1726
+ isUpTrend = true;
1727
+ sarValue = Math.min(low[i]!, extremePoint);
1728
+ extremePoint = high[i]!;
1729
+ acceleration = start;
1730
+ }
1731
+ }
1732
+
1733
+ if (!isFirstTrendBar) {
1734
+ if (isUpTrend) {
1735
+ if (high[i]! > extremePoint) {
1736
+ extremePoint = high[i]!;
1737
+ acceleration = Math.min(acceleration + inc, max);
1738
+ }
1739
+ } else {
1740
+ if (low[i]! < extremePoint) {
1741
+ extremePoint = low[i]!;
1742
+ acceleration = Math.min(acceleration + inc, max);
1743
+ }
1744
+ }
1745
+ }
1746
+
1747
+ if (isUpTrend) {
1748
+ sarValue = Math.min(sarValue, low[i - 1]!);
1749
+ if (i > 1) {
1750
+ sarValue = Math.min(sarValue, low[i - 2]!);
1751
+ }
1752
+ } else {
1753
+ sarValue = Math.max(sarValue, high[i - 1]!);
1754
+ if (i > 1) {
1755
+ sarValue = Math.max(sarValue, high[i - 2]!);
1756
+ }
1757
+ }
1758
+
1759
+ result.push(sarValue);
1760
+ isFirstTrendBar = false;
1761
+ }
1762
+
1763
+ return result;
1764
+ }
1765
+
1766
+ /**
1767
+ * Pivot detection as PineScript computes it.
1768
+ *
1769
+ * The value of the pivot bar is returned `rightbars` bars later, on the bar where the pivot is
1770
+ * confirmed, so no future bar is used. A pivot high may equal values on its left but must be higher
1771
+ * than every value on its right (a pivot low: equal on the left, lower on the right). An `na`
1772
+ * neighbour ends the check on its side; an `na` pivot value gives no pivot.
1773
+ */
1774
+ function pivot(source: Source, leftbars: simple_int, rightbars: simple_int, isHigh: boolean): series_float {
1775
+ // "beats" = the neighbour prevents the pivot
1776
+ const beatsLeft = isHigh ? (x: number, v: number) => x > v : (x: number, v: number) => x < v;
1777
+ const beatsRight = isHigh ? (x: number, v: number) => x >= v : (x: number, v: number) => x <= v;
1778
+
1779
+ return source.map((_, i) => {
1780
+ const center = i - rightbars;
1781
+ if (center - leftbars < 0) return NaN;
1782
+ const v = source[center]!;
1783
+ if (Number.isNaN(v)) return NaN;
1784
+ for (let j = 1; j <= leftbars; j++) {
1785
+ const x = source[center - j]!;
1786
+ if (Number.isNaN(x)) break;
1787
+ if (beatsLeft(x, v)) return NaN;
1788
+ }
1789
+ for (let j = 1; j <= rightbars; j++) {
1790
+ const x = source[center + j]!;
1791
+ if (Number.isNaN(x)) break;
1792
+ if (beatsRight(x, v)) return NaN;
1793
+ }
1794
+ return v;
1795
+ });
1796
+ }
1797
+
1798
+ /**
1799
+ * Pivot High - detects pivot high points in the price series.
1800
+ *
1801
+ * @param sourceOrLeftbars - Source series or leftbars (overloaded)
1802
+ * @param leftbarsOrRightbars - Leftbars or rightbars (overloaded)
1803
+ * @param rightbars - Number of bars to the right (optional)
1804
+ * @param high - High price series (used in 2-param version)
1805
+ * @returns Series with the pivot high value on the bar where the pivot is confirmed
1806
+ * (`rightbars` bars after the pivot bar), NaN elsewhere
1807
+ *
1808
+ * @remarks
1809
+ * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1810
+ * index is `i - rightbars`
1811
+ * - A pivot high may equal values on its left but must be higher than all values on its right
1812
+ * - An `na` neighbour ends the check on its side
1813
+ *
1814
+ * @example
1815
+ * ```typescript
1816
+ * const pivotHighs = ta.pivothigh(high, 2, 2);
1817
+ * // pivotHighs[i] is high[i - 2] when bar i - 2 is a pivot high
1818
+ * ```
1819
+ */
1820
+ export function pivothigh(
1821
+ sourceOrLeftbars: Source | simple_int,
1822
+ leftbarsOrRightbars: simple_int,
1823
+ rightbars?: simple_int,
1824
+ high?: Source
1825
+ ): series_float {
1826
+ if (rightbars === undefined) {
1827
+ if (!high) {
1828
+ throw new Error('ta.pivothigh() requires high series when using two-parameter version.');
1829
+ }
1830
+ return pivot(high, sourceOrLeftbars as simple_int, leftbarsOrRightbars, true);
1831
+ }
1832
+ return pivot(sourceOrLeftbars as Source, leftbarsOrRightbars, rightbars, true);
1833
+ }
1834
+
1835
+ /**
1836
+ * Pivot Low - detects pivot low points in the price series.
1837
+ *
1838
+ * @param sourceOrLeftbars - Source series or leftbars (overloaded)
1839
+ * @param leftbarsOrRightbars - Leftbars or rightbars (overloaded)
1840
+ * @param rightbars - Number of bars to the right (optional)
1841
+ * @param low - Low price series (used in 2-param version)
1842
+ * @returns Series with the pivot low value on the bar where the pivot is confirmed
1843
+ * (`rightbars` bars after the pivot bar), NaN elsewhere
1844
+ *
1845
+ * @remarks
1846
+ * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1847
+ * index is `i - rightbars`
1848
+ * - A pivot low may equal values on its left but must be lower than all values on its right
1849
+ * - An `na` neighbour ends the check on its side
1850
+ *
1851
+ * @example
1852
+ * ```typescript
1853
+ * const pivotLows = ta.pivotlow(low, 2, 2);
1854
+ * // pivotLows[i] is low[i - 2] when bar i - 2 is a pivot low
1855
+ * ```
1856
+ */
1857
+ export function pivotlow(
1858
+ sourceOrLeftbars: Source | simple_int,
1859
+ leftbarsOrRightbars: simple_int,
1860
+ rightbars?: simple_int,
1861
+ low?: Source
1862
+ ): series_float {
1863
+ if (rightbars === undefined) {
1864
+ if (!low) {
1865
+ throw new Error('ta.pivotlow() requires low series when using two-parameter version.');
1866
+ }
1867
+ return pivot(low, sourceOrLeftbars as simple_int, leftbarsOrRightbars, false);
1868
+ }
1869
+ return pivot(sourceOrLeftbars as Source, leftbarsOrRightbars, rightbars, false);
1870
+ }
1871
+
1872
+ /**
1873
+ * Bars Since - returns number of bars since condition was true.
1874
+ *
1875
+ * @param condition - Boolean series condition
1876
+ * @returns Series with number of bars since condition was last true
1877
+ *
1878
+ * @remarks
1879
+ * - Returns 0 when condition is currently true
1880
+ * - Increments by 1 for each bar condition remains false
1881
+ * - Returns NaN if condition has never been true
1882
+ *
1883
+ * @example
1884
+ * ```typescript
1885
+ * const crossovers = ta.crossover(fastMA, slowMA);
1886
+ * const barsSinceCross = ta.barssince(crossovers);
1887
+ * ```
1888
+ */
1889
+ export function barssince(condition: series_bool): series_float {
1890
+ const result: series_float = [];
1891
+ let barsSinceTrue = NaN;
1892
+
1893
+ for (let i = 0; i < condition.length; i++) {
1894
+ if (condition[i]!) {
1895
+ barsSinceTrue = 0;
1896
+ } else if (!isNaN(barsSinceTrue)) {
1897
+ barsSinceTrue++;
1898
+ }
1899
+ result.push(barsSinceTrue);
1900
+ }
1901
+
1902
+ return result;
1903
+ }
1904
+
1905
+ /**
1906
+ * Value When - returns the value when condition was true.
1907
+ *
1908
+ * @param condition - Boolean series condition
1909
+ * @param source - Source series to get value from
1910
+ * @param occurrence - Which occurrence to get (0 = most recent)
1911
+ * @returns Series with values from when condition was true
1912
+ *
1913
+ * @remarks
1914
+ * - occurrence=0 returns value from most recent true condition
1915
+ * - occurrence=1 returns value from second most recent, etc.
1916
+ * - Returns NaN if condition hasn't been true occurrence+1 times yet
1917
+ *
1918
+ * @example
1919
+ * ```typescript
1920
+ * const crossovers = ta.crossover(fastMA, slowMA);
1921
+ * const lastCrossPrice = ta.valuewhen(crossovers, close, 0);
1922
+ * ```
1923
+ */
1924
+ export function valuewhen(condition: series_bool, source: Source, occurrence: simple_int): series_float {
1925
+ const result: series_float = [];
1926
+
1927
+ for (let i = 0; i < condition.length; i++) {
1928
+ let occurrenceCount = 0;
1929
+ let foundValue = NaN;
1930
+
1931
+ for (let j = i; j >= 0; j--) {
1932
+ if (condition[j]) {
1933
+ if (occurrenceCount === occurrence) {
1934
+ foundValue = source[j]!;
1935
+ break;
1936
+ }
1937
+ occurrenceCount++;
1938
+ }
1939
+ }
1940
+
1941
+ result.push(foundValue);
1942
+ }
1943
+
1944
+ return result;
1945
+ }
1946
+
1947
+ /**
1948
+ * Directional Movement Index - returns Directional Movement indicators.
1949
+ *
1950
+ * @param diLength - DI averaging length
1951
+ * @param adxSmoothing - ADX smoothing length
1952
+ * @returns Tuple of [plusDI, minusDI, ADX]
1953
+ *
1954
+ * @remarks
1955
+ * - +DI and -DI measure directional movement
1956
+ * - ADX measures trend strength (0-100)
1957
+ * - ADX above 25 typically indicates strong trend
1958
+ * - Requires high, low, and close data from context
1959
+ *
1960
+ * @example
1961
+ * ```typescript
1962
+ * const [plusDI, minusDI, adx] = ta.dmi(14, 14);
1963
+ * // When +DI > -DI and ADX > 25, strong uptrend
1964
+ * // When -DI > +DI and ADX > 25, strong downtrend
1965
+ * ```
1966
+ */
1967
+ export function dmi(
1968
+ diLength: simple_int,
1969
+ adxSmoothing: simple_int,
1970
+ high: Source,
1971
+ low: Source,
1972
+ close: Source
1973
+ ): [series_float, series_float, series_float] {
1974
+ // PineScript reference (built-in DMI):
1975
+ // up = ta.change(high), down = -ta.change(low)
1976
+ // plusDM = na(up) ? na : (up > down and up > 0 ? up : 0), minusDM likewise
1977
+ // trur = ta.rma(ta.tr, len)
1978
+ // plus = fixnan(100 * ta.rma(plusDM, len) / trur), minus likewise
1979
+ // adx = 100 * ta.rma(math.abs(plus - minus) / (sum == 0 ? 1 : sum), lensig)
1980
+ const len = Math.max(high.length, low.length, close.length);
1981
+ const plusDM: series_float = [];
1982
+ const minusDM: series_float = [];
1983
+ for (let i = 0; i < len; i++) {
1984
+ const up = i === 0 ? NaN : high[i]! - high[i - 1]!;
1985
+ const down = i === 0 ? NaN : low[i - 1]! - low[i]!;
1986
+ plusDM.push(Number.isNaN(up) ? NaN : gt(up, down) && gt(up, 0) ? up : 0);
1987
+ minusDM.push(Number.isNaN(down) ? NaN : gt(down, up) && gt(down, 0) ? down : 0);
1988
+ }
1989
+
1990
+ const trur = rma(tr(false, high, low, close), diLength);
1991
+ const smoothedPlus = rma(plusDM, diLength);
1992
+ const smoothedMinus = rma(minusDM, diLength);
1993
+ // x / 0 is na; fixnan keeps the previous value
1994
+ const directional = (smoothed: series_float): series_float => {
1995
+ let last = NaN;
1996
+ return smoothed.map((v, i) => {
1997
+ const t = trur[i]!;
1998
+ const value = t === 0 ? NaN : (100 * v) / t;
1999
+ if (!Number.isNaN(value)) last = value;
2000
+ return last;
2001
+ });
2002
+ };
2003
+ const plusDI = directional(smoothedPlus);
2004
+ const minusDI = directional(smoothedMinus);
2005
+
2006
+ const dx = plusDI.map((plus, i) => {
2007
+ const minus = minusDI[i]!;
2008
+ const sum = plus + minus;
2009
+ return Math.abs(plus - minus) / (eq(sum, 0) ? 1 : sum);
2010
+ });
2011
+ const adx = rma(dx, adxSmoothing).map((v) => 100 * v);
2012
+
2013
+ return [plusDI, minusDI, adx];
2014
+ }
2015
+
2016
+ /**
2017
+ * True Strength Index - momentum oscillator based on double smoothed momentum.
2018
+ *
2019
+ * @param source - Series of values to process
2020
+ * @param shortLength - Short smoothing length
2021
+ * @param longLength - Long smoothing length
2022
+ * @returns TSI series
2023
+ *
2024
+ * @remarks
2025
+ * - TSI oscillates between +100 and -100
2026
+ * - Positive values indicate bullish momentum
2027
+ * - Negative values indicate bearish momentum
2028
+ * - Crossovers of zero line can signal trend changes
2029
+ * - Less sensitive to short-term price fluctuations than RSI
2030
+ *
2031
+ * @example
2032
+ * ```typescript
2033
+ * const tsi = ta.tsi(close, 13, 25);
2034
+ * // TSI > 0: bullish momentum
2035
+ * // TSI < 0: bearish momentum
2036
+ * ```
2037
+ */
2038
+ export function tsi(source: Source, shortLength: simple_int, longLength: simple_int): series_float {
2039
+ const momentum: series_float = [];
2040
+
2041
+ // Calculate momentum (price change)
2042
+ for (let i = 0; i < source.length; i++) {
2043
+ if (i === 0) {
2044
+ momentum.push(NaN);
2045
+ } else {
2046
+ momentum.push(source[i]! - source[i - 1]!);
2047
+ }
2048
+ }
2049
+
2050
+ // Double smooth momentum
2051
+ const smoothedMomentum = ema(ema(momentum, longLength), shortLength);
2052
+
2053
+ // Double smooth absolute momentum
2054
+ const absMomentum = momentum.map(Math.abs);
2055
+ const smoothedAbsMomentum = ema(ema(absMomentum, longLength), shortLength);
2056
+
2057
+ // Calculate TSI
2058
+ const result: series_float = [];
2059
+ for (let i = 0; i < source.length; i++) {
2060
+ if (smoothedAbsMomentum[i]! === 0) {
2061
+ result.push(0);
2062
+ } else {
2063
+ result.push((smoothedMomentum[i]! / smoothedAbsMomentum[i]!) * 100);
2064
+ }
2065
+ }
2066
+
2067
+ return result;
2068
+ }
2069
+
2070
+ /**
2071
+ * Chande Momentum Oscillator - momentum indicator similar to RSI.
2072
+ *
2073
+ * @param source - Series of values to process
2074
+ * @param length - Number of bars
2075
+ * @returns CMO series
2076
+ *
2077
+ * @remarks
2078
+ * - CMO oscillates between +100 and -100
2079
+ * - CMO > +50: overbought conditions
2080
+ * - CMO < -50: oversold conditions
2081
+ * - Unlike RSI, CMO uses sum of gains/losses instead of averages
2082
+ * - More volatile than RSI
2083
+ *
2084
+ * @example
2085
+ * ```typescript
2086
+ * const cmo = ta.cmo(close, 14);
2087
+ * // CMO > 50: overbought
2088
+ * // CMO < -50: oversold
2089
+ * ```
2090
+ */
2091
+ export function cmo(source: Source, length: simple_int): series_float {
2092
+ const result: series_float = [];
2093
+
2094
+ for (let i = 0; i < source.length; i++) {
2095
+ if (i < length) {
2096
+ result.push(NaN);
2097
+ continue;
2098
+ }
2099
+
2100
+ let sumGains = 0;
2101
+ let sumLosses = 0;
2102
+
2103
+ for (let j = 0; j < length; j++) {
2104
+ const change = source[i - j]! - source[i - j - 1]!;
2105
+ if (change > 0) {
2106
+ sumGains += change;
2107
+ } else {
2108
+ sumLosses += Math.abs(change);
2109
+ }
2110
+ }
2111
+
2112
+ const totalMovement = sumGains + sumLosses;
2113
+ if (totalMovement === 0) {
2114
+ result.push(0);
2115
+ } else {
2116
+ const cmoValue = ((sumGains - sumLosses) / totalMovement) * 100;
2117
+ result.push(cmoValue);
2118
+ }
2119
+ }
2120
+
2121
+ return result;
2122
+ }
2123
+
2124
+ /**
2125
+ * Keltner Channels - volatility-based envelope indicator.
2126
+ *
2127
+ * @param source - Series of values to process
2128
+ * @param length - Number of bars for EMA
2129
+ * @param mult - Multiplier for the range
2130
+ * @param useTrueRange - Use True Range (default: true) or high-low
2131
+ * @returns Tuple of [middle, upper, lower]
2132
+ *
2133
+ * @remarks
2134
+ * - Middle band is EMA of source
2135
+ * - Upper/lower bands are middle ± (range EMA × multiplier)
2136
+ * - When useTrueRange=true, uses ATR for volatility
2137
+ * - When useTrueRange=false, uses high-low range
2138
+ * - Price breaking out of bands may signal trend continuation
2139
+ * - Requires high, low, close data from context when useTrueRange=true
2140
+ *
2141
+ * @example
2142
+ * ```typescript
2143
+ * const [middle, upper, lower] = ta.kc(close, 20, 2, true);
2144
+ * // Price above upper: potential uptrend
2145
+ * // Price below lower: potential downtrend
2146
+ * ```
2147
+ */
2148
+ export function kc(
2149
+ source: Source,
2150
+ length: simple_int,
2151
+ mult: simple_float,
2152
+ useTrueRange: simple_bool = true,
2153
+ high?: Source,
2154
+ low?: Source,
2155
+ close?: Source
2156
+ ): [series_float, series_float, series_float] {
2157
+ // Calculate middle band (EMA of source)
2158
+ const middle = ema(source, length);
2159
+
2160
+ // Calculate range
2161
+ let range: series_float;
2162
+ if (useTrueRange) {
2163
+ if (!high || !low || !close) {
2164
+ throw new Error('ta.kc() with useTrueRange=true requires high, low, and close data');
2165
+ }
2166
+ // PineScript reference: ta.tr, which is na on bar 0
2167
+ range = tr(false, high, low, close);
2168
+ } else {
2169
+ if (!high || !low) {
2170
+ throw new Error('ta.kc() requires high and low data');
2171
+ }
2172
+ range = [];
2173
+ for (let i = 0; i < high.length; i++) {
2174
+ range.push(high[i]! - low[i]!);
2175
+ }
2176
+ }
2177
+
2178
+ // Smooth the range with EMA
2179
+ const rangeEma = ema(range, length);
2180
+
2181
+ // Calculate upper and lower bands
2182
+ const upper: series_float = [];
2183
+ const lower: series_float = [];
2184
+
2185
+ for (let i = 0; i < middle.length; i++) {
2186
+ upper.push(middle[i]! + rangeEma[i]! * mult);
2187
+ lower.push(middle[i]! - rangeEma[i]! * mult);
2188
+ }
2189
+
2190
+ return [middle, upper, lower];
2191
+ }
2192
+
2193
+ /**
2194
+ * Bollinger Bands Width - measures the width of Bollinger Bands.
2195
+ *
2196
+ * @param source - Series of values to process
2197
+ * @param length - Number of bars
2198
+ * @param mult - Standard deviation multiplier
2199
+ * @returns BBW series (percentage)
2200
+ *
2201
+ * @remarks
2202
+ * - BBW = ((upper band - lower band) / middle band) × 100
2203
+ * - Low BBW values indicate low volatility (potential breakout setup)
2204
+ * - High BBW values indicate high volatility
2205
+ * - BBW squeeze (narrowing bands) often precedes strong moves
2206
+ * - Works with existing bb() function
2207
+ *
2208
+ * @example
2209
+ * ```typescript
2210
+ * const bbw = ta.bbw(close, 20, 2);
2211
+ * // Low BBW: potential breakout coming
2212
+ * // High BBW: high volatility period
2213
+ * ```
2214
+ */
2215
+ export function bbw(source: Source, length: simple_int, mult: simple_float): series_float {
2216
+ const [basis, upper, lower] = bb(source, length, mult);
2217
+ const result: series_float = [];
2218
+
2219
+ for (let i = 0; i < source.length; i++) {
2220
+ if (basis[i]! === 0) {
2221
+ result.push(NaN);
2222
+ } else {
2223
+ const width = ((upper[i]! - lower[i]!) / basis[i]!) * 100;
2224
+ result.push(width);
2225
+ }
2226
+ }
2227
+
2228
+ return result;
2229
+ }
2230
+
2231
+ /**
2232
+ * Williams %R (Williams Percent Range)
2233
+ *
2234
+ * Williams %R is a momentum indicator that measures overbought/oversold levels.
2235
+ * It compares the closing price to the high-low range over a specified period.
2236
+ *
2237
+ * Values range from -100 (oversold) to 0 (overbought):
2238
+ * - Above -20: Overbought
2239
+ * - Below -80: Oversold
2240
+ *
2241
+ * @param high - High price series
2242
+ * @param low - Low price series
2243
+ * @param close - Close price series
2244
+ * @param length - Lookback period (default: 14)
2245
+ * @returns Williams %R series
2246
+ *
2247
+ * @example
2248
+ * ```typescript
2249
+ * const wpr = ta.wpr(high, low, close, 14);
2250
+ * // wpr < -80: oversold
2251
+ * // wpr > -20: overbought
2252
+ * ```
2253
+ */
2254
+ export function wpr(high: Source, low: Source, close: Source, length: simple_int = 14): series_float {
2255
+ const result: series_float = [];
2256
+
2257
+ for (let i = 0; i < close.length; i++) {
2258
+ if (i < length - 1) {
2259
+ result.push(NaN);
2260
+ continue;
2261
+ }
2262
+
2263
+ // Find highest high and lowest low in the period
2264
+ let highestHigh = high[i - length + 1]!;
2265
+ let lowestLow = low[i - length + 1]!;
2266
+
2267
+ for (let j = i - length + 2; j <= i; j++) {
2268
+ if (high[j]! > highestHigh) highestHigh = high[j]!;
2269
+ if (low[j]! < lowestLow) lowestLow = low[j]!;
2270
+ }
2271
+
2272
+ const range = highestHigh - lowestLow;
2273
+ if (range === 0) {
2274
+ result.push(NaN);
2275
+ } else {
2276
+ // Formula: (Highest High - Close) / (Highest High - Lowest Low) * -100
2277
+ const wprValue = ((highestHigh! - close[i]!) / range) * -100;
2278
+ result.push(wprValue);
2279
+ }
2280
+ }
2281
+
2282
+ return result;
2283
+ }
2284
+
2285
+ /**
2286
+ * Volume Weighted Average Price, restarted on each bar where `anchor` is true.
2287
+ *
2288
+ * @param source - Source series (PineScript default `hlc3`)
2289
+ * @param volume - Volume series
2290
+ * @param anchor - Reset condition per bar (PineScript `anchor`). Without it the sums never restart;
2291
+ * PineScript's default anchor is a new day (`timeframe.change("1D")`), which needs the exchange session
2292
+ * @param stdev_mult - When given, also returns the bands `vwap ± stdev_mult × stdev`
2293
+ * @returns The VWAP series, or `[vwap, upper, lower]` when `stdev_mult` is given
2294
+ *
2295
+ * @remarks
2296
+ * As in PineScript: the sums restart on anchor bars; the standard deviation is
2297
+ * `sqrt(Σ(volume × source²) / Σvolume − vwap²)`.
2298
+ */
2299
+ export function vwap(source: Source, volume: Source, anchor?: ArrayLike<boolean | number>): series_float;
2300
+ export function vwap(
2301
+ source: Source,
2302
+ volume: Source,
2303
+ anchor: ArrayLike<boolean | number> | undefined,
2304
+ stdev_mult: simple_float
2305
+ ): [series_float, series_float, series_float];
2306
+ export function vwap(
2307
+ source: Source,
2308
+ volume: Source,
2309
+ anchor?: ArrayLike<boolean | number>,
2310
+ stdev_mult?: simple_float
2311
+ ): series_float | [series_float, series_float, series_float] {
2312
+ if (source.length !== volume.length) {
2313
+ throw new Error('ta.vwap: source and volume must have the same length');
2314
+ }
2315
+ const mid: series_float = [];
2316
+ const upper: series_float = [];
2317
+ const lower: series_float = [];
2318
+ let sumPV = 0;
2319
+ let sumV = 0;
2320
+ let sumPPV = 0;
2321
+ for (let i = 0; i < source.length; i++) {
2322
+ if (anchor && anchor[i]) {
2323
+ sumPV = 0;
2324
+ sumV = 0;
2325
+ sumPPV = 0;
2326
+ }
2327
+ const s = source[i]!;
2328
+ const v = volume[i]!;
2329
+ if (!Number.isNaN(s) && !Number.isNaN(v)) {
2330
+ sumPV += s * v;
2331
+ sumV += v;
2332
+ sumPPV += s * s * v;
2333
+ }
2334
+ const m = sumV === 0 ? NaN : sumPV / sumV;
2335
+ mid.push(m);
2336
+ if (stdev_mult !== undefined) {
2337
+ const sd = Math.sqrt(Math.max(0, sumPPV / sumV - m * m));
2338
+ upper.push(m + stdev_mult * sd);
2339
+ lower.push(m - stdev_mult * sd);
2340
+ }
2341
+ }
2342
+ return stdev_mult === undefined ? mid : [mid, upper, lower];
2343
+ }
2344
+
2345
+ /**
2346
+ * Arnaud Legoux Moving Average (ALMA)
2347
+ *
2348
+ * ALMA uses a Gaussian distribution for weighting, reducing lag while maintaining smoothness.
2349
+ * It's particularly good at tracking price action with minimal lag.
2350
+ *
2351
+ * @param source - Source series
2352
+ * @param length - Window size (default: 9)
2353
+ * @param offset - Controls the center of the Gaussian curve. 0.85 = focus on recent prices (default: 0.85)
2354
+ * @param sigma - Standard deviation of the Gaussian. Controls smoothness (default: 6)
2355
+ * @returns ALMA series
2356
+ *
2357
+ * @example
2358
+ * ```typescript
2359
+ * const alma = ta.alma(close, 9, 0.85, 6);
2360
+ * // offset closer to 1: more responsive
2361
+ * // offset closer to 0: smoother
2362
+ * ```
2363
+ */
2364
+ export function alma(
2365
+ source: Source,
2366
+ length: simple_int = 9,
2367
+ offset: simple_float = 0.85,
2368
+ sigma: simple_float = 6,
2369
+ floor: boolean = false
2370
+ ): series_float {
2371
+ const result: series_float = [];
2372
+ const m = floor ? Math.floor(offset * (length - 1)) : offset * (length - 1);
2373
+ const s = length / sigma;
2374
+
2375
+ // Pre-calculate weights
2376
+ const weights: number[] = [];
2377
+ let weightSum = 0;
2378
+
2379
+ for (let i = 0; i < length; i++) {
2380
+ const weight = Math.exp(-1 * Math.pow(i - m, 2) / (2 * Math.pow(s, 2)));
2381
+ weights.push(weight);
2382
+ weightSum += weight;
2383
+ }
2384
+
2385
+ // Normalize weights
2386
+ for (let i = 0; i < length; i++) {
2387
+ weights[i]! /= weightSum;
2388
+ }
2389
+
2390
+ // Calculate ALMA
2391
+ for (let i = 0; i < source.length; i++) {
2392
+ if (i < length - 1) {
2393
+ result.push(NaN);
2394
+ continue;
2395
+ }
2396
+
2397
+ let almaValue = 0;
2398
+ for (let j = 0; j < length; j++) {
2399
+ almaValue += source[i - length + 1 + j]! * weights[j]!;
2400
+ }
2401
+
2402
+ result.push(almaValue);
2403
+ }
2404
+
2405
+ return result;
2406
+ }
2407
+
2408
+ /**
2409
+ * Keltner Channels Width (KCW)
2410
+ *
2411
+ * Measures the width of Keltner Channels as a percentage of the middle line.
2412
+ * Similar to BBW but uses ATR instead of standard deviation.
2413
+ *
2414
+ * Low KCW suggests consolidation/low volatility.
2415
+ * High KCW suggests expansion/high volatility.
2416
+ *
2417
+ * @param source - Source series
2418
+ * @param length - Number of bars for EMA and ATR (default: 20)
2419
+ * @param mult - ATR multiplier (default: 2)
2420
+ * @param useTrueRange - Use True Range instead of high-low (default: true)
2421
+ * @param high - High price series
2422
+ * @param low - Low price series
2423
+ * @param close - Close price series
2424
+ * @returns KCW series
2425
+ *
2426
+ * @example
2427
+ * ```typescript
2428
+ * const kcw = ta.kcw(close, 20, 2, true, high, low, close);
2429
+ * // Low KCW: potential breakout coming
2430
+ * // High KCW: high volatility period
2431
+ * ```
2432
+ */
2433
+ export function kcw(
2434
+ source: Source,
2435
+ length: simple_int = 20,
2436
+ mult: simple_float = 2,
2437
+ useTrueRange: simple_bool = true,
2438
+ high?: Source,
2439
+ low?: Source,
2440
+ close?: Source
2441
+ ): series_float {
2442
+ const [basis, upper, lower] = kc(source, length, mult, useTrueRange, high, low, close);
2443
+ const result: series_float = [];
2444
+
2445
+ for (let i = 0; i < source.length; i++) {
2446
+ if (isNaN(basis[i]!) || basis[i]! === 0) {
2447
+ result.push(NaN);
2448
+ } else {
2449
+ const width = ((upper[i]! - lower[i]!) / basis[i]!) * 100;
2450
+ result.push(width);
2451
+ }
2452
+ }
2453
+
2454
+ return result;
2455
+ }
2456
+
2457
+ /**
2458
+ * Difference between the highest and the lowest value over the last `length` non-na values
2459
+ * (PineScript `ta.range(source, length)`).
2460
+ *
2461
+ * @remarks
2462
+ * As in PineScript: `na` values are skipped (the window holds the last `length` non-na
2463
+ * values), and a bar whose own value is `na` still gets a result.
2464
+ */
2465
+ export function range(source: Source, length: simple_int): series_float {
2466
+ return source.map((_, i) => {
2467
+ const values = lastValues(source, i, length);
2468
+ return values ? Math.max(...values) - Math.min(...values) : NaN;
2469
+ });
2470
+ }
2471
+
2472
+ /**
2473
+ * Offset to the bar with the highest value over the last `length` bars: 0 for the current bar,
2474
+ * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2475
+ *
2476
+ * @remarks
2477
+ * As in PineScript: on equal values the oldest bar wins; an `na` value ends the window;
2478
+ * 0 when the window has no value.
2479
+ */
2480
+ export function highestbars(source: Source, length: simple_int): series_int {
2481
+ return extremeOffset(source, length, false);
2482
+ }
2483
+
2484
+ /**
2485
+ * Offset to the bar with the lowest value over the last `length` bars: 0 for the current bar,
2486
+ * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2487
+ *
2488
+ * @remarks
2489
+ * As in PineScript: on equal values the oldest bar wins; an `na` value ends the window;
2490
+ * 0 when the window has no value.
2491
+ */
2492
+ export function lowestbars(source: Source, length: simple_int): series_int {
2493
+ return extremeOffset(source, length, true);
2494
+ }
2495
+
2496
+ /**
2497
+ * All-time highest value of `source` up to each bar (PineScript `ta.max(source)`).
2498
+ *
2499
+ * @remarks
2500
+ * As in PineScript: `na` values are skipped, the value carries over `na` bars, and the
2501
+ * result is `na` until the first non-na value.
2502
+ */
2503
+ export function max(source: Source): series_float {
2504
+ let best = NaN;
2505
+ return source.map((x) => {
2506
+ if (!Number.isNaN(x) && !(x <= best)) best = x;
2507
+ return best;
2508
+ });
2509
+ }
2510
+
2511
+ /**
2512
+ * All-time lowest value of `source` up to each bar (PineScript `ta.min(source)`).
2513
+ *
2514
+ * @remarks
2515
+ * As in PineScript: `na` values are skipped, the value carries over `na` bars, and the
2516
+ * result is `na` until the first non-na value.
2517
+ */
2518
+ export function min(source: Source): series_float {
2519
+ let best = NaN;
2520
+ return source.map((x) => {
2521
+ if (!Number.isNaN(x) && !(x >= best)) best = x;
2522
+ return best;
2523
+ });
2524
+ }
2525
+
2526
+ /**
2527
+ * Center of Gravity (COG)
2528
+ *
2529
+ * The Center of Gravity indicator is an oscillator developed by John Ehlers.
2530
+ * It identifies turning points with minimal lag and provides clear signals.
2531
+ *
2532
+ * The COG calculates a weighted average where more recent prices have higher weights,
2533
+ * similar to a moving average but with a focus on momentum shifts.
2534
+ *
2535
+ * @param source - Source series (typically close)
2536
+ * @param length - Lookback period (default: 10)
2537
+ * @returns COG series
2538
+ *
2539
+ * @example
2540
+ * ```typescript
2541
+ * const cogValue = ta.cog(close, 10);
2542
+ * // Use COG crossovers as signals:
2543
+ * // - COG crossing above 0: potential buy signal
2544
+ * // - COG crossing below 0: potential sell signal
2545
+ * ```
2546
+ */
2547
+ export function cog(source: Source, length: simple_int = 10): series_float {
2548
+ const result: series_float = [];
2549
+
2550
+ for (let i = 0; i < source.length; i++) {
2551
+ if (i < length - 1) {
2552
+ result.push(NaN);
2553
+ continue;
2554
+ }
2555
+
2556
+ let numerator = 0;
2557
+ let denominator = 0;
2558
+
2559
+ for (let j = 0; j < length; j++) {
2560
+ const weight = j + 1;
2561
+ const price = source[i - length + 1 + j]!;
2562
+ numerator += weight * price;
2563
+ denominator += price;
2564
+ }
2565
+
2566
+ if (denominator === 0) {
2567
+ result.push(NaN);
2568
+ } else {
2569
+ // COG formula: -1 * (sum of (weight * price) / sum of prices) + (length + 1) / 2
2570
+ const cog = -1 * (numerator / denominator) + (length + 1) / 2;
2571
+ result.push(cog);
2572
+ }
2573
+ }
2574
+
2575
+ return result;
2576
+ }
2577
+
2578
+ /**
2579
+ * Mode (Most Frequent Value)
2580
+ *
2581
+ * Returns the mode of the series - the most frequently occurring value.
2582
+ * If there are several values with the same frequency, it returns the smallest value.
2583
+ *
2584
+ * @param source - Series of values to process
2585
+ * @param length - Number of bars to look back
2586
+ * @returns The most frequently occurring value
2587
+ *
2588
+ * @example
2589
+ * ```typescript
2590
+ * const values = [1, 2, 2, 3, 3, 3, 4, 4];
2591
+ * const modeValue = ta.mode(values, 8); // Returns 3 (most frequent)
2592
+ * ```
2593
+ *
2594
+ * @remarks
2595
+ * - `na` values in the source series are ignored
2596
+ * - If no mode exists, returns the smallest value
2597
+ * - Returns NaN for the first (length - 1) values where there's insufficient data
2598
+ */
2599
+ export function mode(source: Source, length: simple_int): series_float {
2600
+ const result: series_float = [];
2601
+
2602
+ for (let i = 0; i < source.length; i++) {
2603
+ if (i < length - 1) {
2604
+ result.push(NaN);
2605
+ continue;
2606
+ }
2607
+
2608
+ // Collect non-NaN values in the window
2609
+ const values: number[] = [];
2610
+ for (let j = 0; j < length; j++) {
2611
+ const value = source[i - j]!;
2612
+ if (!isNaN(value)) {
2613
+ values.push(value);
2614
+ }
2615
+ }
2616
+
2617
+ if (values.length === 0) {
2618
+ result.push(NaN);
2619
+ continue;
2620
+ }
2621
+
2622
+ // Count frequency of each value
2623
+ const frequencyMap = new Map<number, number>();
2624
+ for (const value of values) {
2625
+ frequencyMap.set(value, (frequencyMap.get(value) || 0) + 1);
2626
+ }
2627
+
2628
+ // Find the maximum frequency
2629
+ let maxFrequency = 0;
2630
+ frequencyMap.forEach((freq) => {
2631
+ if (freq > maxFrequency) {
2632
+ maxFrequency = freq;
2633
+ }
2634
+ });
2635
+
2636
+ // Find all values with max frequency, then return the smallest
2637
+ const modesWithMaxFreq: number[] = [];
2638
+ frequencyMap.forEach((freq, value) => {
2639
+ if (freq === maxFrequency) {
2640
+ modesWithMaxFreq.push(value);
2641
+ }
2642
+ });
2643
+
2644
+ result.push(Math.min(...modesWithMaxFreq));
2645
+ }
2646
+
2647
+ return result;
2648
+ }
2649
+
2650
+ /**
2651
+ * Percentile (Linear Interpolation Method)
2652
+ *
2653
+ * Calculates the percentile using the method of linear interpolation between
2654
+ * the two nearest ranks. This method may return values that are not members
2655
+ * of the input data set.
2656
+ *
2657
+ * @param source - Series of values to process
2658
+ * @param length - Number of bars to look back
2659
+ * @param percentage - Percentile to calculate (0-100)
2660
+ * @returns The calculated percentile value
2661
+ *
2662
+ * @example
2663
+ * ```typescript
2664
+ * const p50 = ta.percentile_linear_interpolation(close, 20, 50); // Median
2665
+ * const p75 = ta.percentile_linear_interpolation(close, 20, 75); // 75th percentile
2666
+ * ```
2667
+ *
2668
+ * @remarks
2669
+ * - The result will NOT always be a member of the input data set
2670
+ * - Uses linear interpolation between adjacent values when needed
2671
+ * - Returns NaN for the first (length - 1) values where there's insufficient data
2672
+ *
2673
+ * PineScript rules:
2674
+ * - position in the sorted window: `percentage / 100 * length - 0.5`, clamped to the first / last value, linear
2675
+ * interpolation between the two neighbours (na when a neighbour is na, also at an exact position)
2676
+ * - `na` values stay in the window (window size = `length`). The window is kept sorted from bar to bar: the new
2677
+ * value is inserted before the first value `>=` it, passing the `na` values (an `na` goes last), then the value
2678
+ * leaving the window is removed. So the place of an `na` depends on the history: at the start of a series the
2679
+ * `na` values are before all numbers
2680
+ */
2681
+ export function percentile_linear_interpolation(
2682
+ source: Source,
2683
+ length: simple_int,
2684
+ percentage: number
2685
+ ): series_float {
2686
+ return sortedWindow(source, length, (s) => {
2687
+ const pos = (percentage / 100) * length - 0.5;
2688
+ if (pos <= 0) return s[0]!;
2689
+ if (pos >= length - 1) return s[length - 1]!;
2690
+ const lo = Math.floor(pos);
2691
+ return s[lo]! + (pos - lo) * (s[lo + 1]! - s[lo]!);
2692
+ });
2693
+ }
2694
+
2695
+ /**
2696
+ * Sorted window of ta.percentile_*, as PineScript keeps it: each bar first inserts the new value before the first
2697
+ * value `>=` it (scanning from the start; an `na` is never `>=`, so a value passes the `na` values and an `na` goes
2698
+ * last), then removes the value of the bar leaving the window. `fn` reads the sorted window (`length` values).
2699
+ * na for the first `length - 1` bars.
2700
+ *
2701
+ * @internal
2702
+ */
2703
+ function sortedWindow(source: Source, length: simple_int, fn: (sorted: number[]) => number): series_float {
2704
+ const result: series_float = [];
2705
+ const values: number[] = []; // sorted window
2706
+ const bars: number[] = []; // bar index of each value
2707
+ for (let i = 0; i < source.length; i++) {
2708
+ const v = source[i]!;
2709
+ let k = 0;
2710
+ while (k < values.length && !(values[k]! >= v)) k++;
2711
+ values.splice(k, 0, v);
2712
+ bars.splice(k, 0, i);
2713
+ if (i >= length) {
2714
+ const old = bars.indexOf(i - length);
2715
+ values.splice(old, 1);
2716
+ bars.splice(old, 1);
2717
+ }
2718
+ result.push(i < length - 1 ? NaN : fn(values));
2719
+ }
2720
+ return result;
2721
+ }
2722
+
2723
+ /**
2724
+ * Percentile (Nearest Rank Method)
2725
+ *
2726
+ * Calculates the percentile using the Nearest Rank method. This method
2727
+ * always returns a value that is a member of the input data set.
2728
+ *
2729
+ * @param source - Series of values to process
2730
+ * @param length - Number of bars to look back
2731
+ * @param percentage - Percentile to calculate (0-100)
2732
+ * @returns The calculated percentile value
2733
+ *
2734
+ * @example
2735
+ * ```typescript
2736
+ * const p50 = ta.percentile_nearest_rank(close, 20, 50); // Median
2737
+ * const p90 = ta.percentile_nearest_rank(close, 20, 90); // 90th percentile
2738
+ * ```
2739
+ *
2740
+ * @remarks
2741
+ * - The result will ALWAYS be a member of the input data set
2742
+ * - The 100th percentile is defined as the largest value
2743
+ * - Using this method on lengths < 100 may result in the same value for multiple percentiles
2744
+ * - Returns NaN for the first (length - 1) values where there's insufficient data
2745
+ *
2746
+ * PineScript rules:
2747
+ * - the value at rank `ceil(percentage / 100 * length)` of the sorted window (na when that rank holds an `na`)
2748
+ * - `na` values stay in the window, sorted from bar to bar as in `ta.percentile_linear_interpolation`
2749
+ */
2750
+ export function percentile_nearest_rank(
2751
+ source: Source,
2752
+ length: simple_int,
2753
+ percentage: number
2754
+ ): series_float {
2755
+ const rank = Math.min(length, Math.max(1, Math.ceil((percentage / 100) * length)));
2756
+ return sortedWindow(source, length, (s) => s[rank - 1]!);
2757
+ }
2758
+
2759
+ /**
2760
+ * Rank Correlation Index (RCI)
2761
+ *
2762
+ * Calculates the Rank Correlation Index using Spearman's rank correlation coefficient.
2763
+ * RCI measures the directional consistency of price movements, indicating whether
2764
+ * the source consistently increased (positive values) or decreased (negative values).
2765
+ *
2766
+ * @param source - Series of values to process
2767
+ * @param length - Number of bars to look back
2768
+ * @returns RCI value scaled to range -100 to 100
2769
+ *
2770
+ * @example
2771
+ * ```typescript
2772
+ * const rci9 = ta.rci(close, 9);
2773
+ * // RCI near +100: strong upward consistency
2774
+ * // RCI near -100: strong downward consistency
2775
+ * // RCI near 0: no clear trend
2776
+ * ```
2777
+ *
2778
+ * @remarks
2779
+ * - Result is scaled to -100 to 100 range
2780
+ * - +100 indicates source consistently increased over the period
2781
+ * - -100 indicates source consistently decreased over the period
2782
+ * - 0 indicates no directional consistency
2783
+ * - Returns NaN for the first (length - 1) values where there's insufficient data
2784
+ */
2785
+ export function rci(source: Source, length: simple_int): series_float {
2786
+ const result: series_float = [];
2787
+
2788
+ for (let i = 0; i < source.length; i++) {
2789
+ if (i < length - 1) {
2790
+ result.push(NaN);
2791
+ continue;
2792
+ }
2793
+
2794
+ // Collect values in the window
2795
+ const values: number[] = [];
2796
+ for (let j = 0; j < length; j++) {
2797
+ values.push(source[i - length + 1 + j]!);
2798
+ }
2799
+
2800
+ // Check for NaN values
2801
+ if (values.some(v => isNaN(v))) {
2802
+ result.push(NaN);
2803
+ continue;
2804
+ }
2805
+
2806
+ // Create array of indices with their values for ranking
2807
+ const indexed = values.map((value, index) => ({ value, index }));
2808
+
2809
+ // Sort by value to get ranks
2810
+ const sorted = [...indexed].sort((a, b) => a.value - b.value);
2811
+
2812
+ // Assign ranks (handling ties by averaging ranks)
2813
+ const ranks = new Array(length).fill(0);
2814
+ let currentRank = 1;
2815
+ for (let j = 0; j < sorted.length; j++) {
2816
+ // Count ties
2817
+ let tieCount = 1;
2818
+ while (j + tieCount < sorted.length && sorted[j]!.value === sorted[j + tieCount]!.value) {
2819
+ tieCount++;
2820
+ }
2821
+
2822
+ // Average rank for ties
2823
+ const avgRank = (currentRank + (currentRank + tieCount - 1)) / 2;
2824
+
2825
+ // Assign average rank to all tied values
2826
+ for (let k = 0; k < tieCount; k++) {
2827
+ ranks[sorted[j + k]!.index] = avgRank;
2828
+ }
2829
+
2830
+ j += tieCount - 1;
2831
+ currentRank += tieCount;
2832
+ }
2833
+
2834
+ // Calculate D² = sum of (price_rank - time_rank)²
2835
+ let sumSquaredDiff = 0;
2836
+ for (let j = 0; j < length; j++) {
2837
+ const timeRank = j + 1;
2838
+ const diff = ranks[j] - timeRank;
2839
+ sumSquaredDiff += diff * diff;
2840
+ }
2841
+
2842
+ const n = length;
2843
+ const base = (n * n * n - n) / 12;
2844
+
2845
+ // Compute tie correction for price ranks
2846
+ // Count tied groups from the sorted array
2847
+ let tieCorrection = 0;
2848
+ for (let j = 0; j < sorted.length; ) {
2849
+ let tieCount = 1;
2850
+ while (j + tieCount < sorted.length && sorted[j]!.value === sorted[j + tieCount]!.value) {
2851
+ tieCount++;
2852
+ }
2853
+ if (tieCount > 1) {
2854
+ tieCorrection += (tieCount * tieCount * tieCount - tieCount) / 12;
2855
+ }
2856
+ j += tieCount;
2857
+ }
2858
+
2859
+ const A = base - tieCorrection; // Corrected for tied price ranks
2860
+ const B = base; // Time ranks have no ties
2861
+
2862
+ let rho: number;
2863
+ if (A === 0 || B === 0) {
2864
+ rho = 0;
2865
+ } else {
2866
+ rho = (A + B - sumSquaredDiff) / (2 * Math.sqrt(A * B));
2867
+ }
2868
+
2869
+ result.push(rho * 100);
2870
+ }
2871
+
2872
+ return result;
2873
+ }
2874
+
2875
+ /**
2876
+ * Pivot Point Levels
2877
+ *
2878
+ * Calculates pivot point levels using various calculation methods.
2879
+ * Returns an array containing: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
2880
+ *
2881
+ * @param type - Calculation type: "Traditional", "Fibonacci", "Woodie", "Classic", "DM", "Camarilla"
2882
+ * @param anchor - Condition that triggers reset of calculations
2883
+ * @param developing - If true, pivots recalculate continuously; if false, use last anchor values
2884
+ * @param high - High price series (optional, uses context if not provided)
2885
+ * @param low - Low price series (optional, uses context if not provided)
2886
+ * @param close - Close price series (optional, uses context if not provided)
2887
+ * @param open - Open price series (optional, uses context if not provided)
2888
+ * @returns Array of 11 pivot levels
2889
+ *
2890
+ * @example
2891
+ * ```typescript
2892
+ * const weekChange = [false, false, false, false, true, false, ...]; // Weekly anchor
2893
+ * const pivots = ta.pivot_point_levels("Traditional", weekChange, false, high, low, close);
2894
+ * // pivots[i]! = [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
2895
+ * ```
2896
+ *
2897
+ * @remarks
2898
+ * - Woodie type cannot use developing=true (will error in PineScript)
2899
+ * - DM type only calculates P, R1, S1 (other levels are NaN)
2900
+ * - All calculations follow PineScript v6 specifications
2901
+ */
2902
+ export function pivot_point_levels(
2903
+ type: series_float | string,
2904
+ anchor: series_bool,
2905
+ developing: series_bool | boolean = false,
2906
+ high?: Source,
2907
+ low?: Source,
2908
+ close?: Source,
2909
+ open?: Source
2910
+ ): series_float[] {
2911
+ // For simplicity, we need to handle the case where type is a string array
2912
+ // But according to the docs, type is "series string", meaning it can change per bar
2913
+ // For this implementation, we'll support constant type strings
2914
+
2915
+ const typeStr = typeof type === 'string' ? type : String(type[0]);
2916
+ const isDeveloping = typeof developing === 'boolean' ? developing : developing[0];
2917
+
2918
+ // Woodie cannot be developing
2919
+ if (typeStr === 'Woodie' && isDeveloping) {
2920
+ throw new Error('ta.pivot_point_levels: Woodie type cannot use developing=true');
2921
+ }
2922
+
2923
+ // Ensure all series have the same length
2924
+ const length = anchor.length;
2925
+ if (high && high.length !== length) throw new Error('High series length mismatch');
2926
+ if (low && low.length !== length) throw new Error('Low series length mismatch');
2927
+ if (close && close.length !== length) throw new Error('Close series length mismatch');
2928
+ if (open && open.length !== length) throw new Error('Open series length mismatch');
2929
+
2930
+ // Initialize result arrays for all 11 levels
2931
+ const results: series_float[] = Array.from({ length: 11 }, () => []);
2932
+
2933
+ // Track the last anchor point data
2934
+ let lastH = NaN, lastL = NaN, lastC = NaN, lastO = NaN;
2935
+ let lastAnchorIndex = -1;
2936
+
2937
+ for (let i = 0; i < length; i++) {
2938
+ // Check if anchor triggered
2939
+ if (anchor[i]!) {
2940
+ lastAnchorIndex = i;
2941
+ // Store OHLC at anchor point (these will be used for calculations)
2942
+ lastH = high ? high[i]! : NaN;
2943
+ lastL = low ? low[i]! : NaN;
2944
+ lastC = close ? close[i]! : NaN;
2945
+ lastO = open ? open[i]! : NaN;
2946
+ }
2947
+
2948
+ // Calculate data to use
2949
+ let h: number, l: number, c: number, o: number;
2950
+
2951
+ if (isDeveloping && lastAnchorIndex >= 0) {
2952
+ // Developing: use max/min/last since anchor
2953
+ h = high ? Math.max(...high.slice(lastAnchorIndex, i + 1).filter(v => !isNaN(v))) : NaN;
2954
+ l = low ? Math.min(...low.slice(lastAnchorIndex, i + 1).filter(v => !isNaN(v))) : NaN;
2955
+ c = close ? close[i]! : NaN;
2956
+ o = open && lastAnchorIndex >= 0 ? open[lastAnchorIndex]! : NaN;
2957
+ } else {
2958
+ // Not developing: use last anchor values
2959
+ h = lastH;
2960
+ l = lastL;
2961
+ c = lastC;
2962
+ o = lastO;
2963
+ }
2964
+
2965
+ // Calculate pivot levels based on type
2966
+ const levels = calculatePivotLevels(typeStr, h, l, c, o);
2967
+
2968
+ // Push to results
2969
+ for (let j = 0; j < 11; j++) {
2970
+ results[j]!.push(levels[j]!);
2971
+ }
2972
+ }
2973
+
2974
+ return results;
2975
+ }
2976
+
2977
+ /**
2978
+ * Helper function to calculate pivot levels
2979
+ */
2980
+ function calculatePivotLevels(
2981
+ type: string,
2982
+ h: number,
2983
+ l: number,
2984
+ c: number,
2985
+ o: number
2986
+ ): number[] {
2987
+ // Return NaN array if data is insufficient
2988
+ if (isNaN(h) || isNaN(l) || isNaN(c)) {
2989
+ return Array(11).fill(NaN);
2990
+ }
2991
+
2992
+ const levels: number[] = Array(11).fill(NaN);
2993
+
2994
+ // Calculate pivot point (P)
2995
+ let P: number;
2996
+
2997
+ switch (type) {
2998
+ case 'Traditional':
2999
+ case 'Fibonacci':
3000
+ case 'Classic':
3001
+ P = (h + l + c) / 3;
3002
+ break;
3003
+ case 'Woodie':
3004
+ P = (h + l + 2 * c) / 4;
3005
+ break;
3006
+ case 'DM':
3007
+ P = (h + l + c) / 3;
3008
+ break;
3009
+ case 'Camarilla':
3010
+ P = (h + l + c) / 3;
3011
+ break;
3012
+ default:
3013
+ P = (h + l + c) / 3;
3014
+ }
3015
+
3016
+ levels[0] = P; // P is at index 0
3017
+
3018
+ // Calculate resistance and support levels based on type
3019
+ switch (type) {
3020
+ case 'Traditional':
3021
+ case 'Classic':
3022
+ levels[1] = 2 * P - l; // R1
3023
+ levels[2] = 2 * P - h; // S1
3024
+ levels[3] = P + (h - l); // R2
3025
+ levels[4] = P - (h - l); // S2
3026
+ levels[5] = h + 2 * (P - l); // R3
3027
+ levels[6] = l - 2 * (h - P); // S3
3028
+ levels[7] = levels[5] + (h - l); // R4
3029
+ levels[8] = levels[6] - (h - l); // S4
3030
+ levels[9] = levels[7] + (h - l); // R5
3031
+ levels[10] = levels[8] - (h - l); // S5
3032
+ break;
3033
+
3034
+ case 'Fibonacci':
3035
+ levels[1] = P + 0.382 * (h - l); // R1
3036
+ levels[2] = P - 0.382 * (h - l); // S1
3037
+ levels[3] = P + 0.618 * (h - l); // R2
3038
+ levels[4] = P - 0.618 * (h - l); // S2
3039
+ levels[5] = P + (h - l); // R3
3040
+ levels[6] = P - (h - l); // S3
3041
+ levels[7] = levels[5] + 0.618 * (h - l); // R4
3042
+ levels[8] = levels[6] - 0.618 * (h - l); // S4
3043
+ levels[9] = levels[7] + 0.382 * (h - l); // R5
3044
+ levels[10] = levels[8] - 0.382 * (h - l); // S5
3045
+ break;
3046
+
3047
+ case 'Woodie':
3048
+ levels[1] = 2 * P - l; // R1
3049
+ levels[2] = 2 * P - h; // S1
3050
+ levels[3] = P + (h - l); // R2
3051
+ levels[4] = P - (h - l); // S2
3052
+ levels[5] = h + 2 * (P - l); // R3
3053
+ levels[6] = l - 2 * (h - P); // S3
3054
+ levels[7] = levels[5] + (h - l); // R4
3055
+ levels[8] = levels[6] - (h - l); // S4
3056
+ levels[9] = levels[7] + (h - l); // R5
3057
+ levels[10] = levels[8] - (h - l); // S5
3058
+ break;
3059
+
3060
+ case 'DM': {
3061
+ // DM (Demark) only calculates P, R1, S1
3062
+ const x = h + l + (c * 2) + (isNaN(o) ? c : o);
3063
+ const newP = x / (isNaN(o) ? 4 : 5);
3064
+ levels[0] = newP;
3065
+ levels[1] = x / 2 - l; // R1
3066
+ levels[2] = x / 2 - h; // S1
3067
+ // R2-S5 remain NaN
3068
+ break;
3069
+ }
3070
+
3071
+ case 'Camarilla': {
3072
+ const range = h - l;
3073
+ levels[1] = c + range * 1.1 / 12; // R1
3074
+ levels[2] = c - range * 1.1 / 12; // S1
3075
+ levels[3] = c + range * 1.1 / 6; // R2
3076
+ levels[4] = c - range * 1.1 / 6; // S2
3077
+ levels[5] = c + range * 1.1 / 4; // R3
3078
+ levels[6] = c - range * 1.1 / 4; // S3
3079
+ levels[7] = c + range * 1.1 / 2; // R4
3080
+ levels[8] = c - range * 1.1 / 2; // S4
3081
+ levels[9] = h; // R5 (high)
3082
+ levels[10] = l; // S5 (low)
3083
+ break;
3084
+ }
3085
+ }
3086
+
3087
+ return levels;
3088
+ }
3089
+
3090
+ /**
3091
+ * Ichimoku Kinko Hyo (Ichimoku Cloud) - Japanese charting technique for trend identification.
3092
+ *
3093
+ * @param conversionPeriods - Period for Tenkan-sen (Conversion Line), default: 9
3094
+ * @param basePeriods - Period for Kijun-sen (Base Line), default: 26
3095
+ * @param laggingSpan2Periods - Period for Senkou Span B (Leading Span B), default: 52
3096
+ * @param displacement - Displacement for Senkou Spans and Chikou Span, default: 26
3097
+ * @param high - High price series
3098
+ * @param low - Low price series
3099
+ * @param close - Close price series
3100
+ * @returns Tuple of [tenkanSen, kijunSen, senkouSpanA, senkouSpanB, chikouSpan]
3101
+ *
3102
+ * @remarks
3103
+ * - **Tenkan-sen (Conversion Line)**: `(highest(high, conversionPeriods) + lowest(low, conversionPeriods)) / 2`
3104
+ * - **Kijun-sen (Base Line)**: `(highest(high, basePeriods) + lowest(low, basePeriods)) / 2`
3105
+ * - **Senkou Span A (Leading Span A)**: `(tenkan + kijun) / 2` offset forward by `displacement` periods
3106
+ * - **Senkou Span B (Leading Span B)**: `(highest(high, laggingSpan2Periods) + lowest(low, laggingSpan2Periods)) / 2` offset forward by `displacement` periods
3107
+ * - **Chikou Span (Lagging Span)**: `close` offset backward by `displacement` periods
3108
+ *
3109
+ * The forward offset for Senkou Spans means they are projected into the future (NaN values at the end).
3110
+ * The backward offset for Chikou Span means it shows past prices (NaN values at the beginning).
3111
+ *
3112
+ * @example
3113
+ * ```typescript
3114
+ * const [tenkan, kijun, senkouA, senkouB, chikou] = ta.ichimoku(9, 26, 52, 26, high, low, close);
3115
+ *
3116
+ * // Tenkan-sen crosses above Kijun-sen = bullish signal
3117
+ * const bullishSignal = ta.crossover(tenkan, kijun);
3118
+ *
3119
+ * // Price above cloud = bullish trend
3120
+ * // Cloud = area between senkouSpanA and senkouSpanB
3121
+ * ```
3122
+ */
3123
+ export function ichimoku(
3124
+ conversionPeriods: simple_int,
3125
+ basePeriods: simple_int,
3126
+ laggingSpan2Periods: simple_int,
3127
+ displacement: simple_int,
3128
+ high: Source,
3129
+ low: Source,
3130
+ close: Source
3131
+ ): [series_float, series_float, series_float, series_float, series_float] {
3132
+ const length = high.length;
3133
+
3134
+ // Calculate Tenkan-sen (Conversion Line): (highest(high, 9) + lowest(low, 9)) / 2
3135
+ const highestConversion = highest(high, conversionPeriods);
3136
+ const lowestConversion = lowest(low, conversionPeriods);
3137
+ const tenkanSen: series_float = [];
3138
+ for (let i = 0; i < length; i++) {
3139
+ if (isNaN(highestConversion[i]!) || isNaN(lowestConversion[i]!)) {
3140
+ tenkanSen.push(NaN);
3141
+ } else {
3142
+ tenkanSen.push((highestConversion[i]! + lowestConversion[i]!) / 2);
3143
+ }
3144
+ }
3145
+
3146
+ // Calculate Kijun-sen (Base Line): (highest(high, 26) + lowest(low, 26)) / 2
3147
+ const highestBase = highest(high, basePeriods);
3148
+ const lowestBase = lowest(low, basePeriods);
3149
+ const kijunSen: series_float = [];
3150
+ for (let i = 0; i < length; i++) {
3151
+ if (isNaN(highestBase[i]!) || isNaN(lowestBase[i]!)) {
3152
+ kijunSen.push(NaN);
3153
+ } else {
3154
+ kijunSen.push((highestBase[i]! + lowestBase[i]!) / 2);
3155
+ }
3156
+ }
3157
+
3158
+ // Calculate Senkou Span A (Leading Span A): (tenkan + kijun) / 2, offset forward by displacement
3159
+ // This means at index i, we store the value that would normally be at index (i - displacement)
3160
+ // Result: first 'displacement' values are NaN, and last 'displacement' calculated values are lost
3161
+ const senkouSpanA: series_float = [];
3162
+ for (let i = 0; i < length; i++) {
3163
+ const sourceIndex = i - displacement;
3164
+ if (sourceIndex < 0 || isNaN(tenkanSen[sourceIndex]!) || isNaN(kijunSen[sourceIndex]!)) {
3165
+ senkouSpanA.push(NaN);
3166
+ } else {
3167
+ senkouSpanA.push((tenkanSen[sourceIndex]! + kijunSen[sourceIndex]!) / 2);
3168
+ }
3169
+ }
3170
+
3171
+ // Calculate Senkou Span B (Leading Span B): (highest(high, 52) + lowest(low, 52)) / 2, offset forward by displacement
3172
+ const highestLagging = highest(high, laggingSpan2Periods);
3173
+ const lowestLagging = lowest(low, laggingSpan2Periods);
3174
+ const senkouSpanB: series_float = [];
3175
+ for (let i = 0; i < length; i++) {
3176
+ const sourceIndex = i - displacement;
3177
+ if (sourceIndex < 0 || isNaN(highestLagging[sourceIndex]!) || isNaN(lowestLagging[sourceIndex]!)) {
3178
+ senkouSpanB.push(NaN);
3179
+ } else {
3180
+ senkouSpanB.push((highestLagging[sourceIndex]! + lowestLagging[sourceIndex]!) / 2);
3181
+ }
3182
+ }
3183
+
3184
+ // Calculate Chikou Span (Lagging Span): close, offset backward by displacement
3185
+ // This means at index i, we store the close value from index (i + displacement)
3186
+ // Result: last 'displacement' values are NaN
3187
+ const chikouSpan: series_float = [];
3188
+ for (let i = 0; i < length; i++) {
3189
+ const sourceIndex = i + displacement;
3190
+ if (sourceIndex >= length || isNaN(close[sourceIndex]!)) {
3191
+ chikouSpan.push(NaN);
3192
+ } else {
3193
+ chikouSpan.push(close[sourceIndex]!);
3194
+ }
3195
+ }
3196
+
3197
+ return [tenkanSen, kijunSen, senkouSpanA, senkouSpanB, chikouSpan];
3198
+ }
3199
+
3200
+ /**
3201
+ * ZigZag indicator - identifies significant trend reversals by filtering out minor price movements.
3202
+ *
3203
+ * @param deviation - Minimum percentage price change to form a new pivot (default: 5.0)
3204
+ * @param depth - Minimum bars between pivots for pivot detection (default: 10)
3205
+ * @param backstep - Bars to look back for confirmation (default: 3)
3206
+ * @param source - Price source (optional, typically close)
3207
+ * @param high - High price series (optional, for high/low mode)
3208
+ * @param low - Low price series (optional, for high/low mode)
3209
+ * @returns Tuple of [zigzag values, direction, pivot flags (boolean)]
3210
+ *
3211
+ * @remarks
3212
+ * - **Parameter order differs from PineScript**: JavaScript signature puts deviation first,
3213
+ * while PineScript uses `ta.zigzag(source, deviation, depth, backstep)`. This allows
3214
+ * deviation to have a default value and makes the API more ergonomic for JS users.
3215
+ * - **JavaScript signature**: Requires explicit `source`, `high`, `low` OR use `createContext()`
3216
+ * - Returns the pivot price at pivot points, NaN for non-pivot bars
3217
+ * - Direction: 1 = uptrend (from low to high), -1 = downtrend (from high to low)
3218
+ * - The ZigZag indicator **repaints** by design - the last segment can change as new data arrives
3219
+ * - Supports two modes:
3220
+ * - Single source mode: uses same series for highs and lows
3221
+ * - High/Low mode: uses high for pivot highs, low for pivot lows
3222
+ *
3223
+ * @example
3224
+ * ```typescript
3225
+ * // Using high/low for more accurate pivots
3226
+ * const [zigzag, direction, isPivot] = ta.zigzag(5, 10, 3, undefined, high, low);
3227
+ *
3228
+ * // Find pivot prices
3229
+ * for (let i = 0; i < zigzag.length; i++) {
3230
+ * if (!isNaN(zigzag[i]!)) {
3231
+ * console.log(`Pivot at bar ${i}: ${zigzag[i]!}, direction: ${direction[i]! === 1 ? 'UP' : 'DOWN'}`);
3232
+ * }
3233
+ * }
3234
+ * ```
3235
+ */
3236
+ export function zigzag(
3237
+ deviation: simple_float = 5.0,
3238
+ depth: simple_int = 10,
3239
+ backstep: simple_int = 3,
3240
+ source?: Source,
3241
+ high?: Source,
3242
+ low?: Source
3243
+ ): [series_float, series_int, series_bool] {
3244
+ // Determine which mode we're in
3245
+ let highSource: Source;
3246
+ let lowSource: Source;
3247
+
3248
+ if (high && low) {
3249
+ // High/Low mode
3250
+ highSource = high;
3251
+ lowSource = low;
3252
+ } else if (source) {
3253
+ // Single source mode
3254
+ highSource = source;
3255
+ lowSource = source;
3256
+ } else {
3257
+ throw new Error(
3258
+ 'ta.zigzag() requires either source series or high/low series. ' +
3259
+ 'Either pass them explicitly or use createContext({ chart: { high, low, close } }) for implicit data.'
3260
+ );
3261
+ }
3262
+
3263
+ const length = highSource.length;
3264
+ if (lowSource.length !== length) {
3265
+ throw new Error('ta.zigzag: high and low must have the same length');
3266
+ }
3267
+
3268
+ // Result arrays
3269
+ const zigzagValues: series_float = new Array(length).fill(NaN);
3270
+ const directions: series_int = new Array(length).fill(0);
3271
+ const isPivot: series_bool = new Array(length).fill(false);
3272
+
3273
+ // Helper function to calculate percentage deviation
3274
+ const getDeviation = (price1: number, price2: number): number => {
3275
+ // Use epsilon threshold to avoid numerical instability with very small values
3276
+ if (Math.abs(price1) < 1e-10 || isNaN(price1) || isNaN(price2)) return 0;
3277
+ return Math.abs((price2 - price1) / price1) * 100;
3278
+ };
3279
+
3280
+ // Track pivot state
3281
+ interface Pivot {
3282
+ index: number;
3283
+ price: number;
3284
+ type: 'high' | 'low';
3285
+ }
3286
+
3287
+ let lastConfirmedPivot: Pivot | null = null;
3288
+ let potentialPivot: Pivot | null = null;
3289
+ let currentDirection = 0; // 0 = undefined, 1 = up, -1 = down
3290
+
3291
+ // Store confirmed pivots for final zigzag construction
3292
+ const confirmedPivots: Pivot[] = [];
3293
+
3294
+ // First pass: Find initial pivot to start
3295
+ let startIndex: number;
3296
+ let initialHighest = -Infinity;
3297
+ let initialLowest = Infinity;
3298
+ let initialHighIndex = -1;
3299
+ let initialLowIndex = -1;
3300
+
3301
+ // Find the first significant pivot using depth bars
3302
+ for (let i = 0; i < Math.min(depth, length); i++) {
3303
+ if (!isNaN(highSource[i]!) && highSource[i]! > initialHighest) {
3304
+ initialHighest = highSource[i]!;
3305
+ initialHighIndex = i;
3306
+ }
3307
+ if (!isNaN(lowSource[i]!) && lowSource[i]! < initialLowest) {
3308
+ initialLowest = lowSource[i]!;
3309
+ initialLowIndex = i;
3310
+ }
3311
+ }
3312
+
3313
+ // Determine starting direction based on which came first
3314
+ if (initialHighIndex >= 0 && initialLowIndex >= 0) {
3315
+ if (initialLowIndex <= initialHighIndex) {
3316
+ // Low came first or same - start with low, direction will be up
3317
+ lastConfirmedPivot = { index: initialLowIndex, price: initialLowest, type: 'low' };
3318
+ currentDirection = 1;
3319
+ } else {
3320
+ // High came first - start with high, direction will be down
3321
+ lastConfirmedPivot = { index: initialHighIndex, price: initialHighest, type: 'high' };
3322
+ currentDirection = -1;
3323
+ }
3324
+ confirmedPivots.push(lastConfirmedPivot);
3325
+ startIndex = lastConfirmedPivot.index + 1;
3326
+ } else {
3327
+ startIndex = depth;
3328
+ }
3329
+
3330
+ // Main loop: process bars
3331
+ for (let i = Math.max(startIndex, depth); i < length; i++) {
3332
+ const currentHigh = highSource[i]!;
3333
+ const currentLow = lowSource[i]!;
3334
+
3335
+ if (isNaN(currentHigh) || isNaN(currentLow)) {
3336
+ directions[i] = currentDirection;
3337
+ continue;
3338
+ }
3339
+
3340
+ // Check for potential pivot high
3341
+ let isPotentialHigh = true;
3342
+ for (let j = 1; j <= backstep && i - j >= 0; j++) {
3343
+ if (!isNaN(highSource[i - j]!) && highSource[i - j]! >= currentHigh) {
3344
+ isPotentialHigh = false;
3345
+ break;
3346
+ }
3347
+ }
3348
+
3349
+ // Check for potential pivot low
3350
+ let isPotentialLow = true;
3351
+ for (let j = 1; j <= backstep && i - j >= 0; j++) {
3352
+ if (!isNaN(lowSource[i - j]!) && lowSource[i - j]! <= currentLow) {
3353
+ isPotentialLow = false;
3354
+ break;
3355
+ }
3356
+ }
3357
+
3358
+ if (currentDirection === 1) {
3359
+ // Looking for pivot high (uptrend ending)
3360
+ if (isPotentialHigh) {
3361
+ if (potentialPivot === null || potentialPivot.type !== 'high') {
3362
+ // New potential high
3363
+ if (lastConfirmedPivot && getDeviation(lastConfirmedPivot.price, currentHigh) >= deviation) {
3364
+ potentialPivot = { index: i, price: currentHigh, type: 'high' };
3365
+ }
3366
+ } else {
3367
+ // Already have a potential high - update if this is higher
3368
+ if (currentHigh > potentialPivot.price) {
3369
+ potentialPivot = { index: i, price: currentHigh, type: 'high' };
3370
+ }
3371
+ }
3372
+ }
3373
+
3374
+ // Check if we should confirm the potential high and start looking for a low
3375
+ if (potentialPivot && potentialPivot.type === 'high') {
3376
+ const deviationFromPotential = getDeviation(potentialPivot.price, currentLow);
3377
+ if (deviationFromPotential >= deviation && i - potentialPivot.index >= backstep) {
3378
+ // Confirm the high pivot
3379
+ confirmedPivots.push(potentialPivot);
3380
+ lastConfirmedPivot = potentialPivot;
3381
+ currentDirection = -1;
3382
+ potentialPivot = null;
3383
+ }
3384
+ }
3385
+ } else if (currentDirection === -1) {
3386
+ // Looking for pivot low (downtrend ending)
3387
+ if (isPotentialLow) {
3388
+ if (potentialPivot === null || potentialPivot.type !== 'low') {
3389
+ // New potential low
3390
+ if (lastConfirmedPivot && getDeviation(lastConfirmedPivot.price, currentLow) >= deviation) {
3391
+ potentialPivot = { index: i, price: currentLow, type: 'low' };
3392
+ }
3393
+ } else {
3394
+ // Already have a potential low - update if this is lower
3395
+ if (currentLow < potentialPivot.price) {
3396
+ potentialPivot = { index: i, price: currentLow, type: 'low' };
3397
+ }
3398
+ }
3399
+ }
3400
+
3401
+ // Check if we should confirm the potential low and start looking for a high
3402
+ if (potentialPivot && potentialPivot.type === 'low') {
3403
+ const deviationFromPotential = getDeviation(potentialPivot.price, currentHigh);
3404
+ if (deviationFromPotential >= deviation && i - potentialPivot.index >= backstep) {
3405
+ // Confirm the low pivot
3406
+ confirmedPivots.push(potentialPivot);
3407
+ lastConfirmedPivot = potentialPivot;
3408
+ currentDirection = 1;
3409
+ potentialPivot = null;
3410
+ }
3411
+ }
3412
+ } else {
3413
+ // Initial state - determine direction based on first significant move
3414
+ if (lastConfirmedPivot) {
3415
+ if (lastConfirmedPivot.type === 'low' && getDeviation(lastConfirmedPivot.price, currentHigh) >= deviation) {
3416
+ currentDirection = 1;
3417
+ } else if (lastConfirmedPivot.type === 'high' && getDeviation(lastConfirmedPivot.price, currentLow) >= deviation) {
3418
+ currentDirection = -1;
3419
+ }
3420
+ }
3421
+ }
3422
+
3423
+ directions[i] = currentDirection;
3424
+ }
3425
+
3426
+ // Include the last potential pivot if it exists (repaint behavior)
3427
+ // This shows where the zigzag line extends to, but doesn't change direction
3428
+ const lastPotentialWasAdded = potentialPivot !== null;
3429
+ if (potentialPivot) {
3430
+ confirmedPivots.push(potentialPivot);
3431
+ }
3432
+
3433
+ // Build the final zigzag output
3434
+ for (const pivot of confirmedPivots) {
3435
+ zigzagValues[pivot.index] = pivot.price;
3436
+ isPivot[pivot.index] = true;
3437
+ }
3438
+
3439
+ // Set directions for all bars based on confirmed pivots
3440
+ // Direction represents the current trend at each bar:
3441
+ // - After a LOW pivot, trend is UP (1) until the next HIGH
3442
+ // - After a HIGH pivot, trend is DOWN (-1) until the next LOW
3443
+ let currentDir = 0;
3444
+ let pivotIdx = 0;
3445
+ const numConfirmed = lastPotentialWasAdded ? confirmedPivots.length - 1 : confirmedPivots.length;
3446
+
3447
+ for (let i = 0; i < length; i++) {
3448
+ // Move to next confirmed pivot if we passed the current one
3449
+ // Don't count the last potential pivot for direction changes
3450
+ while (pivotIdx < numConfirmed && confirmedPivots[pivotIdx]!.index <= i) {
3451
+ const pivot = confirmedPivots[pivotIdx]!;
3452
+ currentDir = pivot.type === 'low' ? 1 : -1;
3453
+ pivotIdx++;
3454
+ }
3455
+ directions[i] = currentDir;
3456
+ }
3457
+
3458
+ return [zigzagValues, directions, isPivot];
3459
+ }
3460
+
3461
+ // ── Volume built-in variables (ta.obv, ta.pvt, ta.accdist, ta.nvi, ta.pvi, ta.iii, ta.wad, ta.wvad) ──
3462
+ //
3463
+ // In PineScript these are series variables computed from the chart bars. Here they take the
3464
+ // bar series explicitly.
3465
+
3466
+ /**
3467
+ * On Balance Volume: `ta.cum(math.sign(ta.change(close)) * volume)`.
3468
+ *
3469
+ * @param close - Close price series
3470
+ * @param volume - Volume series
3471
+ * @returns OBV series (`na` on the first bar, and on bars without volume)
3472
+ */
3473
+ export function obv(close: Source, volume: Source): series_float {
3474
+ return cum(close.map((c, i) => (i === 0 ? NaN : Math.sign(c - close[i - 1]!) * volume[i]!)));
3475
+ }
3476
+
3477
+ /**
3478
+ * Price-Volume Trend: `ta.cum((ta.change(close) / close[1]) * volume)`.
3479
+ *
3480
+ * @param close - Close price series
3481
+ * @param volume - Volume series
3482
+ * @returns PVT series (`na` on the first bar, and on bars without volume)
3483
+ */
3484
+ export function pvt(close: Source, volume: Source): series_float {
3485
+ return cum(
3486
+ close.map((c, i) => (i === 0 ? NaN : ((c - close[i - 1]!) / close[i - 1]!) * volume[i]!))
3487
+ );
3488
+ }
3489
+
3490
+ /**
3491
+ * Accumulation/Distribution index: the running sum of
3492
+ * `(2 * close - low - high) / (high - low) * volume`, with 0 on bars where `high == low`.
3493
+ */
3494
+ export function accdist(high: Source, low: Source, close: Source, volume: Source): series_float {
3495
+ return cum(
3496
+ close.map((c, i) => {
3497
+ const h = high[i]!;
3498
+ const l = low[i]!;
3499
+ return h === l ? 0 : ((2 * c - l - h) / (h - l)) * volume[i]!;
3500
+ })
3501
+ );
3502
+ }
3503
+
3504
+ /**
3505
+ * Intraday Intensity Index: `(2 * close - high - low) / (high - low) * volume`.
3506
+ *
3507
+ * The PineScript built-in value is this formula. The formula shown in the PineScript reference,
3508
+ * `(2 * close - high - low) / ((high - low) * volume)`, gives other values.
3509
+ *
3510
+ * @returns III series (`na` where `high == low`)
3511
+ */
3512
+ export function iii(high: Source, low: Source, close: Source, volume: Source): series_float {
3513
+ return close.map((c, i) => {
3514
+ const range = high[i]! - low[i]!;
3515
+ return range === 0 ? NaN : ((2 * c - high[i]! - low[i]!) / range) * volume[i]!;
3516
+ });
3517
+ }
3518
+
3519
+ /**
3520
+ * Williams Variable Accumulation/Distribution: `(close - open) / (high - low) * volume`.
3521
+ *
3522
+ * @returns WVAD series (`na` where `high == low`)
3523
+ */
3524
+ export function wvad(open: Source, high: Source, low: Source, close: Source, volume: Source): series_float {
3525
+ return close.map((c, i) => {
3526
+ const range = high[i]! - low[i]!;
3527
+ return range === 0 ? NaN : ((c - open[i]!) / range) * volume[i]!;
3528
+ });
3529
+ }
3530
+
3531
+ /**
3532
+ * Williams Accumulation/Distribution: the running sum of `close - min(low, close[1])` on up
3533
+ * closes, `close - max(high, close[1])` on down closes, and 0 otherwise (first bar included).
3534
+ */
3535
+ export function wad(high: Source, low: Source, close: Source): series_float {
3536
+ return cum(
3537
+ close.map((c, i) => {
3538
+ const prev = i === 0 ? NaN : close[i - 1]!;
3539
+ const mom = c - prev;
3540
+ if (mom > 0) return c - Math.min(low[i]!, prev);
3541
+ if (mom < 0) return c - Math.max(high[i]!, prev);
3542
+ return 0;
3543
+ })
3544
+ );
3545
+ }
3546
+
3547
+ /** Volume index shared by NVI and PVI: moves with the close change on bars selected by `use`. */
3548
+ function volumeIndex(close: Source, volume: Source, use: (v: number, prevV: number) => boolean): series_float {
3549
+ const result: series_float = [];
3550
+ let prevIndex = NaN;
3551
+ for (let i = 0; i < close.length; i++) {
3552
+ const prev = !prevIndex ? 1 : prevIndex; // nz(index[1], 0) == 0 ? 1 : index[1]
3553
+ const c = close[i]!;
3554
+ const prevC = i === 0 ? NaN : close[i - 1]!;
3555
+ let value = prev;
3556
+ if (c && prevC) {
3557
+ const prevV = i === 0 || Number.isNaN(volume[i - 1]!) ? 0 : volume[i - 1]!;
3558
+ if (use(volume[i]!, prevV)) value = prev + ((c - prevC) / prevC) * prev;
3559
+ }
3560
+ result.push(value);
3561
+ prevIndex = value;
3562
+ }
3563
+ return result;
3564
+ }
3565
+
3566
+ /**
3567
+ * Negative Volume Index: moves with the close change only on bars where volume falls.
3568
+ * Starts at 1.
3569
+ */
3570
+ export function nvi(close: Source, volume: Source): series_float {
3571
+ return volumeIndex(close, volume, (v, prevV) => v < prevV);
3572
+ }
3573
+
3574
+ /**
3575
+ * Positive Volume Index: moves with the close change only on bars where volume rises.
3576
+ * Starts at 1.
3577
+ */
3578
+ export function pvi(close: Source, volume: Source): series_float {
3579
+ return volumeIndex(close, volume, (v, prevV) => v > prevV);
3580
+ }