oakscriptjs 0.8.2 → 0.9.1

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