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