opencode-pyneruntime 6.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
pynecore/lib/ta.py ADDED
@@ -0,0 +1,2230 @@
1
+ """
2
+ @pyne lib
3
+ """
4
+ from typing import TypeVar, cast, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from pynecore.types.type_checker import *
8
+
9
+ import builtins
10
+ import math
11
+ import heapq
12
+
13
+ from collections import deque
14
+
15
+ from ..types import Series, Persistent, NA, PyneFloat, PyneInt, PyneBool, na_float
16
+ from ..core.module_property import module_property, module_function_property
17
+ from pynecore.core.overload import overload
18
+
19
+ from ..core import safe_convert
20
+
21
+ # We need to use this kind of import to make transformer work
22
+ from pynecore.lib import (open, high, low, close, volume, hl2, hlc3, bar_index, array, session,
23
+ max_bars_back, math as lib_math)
24
+
25
+ TFIB = TypeVar('TFIB', float, int, bool)
26
+ TFI = TypeVar('TFI', float, int)
27
+
28
+ __all__ = [
29
+ "accdist",
30
+ "alma",
31
+ "atr",
32
+ "barssince",
33
+ "bb",
34
+ "bbw",
35
+ "cci",
36
+ "change",
37
+ "cmo",
38
+ "cog",
39
+ "correlation",
40
+ "cross",
41
+ "crossover",
42
+ "crossunder",
43
+ "cum",
44
+ "dev",
45
+ "dmi",
46
+ "ema",
47
+ "falling",
48
+ "highest",
49
+ "highestbars",
50
+ "hma",
51
+ "iii",
52
+ "kc",
53
+ "kcw",
54
+ "linreg",
55
+ "lowest",
56
+ "lowestbars",
57
+ "macd",
58
+ "max",
59
+ "median",
60
+ "mfi",
61
+ "min",
62
+ "mode",
63
+ "mom",
64
+ "nvi",
65
+ "obv",
66
+ "percentile_linear_interpolation",
67
+ "percentile_nearest_rank",
68
+ "percentrank",
69
+ "pivot_point_levels",
70
+ "pivothigh",
71
+ "pivotlow",
72
+ "pvi",
73
+ "pvt",
74
+ "range",
75
+ "rci",
76
+ "rising",
77
+ "rma",
78
+ "roc",
79
+ "rsi",
80
+ "sar",
81
+ "sma",
82
+ "stdev",
83
+ "stoch",
84
+ "supertrend",
85
+ "swma",
86
+ "tr",
87
+ "tsi",
88
+ "valuewhen",
89
+ "variance",
90
+ "vwap",
91
+ "vwma",
92
+ "wad",
93
+ "wma",
94
+ "wpr",
95
+ "wvad"
96
+ ]
97
+
98
+ #
99
+ # Helper functions
100
+ #
101
+
102
+ EPSILON = 1e-14
103
+
104
+
105
+ def _avgrank(values: list[float], val_to_compare: float) -> float:
106
+ """
107
+ Helper function to calculate rank with proper tie handling
108
+
109
+ :param values: The list of values
110
+ :param val_to_compare: The value to compare
111
+ :return: The average rank of the value
112
+ """
113
+ rank_sum = 0.0
114
+ tie_count = 0
115
+
116
+ # Count values less than current and ties
117
+ for vi in values:
118
+ diff = vi - val_to_compare
119
+ if diff > EPSILON:
120
+ rank_sum += 1
121
+ elif abs(diff) < EPSILON:
122
+ tie_count += 1
123
+
124
+ # Return average rank for ties
125
+ return rank_sum + (tie_count - 1) / 2.0 + 1
126
+
127
+
128
+ #
129
+ # Indicators
130
+ #
131
+
132
+ @module_property
133
+ def accdist() -> PyneFloat:
134
+ """
135
+ Accumulation/Distribution index
136
+ A/D = ((Close - Low) - (High - Close)) / (High - Low) * Volume + Previous A/D
137
+
138
+ :return: Accumulation/Distribution index
139
+ """
140
+ ad: Persistent[float] = 0.0
141
+
142
+ mfm = ((close - low) - (high - close)) / (high - low)
143
+ mfv = mfm * volume
144
+ if not (isinstance(mfv, NA) or mfv != mfv):
145
+ ad += mfv
146
+
147
+ return ad
148
+
149
+
150
+ def alma(source: Series[float], length: int, offset: float = 0.85, sigma: float = 6.0, floor=False) \
151
+ -> PyneFloat:
152
+ """
153
+ Calculate the Arnaud Legoux Moving Average (ALMA) of the source series with the given length.
154
+
155
+ Fun fact: ALMA means "soul" in latin and Spanish, and Portugese.
156
+ It means "apple" in Hungarian, Finnish, and Estonian.
157
+ It means "take it" in Turkish.
158
+ It means "water" in Arabic.
159
+ It means "apple tree" in Georgian.
160
+ ...
161
+
162
+ :param source: The source series
163
+ :param length: The length of the ALMA
164
+ :param offset: The offset of the ALMA
165
+ :param sigma: The sigma value of the ALMA
166
+ :param floor: Specifies whether the offset calculation is floored before ALMA is calculated. Default value is false
167
+ :return: The ALMA of the source series
168
+ """
169
+ assert length > 0, "Invalid length, length must be greater than 0!"
170
+ if (isinstance(source, NA) or source != source):
171
+ return na_float
172
+ length = int(length)
173
+
174
+ # Use persistent weights to avoid recalculation
175
+ weights: Persistent[list[float]] = []
176
+ norm: Persistent[float] = 0.0
177
+
178
+ # Calculate weights only once
179
+ if not weights:
180
+ m = offset * (length - 1) if not floor else math.floor(offset * (length - 1))
181
+ s = length / sigma
182
+ weights = [math.exp(-1 * ((i - m) * (i - m)) / (2 * s * s)) for i in builtins.range(length)]
183
+ weights.reverse() # This is faster then using backward range or index subtraction
184
+ norm = sum(weights)
185
+
186
+ # Vectorized calculation using dot product
187
+ summ = 0.0
188
+ for i, w in enumerate(weights):
189
+ summ += w * source[i]
190
+ return summ / norm
191
+
192
+
193
+ def atr(length: int) -> PyneFloat:
194
+ """
195
+ Calculate Average True Range (ATR) of the source series with the given length.
196
+
197
+ :param length: The length of the ATR
198
+ :return: The ATR of the source series
199
+ """
200
+ assert length > 0, "Invalid length, length must be greater than 0!"
201
+ return rma(tr(True), length)
202
+
203
+
204
+ def barssince(condition: bool) -> PyneInt:
205
+ """
206
+ Calculate the number of bars since the condition was true.
207
+
208
+ :param condition: The condition to check
209
+ :return: The number of bars since the condition was true
210
+ """
211
+ counter: Persistent[int] = -1
212
+ if condition:
213
+ counter = 0
214
+ elif counter == -1:
215
+ return NA(int)
216
+ else:
217
+ counter += 1
218
+ return counter
219
+
220
+
221
+ def bb(source: float, length: int, mult: float | int) -> tuple[PyneFloat, PyneFloat, PyneFloat]:
222
+ """
223
+ Calculate the Bollinger Bands (BB) of the source series with the given length and multiplier.
224
+
225
+ :param source: The source series
226
+ :param length: The length of the BB
227
+ :param mult: The multiplier of the BB
228
+ :return: The Bollinger Bands (BB) of the source series
229
+ """
230
+ assert length > 0, "Invalid length, length must be greater than 0!"
231
+ assert mult > 0, "Invalid multiplier, multiplier must be greater than 0!"
232
+
233
+ std_dev = stdev(source, length)
234
+
235
+ middle = sma(source, length)
236
+
237
+ if (isinstance(middle, NA) or middle != middle):
238
+ return na_float, na_float, na_float
239
+ std_dev *= mult
240
+ return middle, middle + std_dev, middle - std_dev
241
+
242
+
243
+ def bbw(source: float, length: int, mult: float | int) -> PyneFloat:
244
+ """
245
+ Calculate the Bollinger Bands Width (BBW) of the source series with the given length and multiplier.
246
+
247
+ :param source: The source series
248
+ :param length: The length of the BBW
249
+ :param mult: The multiplier of the BBW
250
+ :return: The Bollinger Bands Width (BBW) of the source series
251
+ """
252
+ b, h, l = bb(source, length, mult)
253
+ if (isinstance(b, NA) or b != b) or b == 0.0:
254
+ return na_float
255
+ return ((h - l) / b) * 100
256
+
257
+
258
+ def cci(source: float, length: int) -> PyneFloat:
259
+ """
260
+ Calculate the Commodity Channel Index (CCI) of the source series with the given length.
261
+
262
+ :param source: The source series
263
+ :param length: The length of the CCI
264
+ :return: The Commodity Channel Index (CCI) of the source series
265
+ """
266
+ mean = sma(source, length)
267
+ mdev = dev(source, length, _mean=mean)
268
+ if (isinstance(mdev, NA) or mdev != mdev):
269
+ return na_float
270
+ return (source - mean) / (0.015 * mdev)
271
+
272
+
273
+ def change(source: Series[TFIB], length: int = 1) -> TFIB:
274
+ """
275
+ Calculate a simple change with respect to the given bar offset.
276
+
277
+ :param source: The source series
278
+ :param length: The offset in bars
279
+ :return: The change from source to source[length]
280
+ """
281
+ assert length > 0, "Invalid length, length must be greater than 0!"
282
+ length = int(length)
283
+ # Grow the buffer so ``source[length]`` stays addressable for lengths beyond the
284
+ # per-series default max_bars_back (500); otherwise it reads na and the change is na.
285
+ max_bars_back(source, length)
286
+
287
+ # We need to round to prevent problems caused by floating point precision
288
+ if isinstance(source, (float, int)):
289
+ # The cast is for pyright: it types round(float) as float, which is not
290
+ # assignable back to the TFIB-typed parameter; PyCharm resolves it to
291
+ # TFIB already.
292
+ # noinspection PyUnnecessaryCast
293
+ source = cast(TFIB, round(source, 14))
294
+ prev_val = source[length] # noqa
295
+
296
+ if (isinstance(source, NA) or source != source):
297
+ # type(source) would be the NA class itself — keep the source sentinel's type
298
+ return cast(TFIB, source)
299
+ if (isinstance(prev_val, NA) or prev_val != prev_val):
300
+ return NA(type(source))
301
+ if isinstance(source, float):
302
+ return cast(TFIB, round(source - prev_val, 14)) # noqa
303
+ if isinstance(source, int):
304
+ return cast(TFIB, source - prev_val) # noqa
305
+ return source != prev_val
306
+
307
+
308
+ def cmo(source: float, length: int) -> PyneFloat:
309
+ """
310
+ Calculate the Chande Momentum Oscillator (CMO) of the source series with the given length.
311
+
312
+ :param source: The source series
313
+ :param length: The length of the CMO
314
+ :return: The Chande Momentum Oscillator (CMO) of the source series
315
+ """
316
+ momentum = change(source)
317
+ if (isinstance(momentum, NA) or momentum != momentum):
318
+ return na_float
319
+ sum1 = lib_math.sum(momentum if momentum >= 0.0 else 0.0, length)
320
+ sum2 = lib_math.sum(0.0 if momentum >= 0.0 else -momentum, length)
321
+ return 100 * (sum1 - sum2) / (sum1 + sum2)
322
+
323
+
324
+ # noinspection PyUnusedLocal,PyShadowingBuiltins
325
+ def cog(source: Series[float], length: int) -> PyneFloat:
326
+ """
327
+ Calculate the Center of Gravity (COG) of the source series with the given length.
328
+
329
+ :param source: The source series
330
+ :param length: The length of the COG
331
+ :return: The Center of Gravity (COG) of the source series
332
+ """
333
+ count: Persistent[int] = 0
334
+ summ: Persistent[float] = 0.0
335
+ weighted_summ: Persistent[float] = 0.0
336
+ val: Persistent[float] = na_float
337
+
338
+ if (isinstance(source, NA) or source != source):
339
+ # An NA bar leaves the window unchanged; hold the last full value
340
+ # (still NA while warming up)
341
+ return na_float if count < length else val
342
+
343
+ # NA values are NOT stored in the buffer, only skipped, so ``src[length]``
344
+ # indexes past NA gaps to the true oldest value still inside the window.
345
+ # Reading the parameter directly would step back ``length`` *bars* and land
346
+ # inside an NA gap, subtracting an NA that poisons ``summ`` forever.
347
+ src: Series[float] = source
348
+ # Grow the na-compacted buffer so ``src[length]`` stays addressable for lengths
349
+ # beyond the per-series default max_bars_back (500); otherwise the window-drop
350
+ # read returns na and poisons ``summ`` permanently.
351
+ max_bars_back(src, int(length))
352
+
353
+ # Warming up phase — only non-NA samples advance the window
354
+ if count < length:
355
+ count += 1
356
+ summ += source
357
+ weighted_summ += source * (length - count)
358
+ if count < length:
359
+ return na_float
360
+
361
+ # Normal calculation phase
362
+ else:
363
+ new_summ = summ + source - src[length]
364
+ weighted_summ = weighted_summ + summ - length * src[length]
365
+ summ = new_summ
366
+ val = -weighted_summ / summ - 1.0
367
+ return val
368
+
369
+
370
+ def correlation(source1: Series[float], source2: Series[float], length: int) -> PyneFloat:
371
+ """
372
+ Calculate the correlation of the source series with the given length.
373
+
374
+ NOTE: It is about 7 digits accurate to the result of Pine Script's correlation function. There
375
+ are a lot of floating point operations, and even the order matters. I cannot found a
376
+ better matching calculation.
377
+
378
+ :param source1: The first source series
379
+ :param source2: The second source series
380
+ :param length: The length of the correlation
381
+ :return: The correlation of the source series
382
+ """
383
+ assert length > 0, "Length must be greater than 0"
384
+ length = int(length)
385
+ # Grow both buffers so ``source1/source2[length]`` stay addressable for lengths beyond
386
+ # the per-series default max_bars_back (500); otherwise the window-drop reads na and
387
+ # poisons the running sums permanently.
388
+ max_bars_back(source1, length)
389
+ max_bars_back(source2, length)
390
+
391
+ sum_x: Persistent[float] = 0.0
392
+ sum_y: Persistent[float] = 0.0
393
+ sum_xy: Persistent[float] = 0.0
394
+ sum_x2: Persistent[float] = 0.0
395
+ sum_y2: Persistent[float] = 0.0
396
+ count: Persistent[int] = 0
397
+ na_window: Persistent[int] = 0
398
+
399
+ cur_na = (isinstance(source1, NA) or source1 != source1) or (isinstance(source2, NA) or source2 != source2)
400
+
401
+ if count < length:
402
+ # Still filling the window; no bar leaves it yet.
403
+ if cur_na:
404
+ na_window += 1
405
+ else:
406
+ sum_x += source1
407
+ sum_y += source2
408
+ sum_xy += source1 * source2
409
+ sum_x2 += source1 * source1
410
+ sum_y2 += source2 * source2
411
+ count += 1
412
+ if count < length:
413
+ return na_float
414
+ else:
415
+ old1 = source1[length]
416
+ old2 = source2[length]
417
+ old_na = (isinstance(old1, NA) or old1 != old1) or (isinstance(old2, NA) or old2 != old2)
418
+ if not cur_na and not old_na:
419
+ # na-free fast path: same operations and order as before, to keep the
420
+ # documented ~7-digit match with Pine's correlation.
421
+ sum_x += source1 - old1
422
+ sum_y += source2 - old2
423
+ sum_xy += (source1 * source2) - (old1 * old2)
424
+ sum_x2 += (source1 * source1) - (old1 * old1)
425
+ sum_y2 += (source2 * source2) - (old2 * old2)
426
+ else:
427
+ # A na entered or left the window: keep the running sums over the
428
+ # valid bars only and track how many na sit in the window.
429
+ if cur_na:
430
+ na_window += 1
431
+ else:
432
+ sum_x += source1
433
+ sum_y += source2
434
+ sum_xy += source1 * source2
435
+ sum_x2 += source1 * source1
436
+ sum_y2 += source2 * source2
437
+ if old_na:
438
+ na_window -= 1
439
+ else:
440
+ sum_x -= old1
441
+ sum_y -= old2
442
+ sum_xy -= old1 * old2
443
+ sum_x2 -= old1 * old1
444
+ sum_y2 -= old2 * old2
445
+
446
+ if na_window:
447
+ return na_float
448
+ try:
449
+ numerator = (length * sum_xy) - (sum_x * sum_y)
450
+ denominator = math.sqrt((length * sum_x2 - sum_x * sum_x) * (length * sum_y2 - sum_y * sum_y))
451
+ return numerator / denominator
452
+ except (ValueError, ZeroDivisionError):
453
+ return na_float
454
+
455
+
456
+ def cross(source1: float, source2: float) -> PyneBool:
457
+ """
458
+ Check if the source series crossed over or under the given series.
459
+
460
+ :param source1: The first source series
461
+ :param source2: The second source series
462
+ :return: True if the source series crossed over the given series
463
+ """
464
+ return crossover(source1, source2) or crossunder(source1, source2)
465
+
466
+
467
+ # noinspection PyUnusedLocal
468
+ def crossover(source1: float, source2: float) -> PyneBool:
469
+ """
470
+ Check if the source series crossed over the given series.
471
+
472
+ :param source1: The first source series
473
+ :param source2: The second source series
474
+ :return: True if the source series crossed over the given series
475
+ """
476
+ l1_lte_l2: Persistent[bool] = NA(bool)
477
+ res = source1 > source2 and l1_lte_l2
478
+ # Only refresh the relation on bars where it is defined; TV compares against the
479
+ # last bar with both sources present, so na gaps must not reset the state
480
+ if not ((isinstance(source1, NA) or source1 != source1) or (isinstance(source2, NA) or source2 != source2)):
481
+ l1_lte_l2 = source1 <= source2
482
+ return res
483
+
484
+
485
+ # noinspection PyUnusedLocal
486
+ def crossunder(source1: float, source2: float) -> PyneBool:
487
+ """
488
+ Check if the source series crossed under the given series.
489
+
490
+ :param source1: The first source series
491
+ :param source2: The second source series
492
+ :return: True if the source series crossed under the given series
493
+ """
494
+ l1_gte_l2: Persistent[bool] = NA(bool)
495
+ res = source1 < source2 and l1_gte_l2
496
+ # Only refresh the relation on bars where it is defined; TV compares against the
497
+ # last bar with both sources present, so na gaps must not reset the state
498
+ if not ((isinstance(source1, NA) or source1 != source1) or (isinstance(source2, NA) or source2 != source2)):
499
+ l1_gte_l2 = source1 >= source2
500
+ return res
501
+
502
+
503
+ def cum(source: Series[float | int]) -> PyneFloat:
504
+ """
505
+ Calculate the cumulative sum of the source series.
506
+
507
+ :param source: The source series
508
+ :return: The cumulative sum of the source series
509
+ """
510
+ if (isinstance(source, NA) or source != source):
511
+ return na_float
512
+ var: Persistent[float] = 0.0
513
+ var += source
514
+ return var
515
+
516
+
517
+ def dev(source: Series[float], length: int, _mean: PyneFloat | None = None) -> PyneFloat:
518
+ """
519
+ Calculate the Mean Absolute Deviation (MAD) of the source series with the given length.
520
+
521
+ :param source: The source series
522
+ :param length: The length of the MAD calculation
523
+ :param _mean: The mean value of the source series, if it is already calculated
524
+ :return: The mean absolute deviation of the source series
525
+ """
526
+ assert length > 0, "Invalid length, length must be greater than 0!"
527
+ if length == 1:
528
+ return 0.0
529
+ length = int(length)
530
+
531
+ mean = _mean if _mean is not None else sma(source, length)
532
+ if (isinstance(mean, NA) or mean != mean):
533
+ return na_float
534
+
535
+ summ = 0.0
536
+ for i in builtins.range(length):
537
+ summ += abs(source[i] - mean)
538
+
539
+ return summ / length
540
+
541
+
542
+ # noinspection PyPep8Naming
543
+ def dmi(diLength: int, adxSmoothing: int) -> tuple[PyneFloat, PyneFloat, PyneFloat]:
544
+ """
545
+ Calculate the Directional Movement Index (DMI) of the source series with the given DI length and ADX smoothing.
546
+
547
+ :param diLength: The length of the DI
548
+ :param adxSmoothing: The smoothing of the ADX
549
+ :return: Tuple of three DMI series:
550
+ - Positive Directional Movement (+DI)
551
+ - Negative Directional Movement (-DI)
552
+ - Average Directional Movement Index (ADX)
553
+ """
554
+ assert diLength > 0, "Invalid DI length, DI length must be greater than 0!"
555
+ assert adxSmoothing > 0, "Invalid ADX smoothing, ADX smoothing must be greater than 0!"
556
+ up = change(high)
557
+ down = -change(low)
558
+ if (isinstance(up, NA) or up != up) or (isinstance(down, NA) or down != down):
559
+ return na_float, na_float, na_float
560
+ a = atr(diLength)
561
+ plus_dm = up if (up > down and up > 0.0) else 0.0
562
+ minus_dm = down if (down > up and down > 0.0) else 0.0
563
+ p = rma(plus_dm, diLength)
564
+ m = rma(minus_dm, diLength)
565
+ if (isinstance(a, NA) or a != a) or (isinstance(p, NA) or p != p) or (isinstance(m, NA) or m != m) or a == 0.0:
566
+ return na_float, na_float, na_float
567
+ p = 100 * p / a
568
+ m = 100 * m / a
569
+ summ = p + m
570
+ adx = rma(abs(p - m) / (summ if summ != 0.0 else 1.0), adxSmoothing) * 100
571
+ return p, m, adx
572
+
573
+
574
+ def ema(source: PyneFloat, length: int, _alpha: float | None = None) -> PyneFloat:
575
+ """
576
+ Calculate the Exponential Moving Average (EMA) of the source series with the given length.
577
+
578
+ :param source: The source series
579
+ :param length: The length of the EMA
580
+ :param _alpha: The alpha value for EMA calculation (it is a private argument)
581
+ :return:
582
+ """
583
+ assert length > 0, "Invalid length, length must be greater than 0!"
584
+ length = int(length)
585
+ if length == 1: # Shortcut
586
+ return source
587
+
588
+ if (isinstance(source, NA) or source != source):
589
+ return na_float
590
+
591
+ alpha: Persistent[float] = _alpha or (2 / (length + 1))
592
+ last_val: Persistent[float] = na_float
593
+
594
+ # Use SMA at warming stage
595
+ if (isinstance(last_val, NA) or last_val != last_val):
596
+ last_val = sma(source, length)
597
+ return last_val
598
+
599
+ # Warmed result
600
+ last_val = alpha * source + (1 - alpha) * last_val
601
+ return last_val
602
+
603
+
604
+ # noinspection PyUnusedLocal
605
+ def falling(source: float, length: int) -> bool:
606
+ """
607
+ Test if the source series is now falling for length bars long.
608
+
609
+ :param source: The source series
610
+ :param length: The length of the falling test
611
+ :return: True if the source series is falling for length bars long
612
+ """
613
+ assert length > 0, "Invalid length, length must be greater than 0!"
614
+ length = int(length)
615
+
616
+ last_val: Persistent[float] = na_float
617
+ counter: Persistent[int] = 0
618
+
619
+ if (isinstance(last_val, NA) or last_val != last_val):
620
+ last_val = source
621
+ return False
622
+
623
+ if source < last_val:
624
+ counter += 1
625
+ else:
626
+ counter = 0
627
+
628
+ last_val = source
629
+ return counter >= length
630
+
631
+
632
+ # noinspection PyUnusedLocal,DuplicatedCode
633
+ @overload
634
+ def highest(source: Series[float], length: int, _bars: bool = False, _tuple: bool = False, _check_eq: bool = False) \
635
+ -> PyneFloat:
636
+ """
637
+ Calculate the highest value of the source series with the given length.
638
+
639
+ :param source: The source series
640
+ :param length: The length of the highest value
641
+ :param _bars: If true, return the number of bars since the highest value, internal use only
642
+ :param _tuple: If true, return a tuple of the highest value and the number of bars since the highest value,
643
+ internal use only
644
+ :param _check_eq: If true, check for equality too, internal use only
645
+ :return: The highest value of the source series
646
+ """
647
+ last_max: Persistent[float] = na_float
648
+ last_max_index: Persistent[int] = 0
649
+
650
+ if last_max < source or (isinstance(last_max, NA) or last_max != last_max) or (_check_eq and last_max == source):
651
+ last_max = source
652
+ last_max_index = 0
653
+
654
+ if last_max_index >= length:
655
+ last_max = source
656
+ last_max_index = 0
657
+ for i in builtins.range(1, length):
658
+ s = source[i]
659
+ if s > last_max:
660
+ last_max = s
661
+ last_max_index = i
662
+ elif not _check_eq and s == last_max:
663
+ # For normal highest: update index for equal values
664
+ last_max_index = i
665
+ # For pivot detection (_check_eq=True): don't update index for equal values
666
+
667
+ max_index = last_max_index
668
+ last_max_index += 1
669
+
670
+ if bar_index < length - 1:
671
+ return na_float if not _tuple else (na_float, na_float) # type: ignore[return-value]
672
+
673
+ if _bars:
674
+ return -max_index
675
+ if _tuple:
676
+ return last_max, -max_index # type: ignore[return-value]
677
+ return last_max
678
+
679
+
680
+ @overload
681
+ def highest(length: int) -> PyneFloat:
682
+ return highest(high, length)
683
+
684
+
685
+ # noinspection PyUnusedLocal
686
+ @overload
687
+ def highestbars(source: Series[float], length: int) -> PyneFloat:
688
+ """
689
+ Calculate the number of bars since the highest value of the source series with the given length.
690
+
691
+ :param source: The source series
692
+ :param length: The length of the highest value
693
+ :return: The number of bars since the highest value of the source series
694
+ """
695
+ return highest(source, length, _bars=True)
696
+
697
+
698
+ @overload
699
+ def highestbars(length: int) -> PyneFloat:
700
+ return highest(high, length, _bars=True)
701
+
702
+
703
+ def hma(source: float, length: int) -> PyneFloat:
704
+ """
705
+ Calculate the Hull Moving Average (HMA) of the source series with the given length.
706
+
707
+ :param source: The source series
708
+ :param length: The length of the HMA
709
+ :return: The Hull Moving Average (HMA) of the source series
710
+ """
711
+ assert length > 0, "Invalid length, length must be greater than 0!"
712
+ if (isinstance(source, NA) or source != source):
713
+ return na_float
714
+ length = int(length)
715
+
716
+ ma_np2 = wma(source, length // 2)
717
+ ma = wma(source, length)
718
+ if (isinstance(ma, NA) or ma != ma) or (isinstance(ma_np2, NA) or ma_np2 != ma_np2):
719
+ return na_float
720
+ return wma(2 * ma_np2 - ma, int(length ** 0.5))
721
+
722
+
723
+ @module_property
724
+ def iii() -> PyneFloat:
725
+ """
726
+ Intraday Intensity Index.
727
+
728
+ :return: Intraday Intensity Index
729
+ """
730
+ return (2 * close - high - low) / ((high - low) * volume)
731
+
732
+
733
+ # noinspection PyPep8Naming
734
+ def kc(series: float, length: int, mult: float | int, useTrueRange: bool = True) \
735
+ -> tuple[PyneFloat, PyneFloat, PyneFloat]:
736
+ """
737
+ Calculate the Keltner Channels (KC) of the source series with the given length and multiplier.
738
+
739
+ :param series: The source series
740
+ :param length: The length of the KC
741
+ :param mult: The multiplier of the KC
742
+ :param useTrueRange: Specifies whether to use True Range for KC calculation
743
+ :return: The Keltner Channels (KC) of the source series
744
+ """
745
+ assert length > 0, "Invalid length, length must be greater than 0!"
746
+ assert mult > 0, "Invalid multiplier, multiplier must be greater than 0!"
747
+
748
+ base = ema(series, length)
749
+ span = tr(False) if useTrueRange else (high - low)
750
+ range_ma = ema(span, length)
751
+ if (isinstance(base, NA) or base != base):
752
+ return na_float, na_float, na_float
753
+ if (isinstance(range_ma, NA) or range_ma != range_ma):
754
+ return base, na_float, na_float
755
+ range_ma *= mult
756
+ return base, base + range_ma, base - range_ma
757
+
758
+
759
+ # noinspection PyPep8Naming
760
+ def kcw(series: float, length: int, mult: float | int, useTrueRange: bool = True) -> PyneFloat:
761
+ """
762
+ Calculate the Keltner Channels Width (KCW) of the source series with the given length and multiplier.
763
+
764
+ :param series: The source series
765
+ :param length: The length of the KCW
766
+ :param mult: The multiplier of the KCW
767
+ :param useTrueRange: Specifies whether to use True Range for KCW calculation
768
+ :return: The Keltner Channels Width (KCW) of the source series
769
+ """
770
+ b, h, l = kc(series, length, mult, useTrueRange)
771
+ if (isinstance(b, NA) or b != b) or b == 0.0:
772
+ return na_float
773
+ return (h - l) / b
774
+
775
+
776
+ def linreg(source: Series[float], length: int, offset: int) -> PyneFloat:
777
+ """
778
+ Computes the linear regression value of the source series over a given period.
779
+
780
+ :param source: Input series
781
+ :param length: Number of bars to calculate regression
782
+ :param offset: Number of bars to shift the result
783
+ :return: Linear regression value
784
+ """
785
+ assert length > 0, "Invalid length, must be greater than 0!"
786
+ if length == 1:
787
+ return source
788
+ length = int(length)
789
+ window_size = length
790
+
791
+ # Precomputed constants for x-coordinates
792
+ sum_x = window_size * (window_size - 1) / 2.0
793
+ sum_x2 = (window_size - 1) * window_size * (2 * window_size - 1) / 6.0
794
+
795
+ # Persistent state variables
796
+ bar_count: Persistent[int] = 0
797
+ sum_y: Persistent[float] = 0.0 # Sum of source values in the window
798
+ sum_xy: Persistent[float] = 0.0 # Weighted sum: sum((window_size - 1 - i) * source[i])
799
+
800
+ if (isinstance(source, NA) or source != source):
801
+ if bar_count < window_size:
802
+ return na_float
803
+ # An NA bar leaves the window unchanged; fall through to report the held value
804
+ else:
805
+ # NA values are NOT stored in the buffer, only skipped, so ``src[window_size]``
806
+ # indexes past NA gaps to the true oldest value still inside the window.
807
+ # Reading the parameter directly would step back ``window_size`` *bars* and
808
+ # land inside an NA gap, subtracting an NA that poisons ``sum_y`` forever.
809
+ src: Series[float] = source
810
+
811
+ # Warm-up phase: accumulate values until the window is full
812
+ if bar_count < window_size:
813
+ prev_sum_y = sum_y
814
+ sum_y = prev_sum_y + source
815
+ sum_xy = (window_size - 1) * source + sum_xy - prev_sum_y
816
+ bar_count += 1
817
+
818
+ # Return NA until we have enough data
819
+ if bar_count < window_size:
820
+ return na_float
821
+ else:
822
+ # Rolling update: remove the oldest value when the window is full
823
+ dropped_value = src[window_size]
824
+ prev_sum_y = sum_y
825
+ sum_y = prev_sum_y + source - dropped_value
826
+ sum_xy = (window_size - 1) * source + sum_xy - prev_sum_y + dropped_value
827
+
828
+ # Compute slope and intercept
829
+ denominator = window_size * sum_x2 - sum_x * sum_x
830
+ slope = (window_size * sum_xy - sum_x * sum_y) / denominator
831
+ intercept = (sum_y - slope * sum_x) / window_size
832
+
833
+ # Compute final regression value
834
+ return intercept + slope * ((window_size - 1) - offset)
835
+
836
+
837
+ # noinspection PyUnusedLocal,DuplicatedCode
838
+ @overload
839
+ def lowest(source: Series[float], length: int,
840
+ _bars: bool = False, _tuple: bool = False, _check_eq: bool = False) \
841
+ -> PyneFloat:
842
+ """
843
+ Calculate the lowest value of the source series with the given length.
844
+
845
+ :param source: The source series
846
+ :param length: The length of the lowest value
847
+ :param _bars: If true, return the number of bars since the lowest value, internal use only
848
+ :param _tuple: If true, return a tuple of the lowest value and the number of bars since the lowest value,
849
+ Internal use only
850
+ :param _check_eq: If true, check for equality too, internal use only
851
+ :return: The lowest value of the source series
852
+ """
853
+ last_min: Persistent[float] = na_float
854
+ last_min_index: Persistent[int] = 0
855
+
856
+ if last_min > source or (isinstance(last_min, NA) or last_min != last_min) or (_check_eq and last_min == source):
857
+ last_min = source
858
+ last_min_index = 0
859
+
860
+ if last_min_index >= length:
861
+ last_min = source
862
+ last_min_index = 0
863
+ for i in builtins.range(1, length):
864
+ s = source[i]
865
+ if s < last_min:
866
+ last_min = s
867
+ last_min_index = i
868
+ elif not _check_eq and s == last_min:
869
+ # For normal lowest: update index for equal values
870
+ last_min_index = i
871
+ # For pivot detection (_check_eq=True): don't update index for equal values
872
+
873
+ min_index = last_min_index
874
+ last_min_index += 1
875
+
876
+ if bar_index < length - 1:
877
+ return na_float if not _tuple else (na_float, NA(int)) # type: ignore[return-value]
878
+
879
+ if _bars:
880
+ return -min_index
881
+ if _tuple:
882
+ return last_min, -min_index # type: ignore[return-value]
883
+ return last_min
884
+
885
+
886
+ @overload
887
+ def lowest(length: int) -> PyneFloat:
888
+ return lowest(low, length)
889
+
890
+
891
+ # noinspection PyUnusedLocal
892
+ @overload
893
+ def lowestbars(source: Series[float], length: int) -> PyneFloat:
894
+ """
895
+ Calculate the number of bars since the lowest value of the source series with the given length.
896
+
897
+ :param source: The source series
898
+ :param length: The length of the lowest value
899
+ :return: The number of bars since the lowest value of the source series
900
+ """
901
+ return lowest(source, length, _bars=True)
902
+
903
+
904
+ @overload
905
+ def lowestbars(length: int) -> PyneFloat:
906
+ return lowest(low, length, _bars=True)
907
+
908
+
909
+ def macd(source: float, fastlen: int, slowlen: int, siglen: int) \
910
+ -> tuple[PyneFloat, PyneFloat, PyneFloat]:
911
+ """
912
+ Calculate the Moving Average Convergence Divergence (MACD) of the source series with the given
913
+ fast, slow, and signal lengths.
914
+
915
+ :param source: The source series
916
+ :param fastlen: The length of the fast EMA
917
+ :param slowlen: The length of the slow EMA
918
+ :param siglen: The length of the signal EMA
919
+ :return: Tuple of three MACD series:
920
+ - MACD Line
921
+ - Signal Line
922
+ - Histogram
923
+ """
924
+ assert fastlen > 0, "Invalid fast length, fast length must be greater than 0!"
925
+ assert slowlen > 0, "Invalid slow length, slow length must be greater than 0!"
926
+ assert siglen > 0, "Invalid signal length, signal length must be greater than 0!"
927
+ fast = ema(source, fastlen)
928
+ slow = ema(source, slowlen)
929
+ if (isinstance(fast, NA) or fast != fast) or (isinstance(slow, NA) or slow != slow):
930
+ return na_float, na_float, na_float
931
+ macd_val = fast - slow
932
+ signal = ema(macd_val, siglen)
933
+ if (isinstance(signal, NA) or signal != signal):
934
+ return macd_val, na_float, na_float
935
+ return macd_val, signal, macd_val - signal
936
+
937
+
938
+ # noinspection PyShadowingBuiltins
939
+ def max(source: Series[float]) -> PyneFloat:
940
+ """
941
+ Calculate the maximum value of the source series.
942
+
943
+ :param source: The source series
944
+ :return: The maximum value of the source series
945
+ """
946
+ max_val: Persistent[float] = na_float
947
+ if max_val < source or (isinstance(max_val, NA) or max_val != max_val):
948
+ max_val = source
949
+ return max_val
950
+
951
+
952
+ def median(source: Series[TFI], length: int) -> TFI:
953
+ """
954
+ Calculate the median of the source series over a given period.
955
+
956
+ :param source: Input series of values
957
+ :param length: Number of bars to calculate over
958
+ :return: The median value or na during warmup
959
+ """
960
+ assert length > 0, "Invalid length, length must be greater than 0!"
961
+ if length == 1: # Shortcut
962
+ return source
963
+ length = int(length)
964
+
965
+ if (isinstance(source, NA) or source != source):
966
+ # type(source) would be the NA class itself — keep the source sentinel's type
967
+ return cast(TFI, source)
968
+
969
+ # Store heaps and window
970
+ heap_low: Persistent[list[TFI]] = [] # Max heap (negative values)
971
+ heap_high: Persistent[list[TFI]] = [] # Min heap
972
+ window: Persistent[list[TFI]] = [] # Recent values for removal
973
+
974
+ # Add new value and balance heaps
975
+ value = source
976
+ window.append(value)
977
+ heapq.heappush(heap_low, -value)
978
+ heapq.heappush(heap_high, -heapq.heappop(heap_low))
979
+
980
+ if len(heap_low) < len(heap_high):
981
+ heapq.heappush(heap_low, -heapq.heappop(heap_high))
982
+
983
+ # Remove old value if window full
984
+ if len(window) > length:
985
+ old = window.pop(0)
986
+
987
+ # Remove from correct heap
988
+ if old <= -heap_low[0]:
989
+ heap_low.remove(-old)
990
+ heapq.heapify(heap_low)
991
+ else:
992
+ heap_high.remove(old)
993
+ heapq.heapify(heap_high)
994
+
995
+ # Rebalance if needed
996
+ if len(heap_low) < len(heap_high):
997
+ heapq.heappush(heap_low, -heapq.heappop(heap_high))
998
+ elif len(heap_low) > len(heap_high) + 1:
999
+ heapq.heappush(heap_high, -heapq.heappop(heap_low))
1000
+
1001
+ # Return na during warmup
1002
+ if len(window) < length:
1003
+ return NA(cast(type[TFI], type(source))) # type: ignore
1004
+
1005
+ # Return median based on heap sizes
1006
+ if len(heap_low) > len(heap_high):
1007
+ return -heap_low[0] # Max heap root
1008
+ return -heap_low[0] if isinstance(source, int) else (-heap_low[0] + heap_high[0]) / 2 # type: ignore
1009
+
1010
+
1011
+ def mfi(source: float, length: int) -> PyneFloat:
1012
+ """
1013
+ Calculate the Money Flow Index (MFI) of the source series with the given length.
1014
+
1015
+ :param source: The source series
1016
+ :param length: The length of the MFI
1017
+ :return: The Money Flow Index (MFI) of the source series
1018
+ """
1019
+ assert length > 0, "Invalid length, length must be greater than 0!"
1020
+ if (isinstance(source, NA) or source != source):
1021
+ return na_float
1022
+ length = int(length)
1023
+
1024
+ chg = change(source)
1025
+ upper = lib_math.sum(volume * (0.0 if not (isinstance(chg, NA) or chg != chg) and chg <= 0 else source), length)
1026
+ lower = lib_math.sum(volume * (0.0 if not (isinstance(chg, NA) or chg != chg) and chg >= 0 else source), length)
1027
+ if (isinstance(upper, NA) or upper != upper) or (isinstance(lower, NA) or lower != lower):
1028
+ return na_float
1029
+ return 100.0 - (100 * lower / (upper + lower))
1030
+
1031
+
1032
+ # noinspection PyShadowingBuiltins
1033
+ def min(source: Series[float]) -> PyneFloat:
1034
+ """
1035
+ Calculate the minimum value of the source series.
1036
+
1037
+ :param source: The source series
1038
+ :return: The minimum value of the source series
1039
+ """
1040
+ min_val: Persistent[float] = na_float
1041
+ if min_val > source or (isinstance(min_val, NA) or min_val != min_val):
1042
+ min_val = source
1043
+ return min_val
1044
+
1045
+
1046
+ def mode(source: Series[TFI], length: int) -> TFI:
1047
+ """
1048
+ Returns the mode of the series. If there are several values with the same frequency,
1049
+ it returns the smallest value.
1050
+
1051
+ :param source: Series of values to process
1052
+ :param length: Number of bars (length)
1053
+ :return: The most frequently occurring value from the source. If none exists, returns
1054
+ the smallest value instead. Returns na during warm-up period.
1055
+ """
1056
+ assert length > 0, "Invalid length, length must be greater than 0!"
1057
+ if (isinstance(source, NA) or source != source):
1058
+ return cast(TFI, source)
1059
+ if bar_index < length - 1:
1060
+ return cast(TFI, NA(builtins.type(source)))
1061
+ length = int(length)
1062
+
1063
+ # Store values for quick access
1064
+ values = [source[i] for i in builtins.range(length) if not (isinstance(source[i], NA) or source[i] != source[i])]
1065
+ if not values:
1066
+ return cast(TFI, NA(builtins.type(source)))
1067
+
1068
+ # Find mode - sort values to handle equal frequencies
1069
+ values.sort() # Ensure we pick the smallest value when frequencies are equal
1070
+ mode_val = values[0]
1071
+ current_val = values[0]
1072
+ max_freq = curr_freq = 1
1073
+
1074
+ # Single pass through sorted values
1075
+ for i in builtins.range(1, len(values)):
1076
+ if values[i] == current_val:
1077
+ curr_freq += 1
1078
+ if curr_freq > max_freq:
1079
+ max_freq = curr_freq
1080
+ mode_val = current_val
1081
+ else:
1082
+ current_val = values[i]
1083
+ curr_freq = 1
1084
+
1085
+ return mode_val
1086
+
1087
+
1088
+ def mom(source: float, length: int) -> PyneFloat:
1089
+ """
1090
+ Calculate the Momentum of the source series with the given length.
1091
+
1092
+ :param source: The source series
1093
+ :param length: The length of the Momentum
1094
+ :return: The Momentum of the source series
1095
+ """
1096
+ # It is exactly the same as change function
1097
+ return change(source, length)
1098
+
1099
+
1100
+ # noinspection PyUnusedLocal
1101
+ @module_property
1102
+ def nvi() -> PyneFloat:
1103
+ """
1104
+ Negative Volume Index.
1105
+
1106
+ :return: Negative Volume Index
1107
+ """
1108
+ prev_close: Persistent[float] = 0.0
1109
+ prev_volume: Persistent[float] = 0.0
1110
+ prev_nvi: Persistent[float] = 1.0
1111
+
1112
+ if close == 0.0 or prev_close == 0.0:
1113
+ _nvi = prev_nvi
1114
+ else:
1115
+ _nvi = prev_nvi + ((close - prev_close) / prev_close) * prev_nvi if volume < prev_volume else prev_nvi
1116
+
1117
+ prev_close = close
1118
+ prev_volume = volume
1119
+ prev_nvi = _nvi
1120
+
1121
+ return _nvi
1122
+
1123
+
1124
+ @module_property
1125
+ def obv() -> PyneFloat:
1126
+ """
1127
+ On Balance Volume.
1128
+
1129
+ :return: On Balance Volume
1130
+ """
1131
+ chg = change(close)
1132
+ if (isinstance(chg, NA) or chg != chg):
1133
+ return na_float
1134
+ if chg > 0:
1135
+ chg = 1.0
1136
+ elif chg < 0:
1137
+ chg = -1.0
1138
+ else:
1139
+ chg = 0.0
1140
+ return cum(volume * chg)
1141
+
1142
+
1143
+ def percentile_linear_interpolation(source: Series[float], length: int, percentage: int | float) \
1144
+ -> PyneFloat:
1145
+ """
1146
+ Calculates percentile using method of linear interpolation between the two nearest ranks.
1147
+
1148
+ :param source: The source series
1149
+ :param length: The length of the percentile
1150
+ :param percentage: The percentage of the percentile
1151
+ :return: The percentile of the source series
1152
+ """
1153
+ assert length > 0, "Invalid length, length must be greater than 0!"
1154
+ length = int(length)
1155
+ # The final slice reads ``length`` candles of history; grow the source
1156
+ # buffer to fit it (the per-series default may be smaller). Done before the
1157
+ # warmup guard so the oldest candles are kept from the first bar on.
1158
+ max_bars_back(source, length)
1159
+ if (isinstance(source, NA) or source != source):
1160
+ return na_float
1161
+
1162
+ if bar_index < length - 1:
1163
+ return na_float
1164
+
1165
+ return array.percentile_linear_interpolation(source[:length], percentage) # type: ignore
1166
+
1167
+
1168
+ def percentile_nearest_rank(source: Series[float], length: int, percentage: int | float) \
1169
+ -> PyneFloat:
1170
+ """
1171
+ Calculates percentile using the nearest rank method.
1172
+
1173
+ :param source: The source series
1174
+ :param length: The length of the percentile
1175
+ :param percentage: The percentage of the percentile
1176
+ :return: The percentile of the source series
1177
+ """
1178
+ assert length > 0, "Invalid length, length must be greater than 0!"
1179
+ length = int(length)
1180
+ # The final slice reads ``length`` candles of history; grow the source
1181
+ # buffer to fit it (the per-series default may be smaller). Done before the
1182
+ # warmup guard so the oldest candles are kept from the first bar on.
1183
+ max_bars_back(source, length)
1184
+ if (isinstance(source, NA) or source != source):
1185
+ return na_float
1186
+
1187
+ if bar_index < length - 1:
1188
+ return na_float
1189
+
1190
+ return array.percentile_nearest_rank(source[:length], percentage) # type: ignore
1191
+
1192
+
1193
+ def percentrank(source: Series[float], length: int) -> PyneFloat:
1194
+ """
1195
+ Percent rank is the percents of how many previous values was less than or equal to the current
1196
+ value of given series.
1197
+
1198
+ :param source: The source series
1199
+ :param length: Number of bars back to include in the calculation
1200
+ :return: The percentage of values less than or equal to the current value
1201
+ """
1202
+ assert length > 0, "Invalid length, length must be greater than 0!"
1203
+ length = int(length)
1204
+ # The final slice reads ``length + 1`` candles of history; a buffer of
1205
+ # ``max_bars_back == length`` (capacity ``length + 1``) holds exactly that.
1206
+ # Done before the warmup guard so the oldest candles are kept from bar 0.
1207
+ max_bars_back(source, length)
1208
+ if (isinstance(source, NA) or source != source):
1209
+ return na_float
1210
+
1211
+ if bar_index < length:
1212
+ return na_float
1213
+
1214
+ return array.percentrank(source[:length + 1], 0) # type: ignore
1215
+
1216
+
1217
+ # noinspection PyUnusedLocal,PyShadowingBuiltins
1218
+ def pivot_point_levels(type: str, anchor: bool, developing: bool = False) -> list[PyneFloat]:
1219
+ """
1220
+ Calculate pivot point levels based on the specified calculation type.
1221
+
1222
+ Returns an array of 11 float values representing pivot point levels:
1223
+ [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
1224
+
1225
+ :param type: Pivot calculation type: "Traditional", "Fibonacci", "Woodie",
1226
+ "Classic", "DM", or "Camarilla"
1227
+ :param anchor: Condition that triggers recalculation of pivot levels
1228
+ (e.g., timeframe.change("D"))
1229
+ :param developing: If true, values recalculate on each bar using current OHLC;
1230
+ if false (default), values remain constant until the next anchor
1231
+ :return: Array of 11 float values: [P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5]
1232
+ Not all types support all levels - unsupported ones return na_float
1233
+ """
1234
+ # Persistent state for anchor-based calculation
1235
+ # These store the COMPLETED previous period's values (used when developing=False)
1236
+ prev_period_high: Persistent[float] = na_float
1237
+ prev_period_low: Persistent[float] = na_float
1238
+ prev_period_close: Persistent[float] = na_float
1239
+ prev_period_open: Persistent[float] = na_float
1240
+
1241
+ # These accumulate values for the CURRENT period (will become prev_period on next anchor)
1242
+ curr_period_high: Persistent[float] = na_float
1243
+ curr_period_low: Persistent[float] = na_float
1244
+ curr_period_open: Persistent[float] = na_float
1245
+ is_first_bar_of_period: Persistent[bool] = True
1246
+
1247
+ levels: Persistent[list[PyneFloat]] = [na_float] * 11
1248
+ had_anchor: Persistent[bool] = False
1249
+
1250
+ # Normalize type to lowercase for case-insensitive comparison
1251
+ type_lower = type.lower() if isinstance(type, str) else ""
1252
+
1253
+ # On anchor, finalize the previous period and start a new one
1254
+ if anchor:
1255
+ # Save the accumulated current period values as the previous period
1256
+ prev_period_high = curr_period_high
1257
+ prev_period_low = curr_period_low
1258
+ prev_period_close = close[1] if bar_index > 0 else close # Last close of prev period
1259
+ prev_period_open = curr_period_open
1260
+
1261
+ # Reset current period accumulators for the new period
1262
+ curr_period_high = high
1263
+ curr_period_low = low
1264
+ curr_period_open = open
1265
+ is_first_bar_of_period = False
1266
+ had_anchor = True
1267
+ else:
1268
+ # Accumulate OHLC extremes for current period
1269
+ if is_first_bar_of_period or (isinstance(curr_period_high, NA) or curr_period_high != curr_period_high):
1270
+ curr_period_high = high
1271
+ curr_period_low = low
1272
+ curr_period_open = open
1273
+ is_first_bar_of_period = False
1274
+ else:
1275
+ if not (isinstance(high, NA) or high != high) and high > curr_period_high:
1276
+ curr_period_high = high
1277
+ if not (isinstance(low, NA) or low != low) and low < curr_period_low:
1278
+ curr_period_low = low
1279
+
1280
+ # If no anchor has occurred yet, return all NA values
1281
+ if not had_anchor:
1282
+ return [na_float] * 11
1283
+
1284
+ # Determine which OHLC values to use
1285
+ if developing:
1286
+ # Use current accumulated values for developing mode
1287
+ h = curr_period_high
1288
+ l = curr_period_low
1289
+ c = close # Current close for developing
1290
+ o = curr_period_open
1291
+ else:
1292
+ # Use previous period's OHLC (fixed after anchor)
1293
+ h = prev_period_high
1294
+ l = prev_period_low
1295
+ c = prev_period_close
1296
+ o = prev_period_open
1297
+
1298
+ # Check for NA values
1299
+ if (isinstance(h, NA) or h != h) or (isinstance(l, NA) or l != l) or (isinstance(c, NA) or c != c):
1300
+ return [na_float] * 11
1301
+
1302
+ # Calculate range
1303
+ rng = h - l
1304
+
1305
+ # Calculate levels based on type
1306
+ if type_lower in ("traditional", "classic"):
1307
+ # Traditional/Classic Pivot Points
1308
+ p = (h + l + c) / 3
1309
+ r1 = 2 * p - l
1310
+ s1 = 2 * p - h
1311
+ r2 = p + rng
1312
+ s2 = p - rng
1313
+ r3 = r1 + rng
1314
+ s3 = s1 - rng
1315
+ levels = [p, r1, s1, r2, s2, r3, s3, na_float, na_float, na_float, na_float]
1316
+
1317
+ elif type_lower == "fibonacci":
1318
+ # Fibonacci Pivot Points
1319
+ p = (h + l + c) / 3
1320
+ r1 = p + 0.382 * rng
1321
+ s1 = p - 0.382 * rng
1322
+ r2 = p + 0.618 * rng
1323
+ s2 = p - 0.618 * rng
1324
+ r3 = p + 1.000 * rng
1325
+ s3 = p - 1.000 * rng
1326
+ levels = [p, r1, s1, r2, s2, r3, s3, na_float, na_float, na_float, na_float]
1327
+
1328
+ elif type_lower == "woodie":
1329
+ # Woodie Pivot Points
1330
+ # Note: Woodie uses current period's OPEN (not prev period's close) for the "close" component
1331
+ # This makes Woodie more responsive to current price action
1332
+ woodie_c = curr_period_open if not developing else close
1333
+ if (isinstance(woodie_c, NA) or woodie_c != woodie_c):
1334
+ return [na_float] * 11
1335
+ p = (h + l + 2 * woodie_c) / 4
1336
+ r1 = 2 * p - l
1337
+ s1 = 2 * p - h
1338
+ r2 = p + rng
1339
+ s2 = p - rng
1340
+ r3 = r1 + rng
1341
+ s3 = s1 - rng
1342
+ levels = [p, r1, s1, r2, s2, r3, s3, na_float, na_float, na_float, na_float]
1343
+
1344
+ elif type_lower == "dm":
1345
+ # DeMark Pivot Points
1346
+ if (isinstance(o, NA) or o != o):
1347
+ return [na_float] * 11
1348
+
1349
+ if c < o:
1350
+ x = h + 2 * l + c
1351
+ elif c > o:
1352
+ x = 2 * h + l + c
1353
+ else: # c == o
1354
+ x = h + l + 2 * c
1355
+
1356
+ p = x / 4
1357
+ r1 = x / 2 - l
1358
+ s1 = x / 2 - h
1359
+ # DM only has P, R1, S1
1360
+ levels = [p, r1, s1, na_float, na_float, na_float, na_float,
1361
+ na_float, na_float, na_float, na_float]
1362
+
1363
+ elif type_lower == "camarilla":
1364
+ # Camarilla Pivot Points
1365
+ p = (h + l + c) / 3
1366
+ r1 = c + rng * 1.1 / 12
1367
+ s1 = c - rng * 1.1 / 12
1368
+ r2 = c + rng * 1.1 / 6
1369
+ s2 = c - rng * 1.1 / 6
1370
+ r3 = c + rng * 1.1 / 4
1371
+ s3 = c - rng * 1.1 / 4
1372
+ r4 = c + rng * 1.1 / 2
1373
+ s4 = c - rng * 1.1 / 2
1374
+ # Camarilla has P, R1-R4, S1-S4 (no R5, S5)
1375
+ levels = [p, r1, s1, r2, s2, r3, s3, r4, s4, na_float, na_float]
1376
+
1377
+ else:
1378
+ # Unknown type - return all NA
1379
+ levels = [na_float] * 11
1380
+
1381
+ return levels
1382
+
1383
+
1384
+ @overload
1385
+ def pivothigh(source: float, leftbars: int, rightbars: int) -> PyneFloat:
1386
+ """
1387
+ This function returns price of the pivot high point. It returns 'NaN', if there was no pivot high point.
1388
+
1389
+ :param source: The source series
1390
+ :param leftbars: Left strength
1391
+ :param rightbars: Right strength.
1392
+ :return: Price of the pivot high point, or NaN if no pivot
1393
+ """
1394
+ assert leftbars > 0, "Invalid leftbars, leftbars must be greater than 0!"
1395
+ assert rightbars > 0, "Invalid rightbars, rightbars must be greater than 0!"
1396
+
1397
+ if (isinstance(source, NA) or source != source):
1398
+ return na_float
1399
+
1400
+ pivotrange = leftbars + rightbars + 1
1401
+ ph, pi = cast(tuple[float, int], highest(source, pivotrange, _tuple=True, _check_eq=True))
1402
+
1403
+ if pi == -rightbars:
1404
+ return ph
1405
+
1406
+ return na_float
1407
+
1408
+
1409
+ @overload
1410
+ def pivothigh(leftbars: int, rightbars: int) -> PyneFloat:
1411
+ """
1412
+ This function returns price of the pivot high point. It returns 'NaN', if there was no pivot high point.
1413
+
1414
+ :param leftbars: Left strength
1415
+ :param rightbars: Right strength.
1416
+ :return: Price of the pivot high point, or NaN if no pivot
1417
+ """
1418
+ try:
1419
+ return pivothigh(safe_convert.safe_float(high), leftbars, rightbars) # type: ignore
1420
+ except TypeError:
1421
+ if (isinstance(high, NA) or high != high):
1422
+ return na_float
1423
+ else:
1424
+ raise
1425
+
1426
+
1427
+ @overload
1428
+ def pivotlow(source: float, leftbars: int, rightbars: int) -> PyneFloat:
1429
+ """
1430
+ This function returns price of the pivot low point. It returns 'NaN', if there was no pivot low point.
1431
+
1432
+ :param source: The source series
1433
+ :param leftbars: Left strength
1434
+ :param rightbars: Right strength.
1435
+ :return: Price of the pivot low point, or NaN if no pivot
1436
+ """
1437
+ assert leftbars > 0, "Invalid leftbars, leftbars must be greater than 0!"
1438
+ assert rightbars > 0, "Invalid rightbars, rightbars must be greater than 0!"
1439
+
1440
+ if (isinstance(source, NA) or source != source):
1441
+ return na_float
1442
+
1443
+ pivotrange = leftbars + rightbars + 1
1444
+ pl, pi = cast(tuple[float, int], lowest(source, pivotrange, _tuple=True, _check_eq=True))
1445
+ if pi == -rightbars:
1446
+ return pl
1447
+
1448
+ return na_float
1449
+
1450
+
1451
+ @overload
1452
+ def pivotlow(leftbars: int, rightbars: int) -> PyneFloat:
1453
+ """
1454
+ This function returns price of the pivot low point. It returns 'NaN', if there was no pivot low point.
1455
+
1456
+ :param leftbars: Left strength
1457
+ :param rightbars: Right strength.
1458
+ :return: Price of the pivot low point, or NaN if no pivot
1459
+ """
1460
+ try:
1461
+ return pivotlow(safe_convert.safe_float(low), leftbars, rightbars) # type: ignore
1462
+ except TypeError:
1463
+ if (isinstance(low, NA) or low != low):
1464
+ return na_float
1465
+ else:
1466
+ raise
1467
+
1468
+
1469
+ # noinspection PyUnusedLocal
1470
+ @module_property
1471
+ def pvi() -> PyneFloat:
1472
+ """
1473
+ Positive Volume Index.
1474
+
1475
+ :return: Positive Volume Index
1476
+ """
1477
+ prev_close: Persistent[float] = 0.0
1478
+ prev_volume: Persistent[float] = 0.0
1479
+ prev_pvi: Persistent[float] = 1.0
1480
+
1481
+ _pvi = prev_pvi + ((close - prev_close) / prev_close) * prev_pvi if volume > prev_volume else prev_pvi
1482
+ # na() predicate semantics: a division by zero (prev_close warmup 0.0) gives
1483
+ # inf, which is na on TV — not just nan, so the guard must be isfinite-based.
1484
+ if isinstance(_pvi, NA) or not math.isfinite(_pvi):
1485
+ _pvi = prev_pvi
1486
+
1487
+ prev_close = close
1488
+ prev_volume = volume
1489
+ prev_pvi = _pvi
1490
+
1491
+ return _pvi
1492
+
1493
+
1494
+ # noinspection PyUnusedLocal
1495
+ @module_property
1496
+ def pvt() -> PyneFloat:
1497
+ """
1498
+ Price Volume Trend.
1499
+
1500
+ :return: Price Volume Trend
1501
+ """
1502
+ prev_close: Persistent[float] = na_float
1503
+ chg = close - prev_close
1504
+ res = cum((chg / prev_close) * volume)
1505
+ prev_close = close
1506
+ return res
1507
+
1508
+
1509
+ # noinspection PyShadowingBuiltins
1510
+ def range(source: Series[float], length: int) -> PyneFloat:
1511
+ """
1512
+ Returns the difference between the max and min values in a series.
1513
+
1514
+ :param source: The source series
1515
+ :param length: Number of bars
1516
+ :return: The range of the source series
1517
+ """
1518
+ assert length > 0, "Invalid length, length must be greater than 0!"
1519
+ if (isinstance(source, NA) or source != source):
1520
+ return na_float
1521
+ length = int(length)
1522
+
1523
+ return highest(source, length) - lowest(source, length)
1524
+
1525
+
1526
+ def rci(source: Series[float], length: int) -> PyneFloat:
1527
+ """
1528
+ Calculate Rank Correlation Index (RCI).
1529
+
1530
+ :param source: Series of values to calculate RCI for
1531
+ :param length: Length of RCI calculation period
1532
+ :return: RCI value between -100 and 100, or na during warmup
1533
+ """
1534
+ assert length > 0, "Invalid length, length must be greater than 0!"
1535
+ length = int(length)
1536
+ # The slice below reads ``length`` candles of history; grow the source
1537
+ # buffer to fit it (the per-series default may be smaller). Done before the
1538
+ # warmup guard so the oldest candles are kept from the first bar on.
1539
+ max_bars_back(source, length)
1540
+ if (isinstance(source, NA) or source != source):
1541
+ return na_float
1542
+
1543
+ if bar_index < length:
1544
+ return na_float
1545
+
1546
+ # Collect values for performance
1547
+ values = cast(list[float], source[:length]) # type: ignore
1548
+
1549
+ # Calculate sums for correlation
1550
+ sum_x = sum_y = sum_xy = sum_x2 = sum_y2 = 0.0
1551
+
1552
+ for i in builtins.range(length):
1553
+ x = i + 1 # Time rank (newest value gets highest rank)
1554
+ y = _avgrank(values, values[i]) # Data rank # type: ignore
1555
+
1556
+ sum_x += x
1557
+ sum_y += y
1558
+ sum_xy += x * y
1559
+ sum_x2 += x * x
1560
+ sum_y2 += y * y
1561
+
1562
+ # Calculate correlation coefficient
1563
+ n = length
1564
+ numerator = n * sum_xy - sum_x * sum_y
1565
+ denominator = math.sqrt((n * sum_x2 - sum_x * sum_x) * (n * sum_y2 - sum_y * sum_y))
1566
+ # Divide first, then scale by 100. TradingView computes rci in this exact
1567
+ # (num / den) * 100 order, floating-point rounding and all, so this form
1568
+ # reproduces its output. The mathematically "correctly rounded" alternative
1569
+ # num * 100 / den lands 1 ULP off at near-tie bars and diverges from TV: on
1570
+ # the wild RCI-strategy reference (BINANCE:BTCUSDT 30m) it raised the
1571
+ # extra/missing entry divergence from 14/6 to 30/11. Verified — do not reorder.
1572
+ return (numerator / denominator) * 100
1573
+
1574
+
1575
+ # noinspection PyUnusedLocal
1576
+ def rising(source: float, length: int) -> bool:
1577
+ """
1578
+ Test if the source series is now rising for length bars long.
1579
+
1580
+ :param source: The source series
1581
+ :param length: The length of the rising test
1582
+ :return: True if the source series is rising for length bars long
1583
+ """
1584
+ assert length > 0, "Invalid length, length must be greater than 0!"
1585
+ length = int(length)
1586
+
1587
+ last_val: Persistent[float] = na_float
1588
+ counter: Persistent[int] = 0
1589
+
1590
+ if (isinstance(last_val, NA) or last_val != last_val):
1591
+ last_val = source
1592
+ return False
1593
+
1594
+ if source > last_val:
1595
+ counter += 1
1596
+ else:
1597
+ counter = 0
1598
+
1599
+ last_val = source
1600
+ return counter >= length
1601
+
1602
+
1603
+ def rma(source: PyneFloat, length: int) -> PyneFloat:
1604
+ """
1605
+ Calculate the RMA (Running Moving Average) of the source series with the given length.
1606
+
1607
+ :param source: The source series
1608
+ :param length: The length of the RMA
1609
+ :return: The RMA of the source series
1610
+ """
1611
+ return ema(source, length, 1 / length)
1612
+
1613
+
1614
+ def roc(source: Series[float], length: int) -> PyneFloat:
1615
+ """
1616
+ Calculate the Rate of Change (ROC) of the source series with the given length.
1617
+
1618
+ :param source: The source series
1619
+ :param length: The length of the ROC
1620
+ :return: The Rate of Change (ROC) of the source series
1621
+ """
1622
+ assert length > 0, "Invalid length, length must be greater than 0!"
1623
+ if (isinstance(source, NA) or source != source):
1624
+ return na_float
1625
+ length = int(length)
1626
+ # Grow the buffer so ``source[length]`` stays addressable for lengths beyond the
1627
+ # per-series default max_bars_back (500); otherwise it reads na and the roc is na.
1628
+ max_bars_back(source, length)
1629
+
1630
+ prev_val = source[length]
1631
+ chg = change(source, length)
1632
+
1633
+ if (isinstance(prev_val, NA) or prev_val != prev_val):
1634
+ return na_float
1635
+
1636
+ return 100 * chg / prev_val
1637
+
1638
+
1639
+ # noinspection PyUnusedLocal
1640
+ def rsi(source: float, length: int) -> PyneFloat:
1641
+ """
1642
+ Calculate the Relative Strength Index (RSI) of the source series with the given length.
1643
+
1644
+ :param source: The source series
1645
+ :param length: The length of the RSI
1646
+ :return: The Relative Strength Index (RSI) of the source series
1647
+ """
1648
+ assert length > 0, "Invalid length, length must be greater than 0!"
1649
+ if (isinstance(source, NA) or source != source):
1650
+ return na_float
1651
+
1652
+ prev_src: Persistent[float] = na_float
1653
+ if (isinstance(prev_src, NA) or prev_src != prev_src):
1654
+ prev_src = source
1655
+ return na_float
1656
+
1657
+ rma_u = rma(builtins.max(source - prev_src, 0.0), length)
1658
+ rma_d = rma(builtins.max(prev_src - source, 0.0), length)
1659
+ prev_src = source
1660
+
1661
+ return 100 - 100 / (1 + rma_u / rma_d)
1662
+
1663
+
1664
+ # noinspection PyShadowingBuiltins,PyUnusedLocal,PyShadowingNames
1665
+ def sar(start: float = 0.02, inc: float = 0.02, max: float = 0.2) -> PyneFloat:
1666
+ """
1667
+ Parabolic SAR (Stop and Reverse) - method devised by J. Welles Wilder, Jr.,
1668
+ to find potential reversals in the market price direction of traded goods.
1669
+
1670
+ :param start: Starting value for acceleration factor
1671
+ :param inc: Acceleration factor increment
1672
+ :param max: Maximum acceleration factor value
1673
+ :return: SAR value for current bar
1674
+ """
1675
+ assert 0 < start <= max, "Start must be positive and not greater than max!"
1676
+ assert inc > 0, "Increment must be positive!"
1677
+ assert max <= 0.5, "Maximum cannot exceed 0.5!"
1678
+
1679
+ if bar_index == 0:
1680
+ return na_float
1681
+
1682
+ # Persistent states
1683
+ pos_long: Persistent[bool] = True # Current position (long/short)
1684
+ af: Persistent[float] = start # Current acceleration factor
1685
+ sar_val: Persistent[float] = na_float # Current SAR value
1686
+ ep: Persistent[float] = na_float # Extreme point
1687
+
1688
+ # Initialize on second bar
1689
+ if bar_index == 1:
1690
+ if high[1] > high:
1691
+ pos_long = False
1692
+ sar_val = high[1] # short start
1693
+ ep = low # EP is current low
1694
+ else:
1695
+ pos_long = True
1696
+ sar_val = low[1] # long start
1697
+ ep = high # EP is current high
1698
+ return sar_val
1699
+
1700
+ # Calculate next SAR value
1701
+ next_sar = sar_val + af * (ep - sar_val)
1702
+
1703
+ # Trend-dependent logic
1704
+ if pos_long:
1705
+ # Long trend
1706
+ if low <= next_sar: # Reverse to short
1707
+ pos_long = False
1708
+ af = start
1709
+ next_sar = ep # Start from previous EP (Wilder method)
1710
+ # Clip to current and previous 2 candle highs
1711
+ next_sar = builtins.max(
1712
+ next_sar,
1713
+ high,
1714
+ high[1],
1715
+ high[2] if not (isinstance(high[2], NA) or high[2] != high[2]) else high[1]
1716
+ )
1717
+ ep = low # New EP
1718
+ else:
1719
+ # Continue long
1720
+ next_sar = builtins.min(
1721
+ next_sar,
1722
+ low[1],
1723
+ low[2] if not (isinstance(low[2], NA) or low[2] != low[2]) else low[1]
1724
+ )
1725
+ if high > ep: # New peak
1726
+ ep = high
1727
+ af = builtins.min(af + inc, max)
1728
+ else:
1729
+ # Short trend
1730
+ if high >= next_sar: # Reverse to long
1731
+ pos_long = True
1732
+ af = start
1733
+ next_sar = ep # Start from previous EP (Wilder method)
1734
+ # Clip to current and previous 2 candle lows
1735
+ next_sar = builtins.min(
1736
+ next_sar,
1737
+ low,
1738
+ low[1],
1739
+ low[2] if not (isinstance(low[2], NA) or low[2] != low[2]) else low[1]
1740
+ )
1741
+ ep = high # New EP
1742
+ else:
1743
+ # Continue short
1744
+ next_sar = builtins.max(
1745
+ next_sar,
1746
+ high[1],
1747
+ high[2] if not (isinstance(high[2], NA) or high[2] != high[2]) else high[1]
1748
+ )
1749
+ if low < ep: # New trough
1750
+ ep = low
1751
+ af = builtins.min(af + inc, max)
1752
+
1753
+ sar_val = next_sar
1754
+ return sar_val
1755
+
1756
+
1757
+ def sma(source: Series[float], length: int) -> PyneFloat:
1758
+ """
1759
+ Calculate Simple Moving Average (SMA)
1760
+
1761
+ :param source: The source series
1762
+ :param length: The length of the moving average
1763
+ :return: The Simple Moving Average (SMA)
1764
+ """
1765
+ # Round is necessary to solve precision issues
1766
+ return round(lib_math.sum(source, length) / length, 15)
1767
+
1768
+
1769
+ def stdev(source: float, length: int, biased=True) -> PyneFloat:
1770
+ """
1771
+ Calculate the standard deviation of the source series with the given length.
1772
+
1773
+ :param source: The source series
1774
+ :param length: The length of the standard deviation
1775
+ :param biased: Specifies whether the biased or unbiased standard deviation is calculated
1776
+ :return: The standard deviation of the source series
1777
+ """
1778
+ try:
1779
+ return math.sqrt(variance(source, length, biased))
1780
+ except TypeError:
1781
+ return na_float
1782
+
1783
+
1784
+ # noinspection PyShadowingNames
1785
+ def stoch(source: float | Series[float], high: float | Series[float], low: float | Series[float],
1786
+ length: int) -> PyneFloat:
1787
+ """
1788
+ Calculate the Stochastic Oscillator of the source series with the given length.
1789
+
1790
+ :param source: The source series
1791
+ :param high: Series of high values
1792
+ :param low: Series of low values
1793
+ :param length: The length of the Stochastic Oscillator
1794
+ :return: The Stochastic Oscillator of the source series
1795
+ """
1796
+ assert length > 0, "Invalid length, length must be greater than 0!"
1797
+ if (isinstance(source, NA) or source != source) or (isinstance(high, NA) or high != high) or (isinstance(low, NA) or low != low):
1798
+ return na_float
1799
+ length = int(length)
1800
+
1801
+ highs: Series[float] = high
1802
+ lows: Series[float] = low
1803
+ hmax = highest(highs, length)
1804
+ lmin = lowest(lows, length)
1805
+
1806
+ if bar_index < length - 1:
1807
+ return na_float
1808
+
1809
+ dl_diff = source - lmin
1810
+ hl_diff = hmax - lmin
1811
+ if dl_diff < 0.0:
1812
+ k = 0.0
1813
+ else:
1814
+ k = 100 * dl_diff / hl_diff
1815
+ k = 100.0 if k > 100.0 else 0.0 if k < 0.0 else k
1816
+ return k # type: ignore
1817
+
1818
+
1819
+ # noinspection PyUnusedLocal,PyShadowingNames
1820
+ def supertrend(factor: float | int, atr_period: int) -> tuple[PyneFloat, PyneInt]:
1821
+ """
1822
+ Calculate Supertrend indicator.
1823
+
1824
+ :param factor: ATR multiplier
1825
+ :param atr_period: ATR period length
1826
+ :return: Tuple of (supertrend value, direction). Direction: 1=down, -1=up
1827
+ """
1828
+ assert atr_period > 0, "Invalid ATR period, must be greater than 0!"
1829
+
1830
+ # Store persistent state
1831
+ prev_lower: Persistent[float] = na_float
1832
+ prev_upper: Persistent[float] = na_float
1833
+ prev_close: Persistent[float] = na_float
1834
+ prev_direction: Persistent[int] = NA(int)
1835
+ prev_supertrend: Persistent[float] = na_float
1836
+
1837
+ # Calculate base values
1838
+ src = hl2
1839
+ atr_val = atr(atr_period)
1840
+
1841
+ # This is a strange bug in Pine Script, but we need to replicate it
1842
+ if bar_index == 0:
1843
+ return 0.0, 1
1844
+
1845
+ if (isinstance(src, NA) or src != src) or (isinstance(atr_val, NA) or atr_val != atr_val):
1846
+ return na_float, prev_direction if not (isinstance(prev_direction, NA) or prev_direction != prev_direction) else 1
1847
+
1848
+ # Calculate bands
1849
+ upper = src + factor * atr_val
1850
+ lower = src - factor * atr_val
1851
+
1852
+ # First value initialization
1853
+ if (isinstance(prev_direction, NA) or prev_direction != prev_direction):
1854
+ direction = 1
1855
+ supertrend = upper
1856
+ prev_direction = direction
1857
+ prev_supertrend = supertrend
1858
+ prev_lower = lower
1859
+ prev_upper = upper
1860
+ prev_close = close
1861
+ return supertrend, direction
1862
+
1863
+ # Adjust bands based on previous values
1864
+ if lower > prev_lower or prev_close < prev_lower:
1865
+ curr_lower = lower
1866
+ else:
1867
+ curr_lower = prev_lower
1868
+
1869
+ if upper < prev_upper or prev_close > prev_upper:
1870
+ curr_upper = upper
1871
+ else:
1872
+ curr_upper = prev_upper
1873
+
1874
+ # Calculate direction
1875
+ if prev_supertrend == prev_upper:
1876
+ direction = -1 if close > curr_upper else 1
1877
+ else:
1878
+ direction = 1 if close < curr_lower else -1
1879
+
1880
+ # Calculate supertrend value
1881
+ supertrend = curr_upper if direction == 1 else curr_lower
1882
+
1883
+ # Store values for next iteration
1884
+ prev_direction = direction
1885
+ prev_supertrend = supertrend
1886
+ prev_lower = curr_lower
1887
+ prev_upper = curr_upper
1888
+ prev_close = close
1889
+
1890
+ return supertrend, direction
1891
+
1892
+
1893
+ def swma(source: Series[float]) -> PyneFloat:
1894
+ """
1895
+ Symmetrically weighted moving average with fixed length: 4. Weights: [1/6, 2/6, 2/6, 1/6].
1896
+
1897
+ :param source: The source series
1898
+ :return: The SWWMA of the source series
1899
+ """
1900
+ if (isinstance(source, NA) or source != source):
1901
+ return na_float
1902
+
1903
+ return (source + 2 * source[1] + 2 * source[2] + source[3]) / 6
1904
+
1905
+
1906
+ # noinspection PyUnusedLocal
1907
+ @module_function_property
1908
+ def tr(handle_na: bool = False) -> PyneFloat:
1909
+ """
1910
+ Calculate True Range (TR)
1911
+
1912
+ :param handle_na: If true, and previous day's close is NaN then tr would be calculated as
1913
+ current day high-low. Otherwise (if false) tr would return NaN in such cases
1914
+ :return: True Range (TR)
1915
+ """
1916
+ prev_close: Persistent[float] = na_float
1917
+
1918
+ if (isinstance(prev_close, NA) or prev_close != prev_close):
1919
+ val = (high - low) if handle_na else na_float
1920
+ else:
1921
+ val = builtins.max(high - low, abs(high - prev_close), abs(low - prev_close))
1922
+
1923
+ prev_close = close
1924
+ return val # type: ignore
1925
+
1926
+
1927
+ def tsi(source: Series[float], short_length: int, long_length: int) -> PyneFloat:
1928
+ """
1929
+ True strength index. It uses moving averages of the underlying momentum
1930
+ of a financial instrument.
1931
+
1932
+ :param source: Source series
1933
+ :param short_length: Short length
1934
+ :param long_length: Long length
1935
+ :return: True strength index between -1 and 1
1936
+ """
1937
+ assert short_length > 0, "Invalid short length, must be greater than 0!"
1938
+ assert long_length > 0, "Invalid long length, must be greater than 0!"
1939
+ if (isinstance(source, NA) or source != source):
1940
+ return na_float
1941
+
1942
+ # Calculate momentum
1943
+ momentum = change(source)
1944
+ if (isinstance(momentum, NA) or momentum != momentum):
1945
+ return na_float
1946
+
1947
+ # First smooth both momentum and abs(momentum)
1948
+ momentum_ema = ema(momentum, long_length)
1949
+ abs_momentum_ema = ema(abs(momentum), long_length)
1950
+
1951
+ if (isinstance(momentum_ema, NA) or momentum_ema != momentum_ema) or (isinstance(abs_momentum_ema, NA) or abs_momentum_ema != abs_momentum_ema):
1952
+ return na_float
1953
+
1954
+ # Second smooth
1955
+ tsi_value = ema(momentum_ema, short_length)
1956
+ abs_value = ema(abs_momentum_ema, short_length)
1957
+
1958
+ if (isinstance(abs_value, NA) or abs_value != abs_value):
1959
+ return na_float
1960
+
1961
+ return tsi_value / abs_value
1962
+
1963
+
1964
+ def variance(source: Series[float],
1965
+ length: int,
1966
+ biased: bool = True) -> PyneFloat:
1967
+ """
1968
+ Calculate the rolling variance of the source series.
1969
+
1970
+ :param source: The source series.
1971
+ :param length: The length of the rolling window.
1972
+ :param biased: If True, calculates biased variance; otherwise, calculates unbiased variance.
1973
+ :return: The variance of the source series.
1974
+ """
1975
+ assert length > 0, "Invalid length, must be > 0!"
1976
+ length = int(length)
1977
+ if length == 1:
1978
+ return 0.0
1979
+ # Grow the buffer so the decremental read ``source[length]`` stays addressable for
1980
+ # lengths beyond the per-series default max_bars_back (500); otherwise it reads na and
1981
+ # poisons the Welford accumulators permanently (feeds stdev/bb/bbw/kc). Runs before the
1982
+ # na guard so the buffer keeps growing on na bars too, ahead of any wrap.
1983
+ max_bars_back(source, length)
1984
+ if (isinstance(source, NA) or source != source):
1985
+ return na_float
1986
+
1987
+ # Welford online recurrence with Pébay (2008) decremental step for sliding
1988
+ # window. Avoids the catastrophic cancellation of the textbook
1989
+ # `E[X²] - E[X]²` form by accumulating m2 (sum of squared deviations from
1990
+ # the running mean) directly. Kahan compensation on `mean` and `m2`
1991
+ # eliminates ULP-level drift across long add/remove sequences. The final
1992
+ # `max(0.0, ...)` is a safety clamp for the rare case where decremental
1993
+ # round-off lands one ULP below zero — `stdev()` calls `math.sqrt(var)`
1994
+ # and would crash on a negative argument.
1995
+ count: Persistent[int] = 0
1996
+ mu: Persistent[float] = 0.0
1997
+ mu_c: Persistent[float] = 0.0
1998
+ m2: Persistent[float] = 0.0
1999
+ m2_c: Persistent[float] = 0.0
2000
+
2001
+ # Add new sample (Welford incremental)
2002
+ if count < length:
2003
+ count += 1
2004
+ n_after = count
2005
+ else:
2006
+ count += 1
2007
+ n_after = length + 1 # transient: post-add, pre-remove
2008
+
2009
+ delta = source - mu
2010
+ inc = delta / n_after
2011
+ y = inc - mu_c
2012
+ t = mu + y
2013
+ mu_c = (t - mu) - y # noqa - it is persistent
2014
+ mu = t
2015
+ m2_inc = delta * (source - mu)
2016
+ y = m2_inc - m2_c
2017
+ t = m2 + y
2018
+ m2_c = (t - m2) - y # noqa - it is persistent
2019
+ m2 = t
2020
+
2021
+ if count < length:
2022
+ return na_float
2023
+
2024
+ if count > length:
2025
+ # Remove oldest sample (Welford decremental, Pébay 2008)
2026
+ old_value = source[length]
2027
+ delta = old_value - mu
2028
+ dec = -delta / length
2029
+ y = dec - mu_c
2030
+ t = mu + y
2031
+ mu_c = (t - mu) - y # noqa - it is persistent
2032
+ mu = t
2033
+ m2_dec = -delta * (old_value - mu)
2034
+ y = m2_dec - m2_c
2035
+ t = m2 + y
2036
+ m2_c = (t - m2) - y # noqa - it is persistent
2037
+ m2 = t
2038
+
2039
+ if biased:
2040
+ var = m2 / length
2041
+ else:
2042
+ var = m2 / (length - 1)
2043
+ # Safety clamp: decremental round-off can occasionally land 1 ULP below
2044
+ # zero on long constant runs; stdev() would crash inside math.sqrt().
2045
+ return builtins.max(0.0, var)
2046
+
2047
+
2048
+ def valuewhen(condition: bool, source: float, occurrence: int) -> PyneFloat:
2049
+ """
2050
+ Returns the value of the source series when the condition is true for the given occurrence.
2051
+
2052
+ :param condition: The condition series
2053
+ :param source: The source series
2054
+ :param occurrence: The occurrence of the condition
2055
+ :return: The value of the source series when the condition is true for the given occurrence
2056
+ """
2057
+ assert occurrence >= 0, "Invalid occurrence, must be >= 0!"
2058
+ if (isinstance(source, NA) or source != source):
2059
+ return na_float
2060
+
2061
+ values: Persistent[deque[float]] = deque(maxlen=occurrence + 1)
2062
+
2063
+ if condition:
2064
+ values.append(source)
2065
+
2066
+ if len(values) == occurrence + 1:
2067
+ return values[0]
2068
+ return na_float
2069
+
2070
+
2071
+ # noinspection PyUnusedLocal
2072
+ @module_function_property
2073
+ def vwap(source: Series[float] | None = None, anchor: bool | None = None,
2074
+ stdev_mult: float | None = None) -> PyneFloat | tuple[PyneFloat, PyneFloat, PyneFloat]:
2075
+ """
2076
+ Volume weighted average price.
2077
+
2078
+ Referenced bare (``ta.vwap``) this is the Pine built-in variable: the VWAP of
2079
+ ``hlc3`` anchored to the session. Passing an explicit ``source`` selects the
2080
+ function form ``ta.vwap(source)``.
2081
+
2082
+ :param source: The source series; defaults to ``hlc3`` for the bare variable form
2083
+ :param anchor: The condition that triggers the reset of VWAP calculation
2084
+ :param stdev_mult: If specified, the function will calculate the standard deviation bands based on the main VWAP
2085
+ :return: The VWAP value or tuple of (vwap, upper_band, lower_band) if stdev_mult is specified
2086
+ """
2087
+ if source is None:
2088
+ source = hlc3
2089
+ if (isinstance(source, NA) or source != source):
2090
+ return na_float if stdev_mult is None else (na_float, na_float, na_float)
2091
+
2092
+ # Persistent variables for calculation
2093
+ sum_vol: Persistent[float] = 0.0
2094
+ sum_pv: Persistent[float] = 0.0
2095
+ sum_ppv: Persistent[float] = 0.0
2096
+ had_anchor: Persistent[bool] = False
2097
+
2098
+ if anchor is None:
2099
+ anchor = session.isfirstbar
2100
+
2101
+ # Reset calculations if anchor condition is met
2102
+ if anchor is not None and anchor:
2103
+ sum_vol = volume
2104
+ sum_pv = source * volume
2105
+ sum_ppv = 0.0
2106
+ had_anchor = True
2107
+ # Only accumulate after first anchor
2108
+ elif had_anchor:
2109
+ sum_vol += volume
2110
+ sum_pv += source * volume
2111
+ else: # There was no anchor yet
2112
+ return na_float if stdev_mult is None else (na_float, na_float, na_float)
2113
+
2114
+ # Calculate VWAP
2115
+ vwap_value = sum_pv / sum_vol
2116
+ if (isinstance(vwap_value, NA) or vwap_value != vwap_value):
2117
+ return na_float if stdev_mult is None else (na_float, na_float, na_float)
2118
+
2119
+ # If stdev_mult is specified, calculate bands
2120
+ if had_anchor and stdev_mult is not None:
2121
+ sum_ppv += source * source * volume
2122
+ std = math.sqrt(builtins.max(0.0, sum_ppv / sum_vol - vwap_value * vwap_value))
2123
+ band_width = std * stdev_mult
2124
+ # Return tuple of (vwap, upper_band, lower_band)
2125
+ return vwap_value, vwap_value + band_width, vwap_value - band_width
2126
+
2127
+ return vwap_value
2128
+
2129
+
2130
+ def vwma(source: float, length: int) -> PyneFloat:
2131
+ return sma(source * volume, length) / sma(volume, length)
2132
+
2133
+
2134
+ # noinspection PyUnusedLocal
2135
+ @module_property
2136
+ def wad() -> PyneFloat:
2137
+ """
2138
+ Williams Accumulation/Distribution.
2139
+
2140
+ :return: Williams Accumulation/Distribution
2141
+ """
2142
+ prev_close: Persistent[float] = na_float
2143
+ true_high = builtins.max(high, prev_close)
2144
+ true_low = builtins.min(low, prev_close)
2145
+ momentum = close - prev_close
2146
+ gain = (close - true_low) if momentum > 0.0 else ((close - true_high) if momentum < 0.0 else 0.0)
2147
+ prev_close = close
2148
+ return cum(gain)
2149
+
2150
+
2151
+ def wma(source: Series[float], length: int) -> PyneFloat:
2152
+ """
2153
+ Calculate the Weighted Moving Average (WMA) of the source series with the given length.
2154
+
2155
+ :param source: The source series
2156
+ :param length: The length of the WMA
2157
+ :return: The WMA of the source series
2158
+ """
2159
+ assert length > 0, "Invalid length, length must be greater than 0!"
2160
+ length = int(length)
2161
+
2162
+ # Calculate denominator only once
2163
+ denom: Persistent[float] = length * (length + 1) / 2
2164
+
2165
+ count: Persistent[int] = 0
2166
+ summ: Persistent[float] = 0.0
2167
+ weighted_summ: Persistent[float] = 0.0
2168
+
2169
+ if (isinstance(source, NA) or source != source):
2170
+ # An NA bar leaves the window unchanged; hold the last full value
2171
+ # (still NA while warming up)
2172
+ return na_float if count < length else weighted_summ / denom
2173
+
2174
+ # NA values are NOT stored in the buffer, only skipped, so ``src[length]``
2175
+ # indexes past NA gaps to the true oldest value still inside the window.
2176
+ # Reading the parameter directly would step back ``length`` *bars* and land
2177
+ # inside an NA gap, subtracting an NA that poisons ``summ`` forever.
2178
+ src: Series[float] = source
2179
+ # Grow the na-compacted buffer so ``src[length]`` stays addressable for lengths
2180
+ # beyond the per-series default max_bars_back (500); otherwise the window-drop
2181
+ # read returns na and poisons ``summ`` permanently.
2182
+ max_bars_back(src, int(length))
2183
+
2184
+ # Warming up phase — only non-NA samples advance the window
2185
+ if count < length:
2186
+ count += 1
2187
+ summ += source
2188
+ weighted_summ += source * count
2189
+ if count < length:
2190
+ return na_float
2191
+
2192
+ # Normal calculation phase
2193
+ else:
2194
+ old_summ = summ
2195
+ # Substract the oldest value and add the newest value
2196
+ summ -= src[length] - source
2197
+ # Substract the oldest weighted value and add the newest weighted value
2198
+ weighted_summ -= old_summ - length * source
2199
+
2200
+ val = weighted_summ / denom
2201
+ return val # type: ignore
2202
+
2203
+
2204
+ def wpr(length: int) -> PyneFloat:
2205
+ """
2206
+ Williams %R indicator.
2207
+
2208
+ :param length: Length of the indicator
2209
+ :return: Williams %R value
2210
+ """
2211
+ assert length > 0, "Invalid length, must be greater than 0!"
2212
+ length = int(length)
2213
+
2214
+ if length == 1:
2215
+ return close
2216
+
2217
+ hmax = highest(high, length)
2218
+ lmin = lowest(low, length)
2219
+
2220
+ return 100 * (close - hmax) / (hmax - lmin)
2221
+
2222
+
2223
+ @module_property
2224
+ def wvad() -> PyneFloat:
2225
+ """
2226
+ Weighted Volume Accumulation/Distribution.
2227
+
2228
+ :return: Weighted Volume Accumulation/Distribution
2229
+ """
2230
+ return (close - open) / (high - low) * volume