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
@@ -0,0 +1,1771 @@
1
+ """
2
+ Builtin library of Pyne
3
+ """
4
+ from typing import TYPE_CHECKING, TypeAlias, Any, get_origin
5
+
6
+ if TYPE_CHECKING:
7
+ from pynecore.types.type_checker import *
8
+ from ..types.session import SessionInfo
9
+
10
+ import sys
11
+ import math as _math
12
+
13
+ from datetime import datetime, timedelta, time as dt_time, date, UTC
14
+
15
+ from pynecore.types.source import Source
16
+
17
+ from ..core.module_property import module_property, module_function_property
18
+ from ..core.script import script, input
19
+
20
+ from ..types.na import NA
21
+ from ..types import Series, PyneInt
22
+ from ..types.plot_meta import PlotMeta
23
+ from . import syminfo # This should be imported before core.datetime to avoid circular import!
24
+ from . import barstate, string, log, math, plot, hline, linefill, alert, dayofweek
25
+ from .plot import plot as _plot
26
+ from ..types.hline import HLine
27
+ from . import timeframe as timeframe_module
28
+ from . import session as session_module
29
+ from ._fixnan import fixnan
30
+
31
+ from pynecore.core.overload import overload
32
+ from pynecore.core.datetime import parse_datestring as _parse_datestring, parse_timezone as _parse_timezone, \
33
+ TimezoneNotFoundError
34
+ from ..core.resampler import (
35
+ Resampler, ObservedDayCounter as _ObservedDayCounter,
36
+ grid_mode as _grid_mode, overnight_opens as _overnight_opens,
37
+ overnight_starts_by_weekday as _overnight_starts_by_weekday,
38
+ close_table_by_weekday as _close_table_by_weekday,
39
+ trading_day as _trading_day, trading_day_open_sec as _trading_day_open_sec,
40
+ observed_week_key as _observed_week_key,
41
+ )
42
+
43
+ # The interned typeless na: bare ``na`` in compiled scripts evaluates through
44
+ # ``is_na()`` on every bar, so its result must be a constant, not an allocation
45
+ _na_none: NA = NA(None)
46
+
47
+ __all__ = [
48
+ # Other modules
49
+ 'syminfo', 'barstate', 'string', 'log', 'math', 'plot',
50
+
51
+ # Variables
52
+ 'bar_index', 'last_bar_index', 'last_bar_time',
53
+ 'open', 'high', 'low', 'close', 'volume',
54
+ 'bid', 'ask',
55
+ 'hl2', 'hlc3', 'ohlc4', 'hlcc4',
56
+
57
+ # Functions / objects
58
+ 'input', 'script',
59
+
60
+ 'max_bars_back',
61
+
62
+ 'timestamp',
63
+
64
+ 'plotchar', 'plotarrow', 'plotbar', 'plotcandle', 'plotshape', 'barcolor', 'bgcolor',
65
+ 'fill', 'linefill',
66
+
67
+ 'alertcondition',
68
+
69
+ 'fixnan', 'nz',
70
+
71
+ # Module properties
72
+ 'dayofmonth', 'dayofweek', 'hour', 'minute', 'month', 'second', 'weekofyear', 'year',
73
+ 'time', 'time_close', 'time_tradingday', 'timenow', 'na',
74
+ ]
75
+
76
+ #
77
+ # Constants
78
+ #
79
+
80
+ # For better type hints
81
+ TimezoneStr: TypeAlias = str # e.g. "UTC-5", "GMT+0530", "America/New_York"
82
+ DateStr: TypeAlias = str # e.g. "2020-02-20", "20 Feb 2020"
83
+
84
+ #
85
+ # Module variables
86
+ #
87
+
88
+ bar_index: Series[int] = 0
89
+ last_bar_index: Series[int] = 0 # This always points to the bar_index
90
+
91
+ open: float = Source("open") # noqa (shadowing built-in name (open) intentionally)
92
+ high: float = Source("high")
93
+ low: float = Source("low")
94
+ close: float = Source("close")
95
+ volume: float = Source("volume")
96
+
97
+ bid: float = Source("bid")
98
+ ask: float = Source("ask")
99
+
100
+ hl2: float = Source("hl2")
101
+ hlc3: float = Source("hlc3")
102
+ ohlc4: float = Source("ohlc4")
103
+ hlcc4: float = Source("hlcc4")
104
+
105
+ # Store time as integer as in Pine Scripts timestamp format
106
+ _time: int = 0
107
+ last_bar_time: int = 0
108
+
109
+ # Datetime object in the exchange timezone
110
+ _datetime: datetime = datetime.fromtimestamp(0, UTC)
111
+
112
+ # Script settings from `script.indicator`, `script.strategy` or `script.library`
113
+ _script: script = None # type: ignore[assignment]
114
+
115
+ # Chart (main-series) timeframe, propagated into request.security children so
116
+ # ``timeframe.main_period`` there reports the chart TF instead of the context's
117
+ # own period. ``None`` on the chart side, where ``_script`` carries it directly.
118
+ _main_timeframe: str | None = None
119
+
120
+ # Stores data to polot
121
+ _plot_data: dict[str, Any] = {}
122
+
123
+ # Plot-family registration state
124
+ _plot_meta: dict[str, PlotMeta] = {} # id -> meta, insertion order = registration order
125
+ _plot_meta_new: list[PlotMeta] = [] # pending metas, drained only by the viz writer
126
+ _viz_dyn: dict[str, Any] = {} # per-bar dynamic channels, cleared with _plot_data
127
+ _viz_seq: dict[str, int] = {} # per-bar ordinal counters for bgcolor/barcolor/fill/hline
128
+
129
+ # Extra fields from CSV data (beyond OHLCV), populated each bar by ScriptRunner
130
+ extra_fields: dict[str, Any] = {}
131
+
132
+ # Lib semaphore - to prevent lib`s main function to do things it must not (plot, strategy things, etc.)
133
+ _lib_semaphore = False
134
+
135
+ # Live trading mode flag — set by run.py when --live is specified
136
+ _is_live = False
137
+
138
+ # Strategy suppression — prevents strategy order placement during historical phase in live mode
139
+ _strategy_suppressed = False
140
+
141
+ #
142
+ # Function-and-namespace modules — the IDE-facing rebinding; at runtime the AST
143
+ # transformer routes ``hline(...)``-style calls to the module's self-named function
144
+ #
145
+
146
+ if TYPE_CHECKING:
147
+ from .hline import hline
148
+ from .plot import plot
149
+ from .alert import alert
150
+
151
+
152
+ #
153
+ # Functions
154
+ #
155
+
156
+ # noinspection PyUnusedLocal
157
+ def max_bars_back(var: Any, num: int) -> None:
158
+ """
159
+ Function sets the maximum number of bars that is available for historical reference of a given
160
+ built-in or user variable.
161
+
162
+ :param var: Series variable identifier for which history buffer should be resized.
163
+ :param num: History buffer size which is the number of bars to keep.
164
+ """
165
+
166
+
167
+ ### Date / Time ###
168
+
169
+ # noinspection PyShadowingNames
170
+ def _get_dt(time: int | None = None, timezone: str | None = None) -> datetime | NA[datetime]:
171
+ """ Get datetime object from time and timezone """
172
+ if isinstance(time, NA):
173
+ return time
174
+ dt = _datetime if time is None else datetime.fromtimestamp(time / 1000, UTC)
175
+ assert dt is not None
176
+ return dt.astimezone(_parse_timezone(timezone))
177
+
178
+
179
+ @overload
180
+ def timestamp(date_string: DateStr) -> int: # It is more pythonic, but not supported by Pine Script
181
+ """
182
+ Parse date string and return UNIX timestamp in milliseconds
183
+
184
+ Multiple calling formats supported:
185
+ - timestamp("2020-02-20T15:30:00+02:00") # ISO 8601
186
+ - timestamp("20 Feb 2020 15:30:00 GMT+0200") # RFC 2822
187
+ - timestamp("Feb 01 2020 22:10:05") # Pine format
188
+ - timestamp("2011-10-10T14:48:00") # Pine format without timezone
189
+
190
+ :param date_string: Date string in Pine Script format
191
+ :return: UNIX timestamp in milliseconds
192
+ """
193
+ dt = _parse_datestring(date_string)
194
+ return int(dt.timestamp() * 1000)
195
+
196
+
197
+ # noinspection PyPep8Naming
198
+ @overload
199
+ def timestamp(dateString: DateStr) -> int:
200
+ """
201
+ Parse date string and return UNIX timestamp in milliseconds
202
+
203
+ Multiple calling formats supported:
204
+ - timestamp("2020-02-20T15:30:00+02:00") # ISO 8601
205
+ - timestamp("20 Feb 2020 15:30:00 GMT+0200") # RFC 2822
206
+ - timestamp("Feb 01 2020 22:10:05") # Pine format
207
+ - timestamp("2011-10-10T14:48:00") # Pine format without timezone
208
+ - timestamp("UTC-5", 2020, 2, 20, 15, 30) # With timezone
209
+
210
+ :param dateString: Date string in Pine Script format
211
+ :return: UNIX timestamp in milliseconds
212
+ """
213
+ return timestamp(date_string=dateString)
214
+
215
+
216
+ # noinspection PyShadowingNames
217
+ @overload
218
+ def timestamp(timezone: TimezoneStr | None, year: int | float, month: int | float, day: int | float,
219
+ hour: int | float = 0, minute: int | float = 0, second: int | float = 0) -> int:
220
+ """
221
+ Create timestamp from date/time components with timezone:
222
+ - timestamp("UTC-5", 2020, 2, 20, 15, 30)
223
+ - timestamp("GMT+0530", 2020, 2, 20, 15, 30)
224
+
225
+ :param timezone: Timezone string
226
+ :param year: Year
227
+ :param month: Month
228
+ :param day: Day
229
+ :param hour: Hour
230
+ :param minute: Minute
231
+ :param second: Second
232
+ :return: UNIX timestamp in milliseconds
233
+ """
234
+ tz = _parse_timezone(timezone)
235
+ # Pine accepts out-of-range components and rolls them over (e.g. hour 26 ->
236
+ # next day + 2h, month 13 -> next January). Normalize the month into the
237
+ # year, then carry day/hour/minute/second through timedelta so the wall
238
+ # clock overflows before the timezone conversion.
239
+ y = int(year)
240
+ m = int(month)
241
+ y += (m - 1) // 12
242
+ m = (m - 1) % 12 + 1
243
+ dt = datetime(y, m, 1, tzinfo=tz) + timedelta(
244
+ days=int(day) - 1, hours=int(hour), minutes=int(minute), seconds=int(second)
245
+ )
246
+ return int(dt.timestamp() * 1000)
247
+
248
+
249
+ # noinspection PyShadowingNames
250
+ @overload
251
+ def timestamp(year: int | float, month: int | float, day: int | float, hour: int | float = 0,
252
+ minute: int | float = 0, second: int | float = 0) -> int:
253
+ """
254
+ Create timestamp from date/time components:
255
+ - timestamp(2020, 2, 20, 15, 30) # From components
256
+ - timestamp(2020, 2, 20, 15, 30, 0) # With seconds
257
+
258
+ :param year: Year
259
+ :param month: Month
260
+ :param day: Day
261
+ :param hour: Hour
262
+ :param minute: Minute
263
+ :param second: Second
264
+ :return: UNIX timestamp in milliseconds
265
+ """
266
+ return timestamp(None, year=year, month=month, day=day, hour=hour, minute=minute, second=second)
267
+
268
+
269
+ ### Plotting ###
270
+
271
+ def _uniq_title(title: str) -> str:
272
+ """Return a title unique against the current bar's ``_plot_data`` keys."""
273
+ c = 0
274
+ t = title
275
+ while t in _plot_data:
276
+ t = title + ' ' + str(c)
277
+ c += 1
278
+ return t
279
+
280
+
281
+ def _auto_viz_title(seq_key: str, base: str) -> str:
282
+ """
283
+ Return a per-bar-stable default title for an untitled ``bgcolor``/``barcolor``/``fill``.
284
+
285
+ Numbered by call order among untitled records of the same kind (``base``,
286
+ ``base 1``, ``base 2``, ...), mirroring the id sequence so it stays stable
287
+ across bars. ``seq_key`` must differ from the id counter keys.
288
+ """
289
+ n = _viz_seq.get(seq_key, 0)
290
+ _viz_seq[seq_key] = n + 1
291
+ return base if n == 0 else f'{base} {n}'
292
+
293
+
294
+ # noinspection PyProtectedMember,PyShadowingBuiltins
295
+ def plotshape(series: Any, title: str | None = None, style: Any = None, location: Any = None,
296
+ color: Any = None, offset: int = 0, text: str | None = None, textcolor: Any = None,
297
+ editable: bool = True, size: Any = None, show_last: int | None = None,
298
+ display: Any = None, format: str | None = None, precision: int | None = None,
299
+ force_overlay: bool = False) -> None:
300
+ """
301
+ Plot a shape marker on bars where ``series`` is true.
302
+
303
+ :param series: Marker is drawn on bars where this value is true (na propagates)
304
+ :param title: Plot title
305
+ :param style: Shape style (``shape.*``); default ``shape.xcross``
306
+ :param location: Marker location (``location.*``); default ``location.abovebar``
307
+ :param color: Marker color
308
+ :param offset: Horizontal shift in bars
309
+ :param text: Text displayed with the marker
310
+ :param textcolor: Color of the marker text
311
+ :param editable: If true, the plot style is editable in the Format dialog
312
+ :param size: Marker size (``size.*``); default ``size.auto``
313
+ :param show_last: If set, only the last ``show_last`` markers are drawn
314
+ :param display: Controls where the plot is displayed
315
+ :param format: Formatting of the displayed values
316
+ :param precision: Number of decimal places for the displayed values
317
+ :param force_overlay: If true, the plot displays on the main chart pane
318
+ """
319
+ if _lib_semaphore:
320
+ return
321
+ if bar_index == 0:
322
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
323
+ raise RuntimeError("The plotshape function can only be called from the main function!")
324
+ t = _uniq_title('Shape' if title is None else title)
325
+ _plot_data[t] = series if is_na(series) else int(bool(series))
326
+ meta = _plot_meta.get(t)
327
+ if meta is None:
328
+ meta = PlotMeta(id=t, kind='shape', title=t, style=style, location=location, color=color,
329
+ offset=offset, text=text, textcolor=textcolor, editable=editable,
330
+ size=size, show_last=show_last, display=display, format=format,
331
+ precision=precision, force_overlay=force_overlay)
332
+ _plot_meta[t] = meta
333
+ _plot_meta_new.append(meta)
334
+ if not meta.dynamic:
335
+ if (color is not None and color is not meta.color) or \
336
+ (textcolor is not None and textcolor is not meta.textcolor):
337
+ meta.dynamic = True
338
+ # The static meta record is already out — re-queue an updated one.
339
+ _plot_meta_new.append(meta)
340
+ if meta.dynamic:
341
+ # Once dynamic, record every bar so reverts to the static colors are emitted
342
+ _viz_dyn[t] = (color, textcolor)
343
+
344
+
345
+ # noinspection PyProtectedMember,PyShadowingBuiltins
346
+ def plotchar(series: Any, title: str | None = None, char: str | None = None, location: Any = None,
347
+ color: Any = None, offset: int = 0, text: str | None = None, textcolor: Any = None,
348
+ editable: bool = True, size: Any = None, show_last: int | None = None,
349
+ display: Any = None, format: str | None = None, precision: int | None = None,
350
+ force_overlay: bool = False) -> None:
351
+ """
352
+ Plot a character marker on bars where ``series`` is true.
353
+
354
+ :param series: The value plotted (stored raw)
355
+ :param title: Plot title
356
+ :param char: The character to draw; default '◆'
357
+ :param location: Marker location (``location.*``); default ``location.abovebar``
358
+ :param color: Marker color
359
+ :param offset: Horizontal shift in bars
360
+ :param text: Text displayed with the marker
361
+ :param textcolor: Color of the marker text
362
+ :param editable: If true, the plot style is editable in the Format dialog
363
+ :param size: Marker size (``size.*``); default ``size.auto``
364
+ :param show_last: If set, only the last ``show_last`` markers are drawn
365
+ :param display: Controls where the plot is displayed
366
+ :param format: Formatting of the displayed values
367
+ :param precision: Number of decimal places for the displayed values
368
+ :param force_overlay: If true, the plot displays on the main chart pane
369
+ """
370
+ if _lib_semaphore:
371
+ return
372
+ if bar_index == 0:
373
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
374
+ raise RuntimeError("The plotchar function can only be called from the main function!")
375
+ t = _uniq_title('Char' if title is None else title)
376
+ _plot_data[t] = series
377
+ meta = _plot_meta.get(t)
378
+ if meta is None:
379
+ meta = PlotMeta(id=t, kind='char', title=t, char=char, location=location, color=color,
380
+ offset=offset, text=text, textcolor=textcolor, editable=editable,
381
+ size=size, show_last=show_last, display=display, format=format,
382
+ precision=precision, force_overlay=force_overlay)
383
+ _plot_meta[t] = meta
384
+ _plot_meta_new.append(meta)
385
+ if not meta.dynamic:
386
+ if (color is not None and color is not meta.color) or \
387
+ (textcolor is not None and textcolor is not meta.textcolor):
388
+ meta.dynamic = True
389
+ # The static meta record is already out — re-queue an updated one.
390
+ _plot_meta_new.append(meta)
391
+ if meta.dynamic:
392
+ # Once dynamic, record every bar so reverts to the static colors are emitted
393
+ _viz_dyn[t] = (color, textcolor)
394
+
395
+
396
+ # noinspection PyProtectedMember,PyShadowingBuiltins
397
+ def plotarrow(series: Any, title: str | None = None, colorup: Any = None, colordown: Any = None,
398
+ offset: int = 0, minheight: int = 5, maxheight: int = 100, editable: bool = True,
399
+ show_last: int | None = None, display: Any = None, format: str | None = None,
400
+ precision: int | None = None, force_overlay: bool = False) -> None:
401
+ """
402
+ Plot up/down arrows sized by the magnitude of ``series``.
403
+
404
+ :param series: Arrow direction/length; positive draws up, negative draws down
405
+ :param title: Plot title
406
+ :param colorup: Color of up arrows
407
+ :param colordown: Color of down arrows
408
+ :param offset: Horizontal shift in bars
409
+ :param minheight: Minimum arrow height in pixels
410
+ :param maxheight: Maximum arrow height in pixels
411
+ :param editable: If true, the plot style is editable in the Format dialog
412
+ :param show_last: If set, only the last ``show_last`` arrows are drawn
413
+ :param display: Controls where the plot is displayed
414
+ :param format: Formatting of the displayed values
415
+ :param precision: Number of decimal places for the displayed values
416
+ :param force_overlay: If true, the plot displays on the main chart pane
417
+ """
418
+ if _lib_semaphore:
419
+ return
420
+ if bar_index == 0:
421
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
422
+ raise RuntimeError("The plotarrow function can only be called from the main function!")
423
+ t = _uniq_title('Arrows' if title is None else title)
424
+ _plot_data[t] = series
425
+ meta = _plot_meta.get(t)
426
+ if meta is None:
427
+ meta = PlotMeta(id=t, kind='arrow', title=t, colorup=colorup, colordown=colordown,
428
+ offset=offset, minheight=minheight, maxheight=maxheight, editable=editable,
429
+ show_last=show_last, display=display, format=format, precision=precision,
430
+ force_overlay=force_overlay)
431
+ _plot_meta[t] = meta
432
+ _plot_meta_new.append(meta)
433
+ if not meta.dynamic:
434
+ if (colorup is not None and colorup is not meta.colorup) or \
435
+ (colordown is not None and colordown is not meta.colordown):
436
+ meta.dynamic = True
437
+ # The static meta record is already out — re-queue an updated one.
438
+ _plot_meta_new.append(meta)
439
+ if meta.dynamic:
440
+ # Once dynamic, record every bar so reverts to the static colors are emitted
441
+ _viz_dyn[t] = (colorup, colordown)
442
+
443
+
444
+ # noinspection PyProtectedMember,PyShadowingBuiltins
445
+ def plotcandle(open: Any, high: Any, low: Any, close: Any, title: str | None = None,
446
+ color: Any = None, wickcolor: Any = None, editable: bool = True,
447
+ show_last: int | None = None, bordercolor: Any = None, display: Any = None,
448
+ format: str | None = None, precision: int | None = None,
449
+ force_overlay: bool = False) -> None:
450
+ """
451
+ Plot OHLC candles from the four supplied series.
452
+
453
+ :param open: Open value of the candle
454
+ :param high: High value of the candle
455
+ :param low: Low value of the candle
456
+ :param close: Close value of the candle
457
+ :param title: Plot title
458
+ :param color: Body color
459
+ :param wickcolor: Wick color
460
+ :param editable: If true, the plot style is editable in the Format dialog
461
+ :param show_last: If set, only the last ``show_last`` candles are drawn
462
+ :param bordercolor: Border color
463
+ :param display: Controls where the plot is displayed
464
+ :param format: Formatting of the displayed values
465
+ :param precision: Number of decimal places for the displayed values
466
+ :param force_overlay: If true, the plot displays on the main chart pane
467
+ """
468
+ if _lib_semaphore:
469
+ return
470
+ if bar_index == 0:
471
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
472
+ raise RuntimeError("The plotcandle function can only be called from the main function!")
473
+ base = 'Candles' if title is None else title
474
+ c = 0
475
+ t = base
476
+ while f"{t} (open)" in _plot_data:
477
+ t = base + ' ' + str(c)
478
+ c += 1
479
+ _plot_data[f"{t} (open)"] = open
480
+ _plot_data[f"{t} (high)"] = high
481
+ _plot_data[f"{t} (low)"] = low
482
+ _plot_data[f"{t} (close)"] = close
483
+ meta = _plot_meta.get(t)
484
+ if meta is None:
485
+ meta = PlotMeta(id=t, kind='candle', title=t, color=color, wickcolor=wickcolor,
486
+ bordercolor=bordercolor, editable=editable, show_last=show_last,
487
+ display=display, format=format, precision=precision,
488
+ force_overlay=force_overlay)
489
+ _plot_meta[t] = meta
490
+ _plot_meta_new.append(meta)
491
+ if not meta.dynamic:
492
+ if (color is not None and color is not meta.color) or \
493
+ (wickcolor is not None and wickcolor is not meta.wickcolor) or \
494
+ (bordercolor is not None and bordercolor is not meta.bordercolor):
495
+ meta.dynamic = True
496
+ # The static meta record is already out — re-queue an updated one.
497
+ _plot_meta_new.append(meta)
498
+ if meta.dynamic:
499
+ # Once dynamic, record every bar so reverts to the static colors are emitted
500
+ _viz_dyn[t] = (color, wickcolor, bordercolor)
501
+
502
+
503
+ # noinspection PyProtectedMember,PyShadowingBuiltins
504
+ def plotbar(open: Any, high: Any, low: Any, close: Any, title: str | None = None, color: Any = None,
505
+ editable: bool = True, show_last: int | None = None, display: Any = None,
506
+ format: str | None = None, precision: int | None = None,
507
+ force_overlay: bool = False) -> None:
508
+ """
509
+ Plot OHLC bars from the four supplied series.
510
+
511
+ :param open: Open value of the bar
512
+ :param high: High value of the bar
513
+ :param low: Low value of the bar
514
+ :param close: Close value of the bar
515
+ :param title: Plot title
516
+ :param color: Bar color
517
+ :param editable: If true, the plot style is editable in the Format dialog
518
+ :param show_last: If set, only the last ``show_last`` bars are drawn
519
+ :param display: Controls where the plot is displayed
520
+ :param format: Formatting of the displayed values
521
+ :param precision: Number of decimal places for the displayed values
522
+ :param force_overlay: If true, the plot displays on the main chart pane
523
+ """
524
+ if _lib_semaphore:
525
+ return
526
+ if bar_index == 0:
527
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
528
+ raise RuntimeError("The plotbar function can only be called from the main function!")
529
+ base = 'Bars' if title is None else title
530
+ c = 0
531
+ t = base
532
+ while f"{t} (open)" in _plot_data:
533
+ t = base + ' ' + str(c)
534
+ c += 1
535
+ _plot_data[f"{t} (open)"] = open
536
+ _plot_data[f"{t} (high)"] = high
537
+ _plot_data[f"{t} (low)"] = low
538
+ _plot_data[f"{t} (close)"] = close
539
+ meta = _plot_meta.get(t)
540
+ if meta is None:
541
+ meta = PlotMeta(id=t, kind='bar', title=t, color=color, editable=editable,
542
+ show_last=show_last, display=display, format=format, precision=precision,
543
+ force_overlay=force_overlay)
544
+ _plot_meta[t] = meta
545
+ _plot_meta_new.append(meta)
546
+ if meta.dynamic:
547
+ # Once dynamic, record every bar so a return to the static color is emitted
548
+ _viz_dyn[t] = color
549
+ elif color is not None and color is not meta.color:
550
+ _viz_dyn[t] = color
551
+ meta.dynamic = True
552
+ # The static meta record is already out — re-queue an updated one.
553
+ _plot_meta_new.append(meta)
554
+
555
+
556
+ # noinspection PyProtectedMember
557
+ def bgcolor(color: Any = None, offset: int = 0, editable: bool = True, show_last: int | None = None,
558
+ title: str | None = None, display: Any = None, force_overlay: bool = False) -> None:
559
+ """
560
+ Fill the background of bars with ``color``.
561
+
562
+ :param color: Background color for the current bar (na leaves the bar unpainted)
563
+ :param offset: Horizontal shift in bars
564
+ :param editable: If true, the fill is editable in the Format dialog
565
+ :param show_last: If set, only the last ``show_last`` bars are painted
566
+ :param title: Plot title
567
+ :param display: Controls where the fill is displayed
568
+ :param force_overlay: If true, the fill displays on the main chart pane
569
+ """
570
+ if _lib_semaphore:
571
+ return
572
+ if bar_index == 0:
573
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
574
+ raise RuntimeError("The bgcolor function can only be called from the main function!")
575
+ if title is None:
576
+ title = _auto_viz_title('bgcolor:title', 'Background color')
577
+ n = _viz_seq.get('bgcolor', 0)
578
+ _viz_seq['bgcolor'] = n + 1
579
+ key = f'bgcolor#{n}'
580
+ meta = _plot_meta.get(key)
581
+ if meta is None:
582
+ meta = PlotMeta(id=key, kind='bgcolor', title=title, offset=offset, editable=editable,
583
+ show_last=show_last, display=display, force_overlay=force_overlay,
584
+ dynamic=True)
585
+ _plot_meta[key] = meta
586
+ _plot_meta_new.append(meta)
587
+ # Record every bar (na/None -> null "off") so paint/unpaint transitions are emitted
588
+ _viz_dyn[key] = color
589
+
590
+
591
+ # noinspection PyProtectedMember
592
+ def barcolor(color: Any = None, offset: int = 0, editable: bool = True, show_last: int | None = None,
593
+ title: str | None = None, display: Any = None) -> None:
594
+ """
595
+ Color the price bars with ``color``.
596
+
597
+ :param color: Bar color for the current bar (na leaves the bar unchanged)
598
+ :param offset: Horizontal shift in bars
599
+ :param editable: If true, the coloring is editable in the Format dialog
600
+ :param show_last: If set, only the last ``show_last`` bars are colored
601
+ :param title: Plot title
602
+ :param display: Controls where the coloring is displayed
603
+ """
604
+ if _lib_semaphore:
605
+ return
606
+ if bar_index == 0:
607
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
608
+ raise RuntimeError("The barcolor function can only be called from the main function!")
609
+ if title is None:
610
+ title = _auto_viz_title('barcolor:title', 'Bar color')
611
+ n = _viz_seq.get('barcolor', 0)
612
+ _viz_seq['barcolor'] = n + 1
613
+ key = f'barcolor#{n}'
614
+ meta = _plot_meta.get(key)
615
+ if meta is None:
616
+ meta = PlotMeta(id=key, kind='barcolor', title=title, offset=offset, editable=editable,
617
+ show_last=show_last, display=display, dynamic=True)
618
+ _plot_meta[key] = meta
619
+ _plot_meta_new.append(meta)
620
+ # Record every bar (na/None -> null "off") so color/unpaint transitions are emitted
621
+ _viz_dyn[key] = color
622
+
623
+
624
+ # Positional parameter orders of Pine's three ``fill`` overloads; ``fill()`` maps
625
+ # ``*args`` onto one of these depending on the runtime shape of the call.
626
+ _FILL_PLOT_PARAMS = ('plot1', 'plot2', 'color', 'title', 'editable', 'show_last',
627
+ 'fillgaps', 'display')
628
+ _FILL_HLINE_PARAMS = ('hline1', 'hline2', 'color', 'title', 'editable', 'fillgaps', 'display')
629
+ _FILL_GRADIENT_PARAMS = ('plot1', 'plot2', 'top_value', 'bottom_value', 'top_color',
630
+ 'bottom_color', 'title', 'display', 'fillgaps', 'editable')
631
+
632
+
633
+ # noinspection PyProtectedMember
634
+ def fill(*args: Any, **kwargs: Any) -> None:
635
+ """
636
+ Fill the area between two plots or two hlines.
637
+
638
+ Three call shapes are accepted (Pine-compatible):
639
+
640
+ - ``fill(plot1, plot2, color, title, editable, show_last, fillgaps, display)``
641
+ - ``fill(hline1, hline2, color, title, editable, fillgaps, display)``
642
+ - ``fill(plot1, plot2, top_value, bottom_value, top_color, bottom_color, title,
643
+ display, fillgaps, editable)`` — vertical gradient
644
+
645
+ Positional arguments are bound to the hline shape when the first argument is an
646
+ ``hline``, to the gradient shape when the third argument is a numeric ``top_value``
647
+ rather than a color, and to the plot shape otherwise.
648
+
649
+ :param plot1: First plot object (``hline1`` for the hline shape)
650
+ :param plot2: Second plot object (``hline2`` for the hline shape)
651
+ :param color: Solid fill color
652
+ :param title: Plot title
653
+ :param editable: If true, the fill is editable in the Format dialog
654
+ :param show_last: If set, only the last ``show_last`` bars are filled (plot shape only)
655
+ :param fillgaps: If true, the fill continues across gaps (na values)
656
+ :param display: Controls where the fill is displayed
657
+ :param top_value: Value mapped to ``top_color`` in gradient mode
658
+ :param bottom_value: Value mapped to ``bottom_color`` in gradient mode
659
+ :param top_color: Color at ``top_value`` in gradient mode
660
+ :param bottom_color: Color at ``bottom_value`` in gradient mode
661
+ """
662
+ if _lib_semaphore:
663
+ return
664
+ if bar_index == 0:
665
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
666
+ raise RuntimeError("The fill function can only be called from the main function!")
667
+ if args:
668
+ if isinstance(args[0], HLine):
669
+ names = _FILL_HLINE_PARAMS
670
+ else:
671
+ a2 = args[2] if len(args) > 2 else None
672
+ # Gradient shape: the third positional is ``top_value`` — a number (or an na
673
+ # value followed by a gradient color where the plot shape would have a bool).
674
+ if (isinstance(a2, (int, float)) and not isinstance(a2, bool)) or \
675
+ (isinstance(a2, NA) and len(args) > 4 and not isinstance(args[4], bool)):
676
+ names = _FILL_GRADIENT_PARAMS
677
+ else:
678
+ names = _FILL_PLOT_PARAMS
679
+ if len(args) > len(names):
680
+ raise TypeError(f"fill() takes at most {len(names)} positional arguments")
681
+ for name, value in zip(names, args):
682
+ if name in kwargs:
683
+ raise TypeError(f"fill() got multiple values for argument '{name}'")
684
+ kwargs[name] = value
685
+ plot1 = kwargs.get('plot1') if 'plot1' in kwargs else kwargs.get('hline1')
686
+ plot2 = kwargs.get('plot2') if 'plot2' in kwargs else kwargs.get('hline2')
687
+ color = kwargs.get('color')
688
+ title = kwargs.get('title')
689
+ if title is None:
690
+ title = _auto_viz_title('fill:title', 'Plots Background')
691
+ editable = kwargs.get('editable', True)
692
+ show_last = kwargs.get('show_last')
693
+ fillgaps = kwargs.get('fillgaps', False)
694
+ display = kwargs.get('display')
695
+ top_value = kwargs.get('top_value')
696
+ bottom_value = kwargs.get('bottom_value')
697
+ top_color = kwargs.get('top_color')
698
+ bottom_color = kwargs.get('bottom_color')
699
+ n = _viz_seq.get('fill', 0)
700
+ _viz_seq['fill'] = n + 1
701
+ key = f'fill#{n}'
702
+ meta = _plot_meta.get(key)
703
+ created = meta is None
704
+ if meta is None:
705
+ if isinstance(plot1, HLine):
706
+ meta = PlotMeta(id=key, kind='fill', title=title, color=color, editable=editable,
707
+ show_last=show_last, fillgaps=fillgaps, display=display,
708
+ hline1=plot1.id if plot1 is not None else None,
709
+ hline2=plot2.id if plot2 is not None else None)
710
+ else:
711
+ meta = PlotMeta(id=key, kind='fill', title=title, color=color, editable=editable,
712
+ show_last=show_last, fillgaps=fillgaps, display=display,
713
+ plot1=plot1.id if plot1 is not None else None,
714
+ plot2=plot2.id if plot2 is not None else None)
715
+ _plot_meta[key] = meta
716
+ _plot_meta_new.append(meta)
717
+ if top_color is not None or bottom_color is not None \
718
+ or top_value is not None or bottom_value is not None:
719
+ _viz_dyn[key] = (top_value, bottom_value, top_color, bottom_color)
720
+ if not meta.dynamic:
721
+ meta.dynamic = True
722
+ if not created:
723
+ # Already emitted as static — re-queue so an updated meta record
724
+ # (dynamic: true) precedes this bar's color delta.
725
+ _plot_meta_new.append(meta)
726
+ elif meta.dynamic:
727
+ # Once dynamic, record every bar so a return to the static color is emitted
728
+ _viz_dyn[key] = color
729
+ elif color is not None and color is not meta.color:
730
+ _viz_dyn[key] = color
731
+ meta.dynamic = True
732
+ # The static meta record is already out — re-queue an updated one.
733
+ _plot_meta_new.append(meta)
734
+
735
+
736
+ ### Alert ###
737
+
738
+ def alertcondition(*_, **__):
739
+ """
740
+ Define alert condition. Currently implemented as no-op.
741
+
742
+ In the future this could be used to define alert conditions
743
+ that can be triggered based on boolean expressions.
744
+ """
745
+ if bar_index == 0: # Only check if it is the first bar for performance reasons
746
+ # Check if it is called from the main function
747
+ if sys._getframe(1).f_code.co_name != 'main': # noqa
748
+ raise RuntimeError("The alertcondition function can only be called from the main function!")
749
+
750
+
751
+ ### Other ###
752
+
753
+ def is_na(source: Any = None) -> bool | NA:
754
+ """
755
+ Check if the source is NA.
756
+
757
+ Pine treats inf/-inf/nan floats as "na" for na() predicate purposes,
758
+ even though they participate in arithmetic/comparisons as normal IEEE-754
759
+ values. This matches that dual behavior.
760
+ """
761
+ if source is None:
762
+ return _na_none
763
+ # If the source is a type or a subscripted generic, return NA of that type.
764
+ # Match generics via get_origin so BOTH builtin generics (list[float] →
765
+ # types.GenericAlias) and typing generics from user Generic classes
766
+ # (Matrix[float] → typing._GenericAlias) are covered — the old
767
+ # `isinstance(source, GenericAlias)` only caught the builtin kind, so
768
+ # `na(Matrix[float])` fell through and returned False (a bool), which then
769
+ # broke matrix.copy(m) etc. with `'bool' object has no attribute 'copy'`.
770
+ if source is not NA and (isinstance(source, type) or get_origin(source) is not None):
771
+ # na.pyi deliberately types NA(x) as x itself (so na sentinels flow as
772
+ # values in user scripts), which contradicts the honest annotation here
773
+ return NA(source) # pyright: ignore[reportReturnType]
774
+ if isinstance(source, float):
775
+ return not _math.isfinite(source)
776
+ return isinstance(source, NA) or source is NA
777
+
778
+
779
+ # In Pine Script, na is both a property and a function; any narrower type than
780
+ # Any produces false positives on one of its three faces (bare value, na(x)
781
+ # predicate, na(type) constructor)
782
+ na: Any = is_na
783
+
784
+
785
+ def nz(source: Any, replacement: Any = 0) -> Any:
786
+ """
787
+ Replace NA values with a replacement value or 0 if not specified
788
+
789
+ Uses the na() predicate semantics for floats: inf/-inf/nan are all na
790
+ (TV-verified: ``nz(inf, -5)`` is ``-5``).
791
+
792
+ :param source: The source value
793
+ :param replacement: The replacement value, default is 0
794
+ :return: The source value if it is not NA, otherwise the replacement value
795
+ """
796
+ if isinstance(source, float):
797
+ return source if _math.isfinite(source) else replacement
798
+ if isinstance(source, NA):
799
+ return replacement
800
+ return source
801
+
802
+
803
+ #
804
+ # Module properties
805
+ #
806
+
807
+ ### Date / Time ###
808
+
809
+ # noinspection PyShadowingNames
810
+ @module_function_property
811
+ def dayofmonth(time: int | None = None, timezone: str | None = None) -> int:
812
+ """
813
+ Day of the month
814
+
815
+ :param time: The time to get the day of the month from, if None the current time is used
816
+ :param timezone: The timezone of the time, if not specified the exchange timezone is used
817
+ :return: The day of the month
818
+ """
819
+ return _get_dt(time, timezone).day
820
+
821
+
822
+ # noinspection PyShadowingNames
823
+ @module_function_property
824
+ def hour(time: int | None = None, timezone: str | None = None) -> int:
825
+ """
826
+ Hour of the day
827
+
828
+ :param time: The time to get the hour of the day from, if None the current time is used
829
+ :param timezone: The timezone of the time, if not specified the exchange timezone is used
830
+ :return: The hour of the day
831
+ """
832
+ return _get_dt(time, timezone).hour
833
+
834
+
835
+ # noinspection PyShadowingNames
836
+ @module_function_property
837
+ def minute(time: int | None = None, timezone: str | None = None) -> int:
838
+ """
839
+ Minute of the hour
840
+
841
+ :param time: The time to get the minute of the hour from, if None the current time is used
842
+ :param timezone: The timezone of the time, if not specified the exchange timezone is used
843
+ :return: The minute of the hour
844
+ """
845
+ return _get_dt(time, timezone).minute
846
+
847
+
848
+ # noinspection PyShadowingNames
849
+ @module_function_property
850
+ def month(time: int | None = None, timezone: str | None = None) -> int:
851
+ """
852
+ Month of the year
853
+
854
+ :param time: The time to get the month of the year from, if None the current time is used
855
+ :param timezone: The timezone of the time, if not specified the exchange timezone is used
856
+ :return: The month of the year
857
+ """
858
+ return _get_dt(time, timezone).month
859
+
860
+
861
+ # noinspection PyShadowingNames
862
+ @module_function_property
863
+ def second(time: int | None = None, timezone: str | None = None) -> int:
864
+ """
865
+ Second of the minute
866
+
867
+ :param time: The time to get the second of the minute from, if None the current time is used
868
+ :param timezone: The timezone of the time, if not specified the exchange timezone is used
869
+ :return: The second of the minute
870
+ """
871
+ return _get_dt(time, timezone).second
872
+
873
+
874
+ ### Session parsing and validation helpers ###
875
+
876
+ def _parse_session_string(session: str, timezone: str | None = None) -> 'SessionInfo':
877
+ """
878
+ Parse a session string into a SessionInfo object.
879
+
880
+ :param session: Session string (e.g., "0930-1600", "0930-1600:23456", "0000-0000:1234567")
881
+ :param timezone: Timezone string, defaults to exchange timezone if None
882
+ :return: SessionInfo object
883
+ :raises ValueError: If session string is invalid
884
+ """
885
+ from ..types.session import SessionInfo
886
+
887
+ if not session or session.strip() == "":
888
+ raise ValueError("Session string cannot be empty")
889
+
890
+ # Use exchange timezone if not specified
891
+ if timezone is None:
892
+ # Use a safe default if syminfo.timezone is not available
893
+ timezone = getattr(syminfo, 'timezone', 'UTC')
894
+ # Handle NA values
895
+ if hasattr(timezone, '__class__') and 'NA' in timezone.__class__.__name__:
896
+ timezone = 'UTC'
897
+
898
+ # Split session and days if present
899
+ if ':' in session:
900
+ time_part, days_part = session.split(':', 1)
901
+ else:
902
+ time_part = session
903
+ # Default days in Pine Script v5 is all days (1234567)
904
+ days_part = "1234567"
905
+
906
+ # Parse time part (HHMM-HHMM format)
907
+ if '-' not in time_part:
908
+ raise ValueError(f"Invalid session format: {session}. Expected HHMM-HHMM format")
909
+
910
+ start_str, end_str = time_part.split('-', 1)
911
+
912
+ if len(start_str) != 4 or len(end_str) != 4:
913
+ raise ValueError(f"Invalid time format in session: {session}. Expected HHMM-HHMM")
914
+
915
+ try:
916
+ start_hour = int(start_str[:2])
917
+ start_minute = int(start_str[2:])
918
+ end_hour = int(end_str[:2])
919
+ end_minute = int(end_str[2:])
920
+
921
+ # Validate time values
922
+ if not (0 <= start_hour <= 23 and 0 <= start_minute <= 59):
923
+ raise ValueError(f"Invalid start time: {start_str}")
924
+ if not (0 <= end_hour <= 23 and 0 <= end_minute <= 59):
925
+ raise ValueError(f"Invalid end time: {end_str}")
926
+
927
+ start_time = dt_time(start_hour, start_minute)
928
+ end_time = dt_time(end_hour, end_minute)
929
+
930
+ except ValueError as e:
931
+ raise ValueError(f"Invalid time values in session: {session}") from e
932
+
933
+ # Parse days (1=Sunday, 2=Monday, ..., 7=Saturday)
934
+ try:
935
+ days = set()
936
+ for day_char in days_part:
937
+ day_num = int(day_char)
938
+ if not 1 <= day_num <= 7:
939
+ raise ValueError(f"Invalid day: {day_num}")
940
+ days.add(day_num)
941
+ except ValueError as e:
942
+ raise ValueError(f"Invalid days specification: {days_part}") from e
943
+
944
+ return SessionInfo(
945
+ start_time=start_time,
946
+ end_time=end_time,
947
+ days=days,
948
+ timezone=timezone
949
+ )
950
+
951
+
952
+ def _is_bar_in_session(bar_time_ms: int, session_info: 'SessionInfo', timeframe: str) -> bool:
953
+ """
954
+ Check if a bar time falls within the specified session.
955
+
956
+ :param bar_time_ms: Bar time in milliseconds (UNIX timestamp)
957
+ :param session_info: Session information
958
+ :param timeframe: Timeframe string for calculating bar duration
959
+ :return: True if bar is within session, False otherwise
960
+ """
961
+ from datetime import datetime, timedelta
962
+
963
+ # Convert bar time to datetime in session timezone
964
+ bar_dt = datetime.fromtimestamp(bar_time_ms / 1000)
965
+ session_tz = _parse_timezone(session_info.timezone)
966
+ bar_dt_local = bar_dt.astimezone(session_tz)
967
+
968
+ # Get the day of week in TradingView format (1=Sunday, 2=Monday, ..., 7=Saturday)
969
+ # Python weekday: 0=Monday, 6=Sunday
970
+ python_weekday = bar_dt_local.weekday()
971
+ tv_weekday = (python_weekday + 2) % 7
972
+ if tv_weekday == 0:
973
+ tv_weekday = 7
974
+
975
+ # Check if the day is in the session days
976
+ if tv_weekday not in session_info.days:
977
+ return False
978
+
979
+ # A session whose start equals its end spans the full 24 hours -- this is
980
+ # Pine's "0000-0000" all-day session (the default of ``input.session``).
981
+ # The day of week is already validated above, so every bar on it qualifies.
982
+ if session_info.start_time == session_info.end_time:
983
+ return True
984
+
985
+ # Get bar time components
986
+ bar_time = bar_dt_local.time()
987
+
988
+ # Get timeframe duration for checking bar overlap
989
+ try:
990
+ tf_seconds = timeframe_module.in_seconds(timeframe)
991
+ except (ValueError, AssertionError):
992
+ # If timeframe is invalid, assume 1-minute bars
993
+ tf_seconds = 60
994
+
995
+ # Calculate bar end time
996
+ bar_end_dt = bar_dt_local + timedelta(seconds=tf_seconds)
997
+ bar_end_time = bar_end_dt.time()
998
+
999
+ # Handle overnight sessions
1000
+ if session_info.is_overnight:
1001
+ # Session spans midnight (e.g., 22:00-06:00)
1002
+ # Bar is in session if it starts after session start OR ends before session end
1003
+ in_session = (bar_time >= session_info.start_time or
1004
+ bar_end_time <= session_info.end_time)
1005
+ else:
1006
+ # Normal session within same day
1007
+ # Bar is in session if it overlaps with the session time range
1008
+ # Bar overlaps if: bar_start < session_end AND bar_end > session_start
1009
+ in_session = (bar_time < session_info.end_time and
1010
+ bar_end_time > session_info.start_time)
1011
+
1012
+ return in_session
1013
+
1014
+
1015
+ def _intraday_session_args(timeframe: str) -> tuple:
1016
+ """
1017
+ Build the ``(tz, session_starts)`` arguments for :meth:`Resampler.get_bar_time`
1018
+ that anchor an intraday ``timeframe`` to the exchange session open, the way
1019
+ TradingView aligns intraday HTF bars. Anchoring is a no-op for on-hour / 24-7
1020
+ markets, so it is always safe to pass for intraday.
1021
+
1022
+ Daily/weekly/monthly timeframes get ``(tz,)`` instead: their calendar floor
1023
+ must run in the exchange timezone (TradingView day/week/month boundaries are
1024
+ exchange-local), not in the machine's local time.
1025
+
1026
+ :param timeframe: The requested timeframe string (already validated).
1027
+ :return: ``(tz, session_starts)`` for intraday with a session, ``(tz,)`` for
1028
+ daily/weekly/monthly, else ``()``.
1029
+ """
1030
+ tz_name = getattr(syminfo, 'timezone', None)
1031
+ tz = _parse_timezone(tz_name) if tz_name else None
1032
+ # noinspection PyProtectedMember
1033
+ modifier, _ = timeframe_module._process_tf(timeframe)
1034
+ if modifier not in ('S', ''):
1035
+ return (tz,) if tz is not None else ()
1036
+ # noinspection PyProtectedMember
1037
+ session_starts = getattr(syminfo, '_session_starts', None)
1038
+ if not session_starts:
1039
+ return (tz,) if tz is not None else ()
1040
+ return tz, session_starts
1041
+
1042
+
1043
+ # Multi-period (nD/nW/nM) scheduled-grid tracker. TradingView counts scheduled
1044
+ # trading days per exchange calendar with a year-reset counter (see the
1045
+ # ``core.resampler`` module docs). 'calendar' (24/7) and 'weekday' (FX) grids
1046
+ # are pure arithmetic; 'observed' symbols (exchange-listed) count the actual
1047
+ # trading days streamed through the chart, which realizes TradingView's
1048
+ # holiday calendar. The tracker is fed from ``_set_lib_properties`` with a
1049
+ # single integer compare per bar (``_dg_next_roll``); the heavy path runs once
1050
+ # per trading day. It only activates for 'observed' symbols on charts of at
1051
+ # most daily resolution — everything else resolves arithmetically on demand.
1052
+ _dg_next_roll: float = 0.0 # epoch-sec threshold of the next possible day roll
1053
+ _dg_mode: str = ''
1054
+ _dg_eff: int = 0 # bar open -> last instant offset in seconds (intraday charts)
1055
+ _dg_tz = None
1056
+ _dg_overnight: dict[int, dt_time] = {}
1057
+ _dg_template: list | None = None # identity guard, like the _ttd machinery
1058
+ _dg_day: date | None = None # current trading day
1059
+ _dg_counter: '_ObservedDayCounter | None' = None # year-reset day counter (+ fold)
1060
+ _dg_last_ts: float = 0.0 # previous bar open (epoch sec) — fed to the fold detector
1061
+ _dg_day_starts: dict[int, dict[int, int]] = {} # year -> {ordinal: bar-open ms}
1062
+ _dg_ord_by_day: dict[date, int] = {} # date -> ordinal (current + previous year)
1063
+ _dg_week_first: dict[tuple[int, int], int] = {} # (monday-year, week ordinal) -> ms
1064
+ _dg_month_first: dict[tuple[int, int], int] = {} # (year, month) -> first bar ms
1065
+
1066
+
1067
+ def _dg_reset() -> None:
1068
+ """Reset the scheduled-grid tracker (new run / new script)."""
1069
+ global _dg_next_roll, _dg_mode, _dg_eff, _dg_template, _dg_day, \
1070
+ _dg_counter, _dg_last_ts
1071
+ _dg_next_roll = 0.0
1072
+ _dg_mode = ''
1073
+ _dg_eff = 0
1074
+ _dg_template = None
1075
+ _dg_day = None
1076
+ _dg_counter = None
1077
+ _dg_last_ts = 0.0
1078
+ _dg_day_starts.clear()
1079
+ _dg_ord_by_day.clear()
1080
+ _dg_week_first.clear()
1081
+ _dg_month_first.clear()
1082
+
1083
+
1084
+ # noinspection PyProtectedMember
1085
+ def _dg_on_roll(ts: float) -> None:
1086
+ """
1087
+ Advance the observed-day tracker to the bar at ``ts`` (epoch seconds).
1088
+
1089
+ Only called when a bar reaches ``_dg_next_roll`` — i.e. at most once per
1090
+ trading day, plus once at configuration time. A bar belongs to the trading
1091
+ day its *last* instant falls into (``_dg_eff`` offset): on intraday charts
1092
+ the bar containing the session open starts the new day even when its own
1093
+ timestamp precedes the open.
1094
+
1095
+ :param ts: Current bar open in epoch seconds
1096
+ """
1097
+ global _dg_next_roll, _dg_mode, _dg_eff, _dg_tz, _dg_overnight, \
1098
+ _dg_template, _dg_day, _dg_counter
1099
+
1100
+ opening_hours = syminfo._opening_hours
1101
+ if opening_hours is not _dg_template:
1102
+ # (Re)configure from the symbol template
1103
+ _dg_template = opening_hours
1104
+ _dg_mode = _grid_mode(getattr(syminfo, 'type', None), opening_hours)
1105
+ tz_name = getattr(syminfo, 'timezone', None)
1106
+ _dg_tz = _parse_timezone(tz_name) if tz_name else None
1107
+ _dg_overnight = _overnight_opens(opening_hours, syminfo._session_starts)
1108
+ try:
1109
+ chart_sec = timeframe_module.in_seconds(str(syminfo.period))
1110
+ chart_mod, _ = timeframe_module._process_tf(str(syminfo.period))
1111
+ except (ValueError, AssertionError):
1112
+ chart_sec = 0
1113
+ chart_mod = None
1114
+ if _dg_mode != 'observed' or not 0 < chart_sec <= 86_400:
1115
+ # Arithmetic grids need no tracking, and day counting needs a
1116
+ # stream of at most daily bars
1117
+ _dg_next_roll = _math.inf
1118
+ return
1119
+ _dg_eff = chart_sec - 1 if chart_mod in ('', 'S') else 0
1120
+ # Intraday charts carry per-bar end instants for the holiday half-day
1121
+ # fold; a daily chart stream is already folded.
1122
+ _dg_counter = _ObservedDayCounter(
1123
+ _dg_tz, opening_hours, fold=chart_mod in ('', 'S'))
1124
+ _dg_day = None
1125
+ _dg_day_starts.clear()
1126
+ _dg_ord_by_day.clear()
1127
+ _dg_week_first.clear()
1128
+ _dg_month_first.clear()
1129
+
1130
+ assert _dg_counter is not None
1131
+ eff = ts + _dg_eff
1132
+ td = _trading_day(eff, _dg_tz, _dg_overnight)
1133
+ prev = _dg_day
1134
+ if td != prev:
1135
+ if prev is not None and td.year != prev.year:
1136
+ # Keep only the current and previous year's records
1137
+ for y in [y for y in _dg_day_starts if y < td.year - 1]:
1138
+ del _dg_day_starts[y]
1139
+ for d in [d for d in _dg_ord_by_day if d.year < td.year - 1]:
1140
+ del _dg_ord_by_day[d]
1141
+ for k in [k for k in _dg_week_first if k[0] < td.year - 1]:
1142
+ del _dg_week_first[k]
1143
+ for k in [k for k in _dg_month_first if k[0] < td.year - 1]:
1144
+ del _dg_month_first[k]
1145
+ # Feed the previous day's last bar end so the fold can tell whether it
1146
+ # closed early, then advance the year-reset counter.
1147
+ if _dg_last_ts:
1148
+ _dg_counter.note_bar_end(int(_dg_last_ts) + _dg_eff + 1)
1149
+ ordinal = _dg_counter.ordinal(td)
1150
+ _dg_day = td
1151
+
1152
+ ms = int(ts * 1000)
1153
+ # setdefault: a folded holiday half-day shares the early-close day's
1154
+ # ordinal and must not overwrite that period's first session open.
1155
+ _dg_day_starts.setdefault(td.year, {}).setdefault(ordinal, ms)
1156
+ _dg_ord_by_day[td] = ordinal
1157
+ wy, week = _observed_week_key(td)
1158
+ _dg_week_first.setdefault((wy, week), ms)
1159
+ _dg_month_first.setdefault((td.year, td.month), ms)
1160
+
1161
+ # Next possible roll: the first chart bar whose span reaches a scheduled
1162
+ # session open. Scheduled opens exist even on holidays — a threshold on a
1163
+ # dataless day is harmless, the next real bar recomputes its trading day
1164
+ # from scratch.
1165
+ for i in range(1, 8):
1166
+ open_sec = _trading_day_open_sec(
1167
+ td + timedelta(days=i), _dg_tz, syminfo._session_starts, _dg_overnight)
1168
+ if open_sec > eff:
1169
+ _dg_next_roll = open_sec - _dg_eff
1170
+ break
1171
+ else:
1172
+ _dg_next_roll = ts + 86_400
1173
+
1174
+
1175
+ def _dwm_change_key(timeframe: str, modifier: str, multiplier: int) -> int:
1176
+ """
1177
+ Period identity of the current bar on a multi-period (nD/nW/nM) grid —
1178
+ the ``timeframe.change`` helper. Kept here so the transformed
1179
+ ``_timeframe_change`` module only makes single-attribute ``lib.*`` calls.
1180
+
1181
+ :param timeframe: The requested timeframe string
1182
+ :param modifier: 'D', 'W' or 'M' (from ``_process_tf``)
1183
+ :param multiplier: Period multiplier (> 1)
1184
+ :return: The period's opening time in milliseconds
1185
+ """
1186
+ return _dwm_bar_time(
1187
+ Resampler.get_resampler(timeframe), modifier, multiplier, _time)
1188
+
1189
+
1190
+ def _chart_span_off_ms() -> int:
1191
+ """
1192
+ Offset from a chart bar's open to its last instant, in milliseconds.
1193
+
1194
+ A chart bar belongs to the D/W/M period its *last* instant falls into: the
1195
+ bar containing a session open is the new trading day's first bar even when
1196
+ its own timestamp precedes the open (e.g. a 17:05 session open on a
1197
+ 240-minute grid — the 17:00 bar starts the new day). D/W/M chart bars are
1198
+ session-aligned by construction, so only intraday charts need the offset.
1199
+
1200
+ :return: ``chart bar span - 1`` for intraday chart periods, else 0
1201
+ """
1202
+ try:
1203
+ # noinspection PyProtectedMember
1204
+ chart_mod, _ = timeframe_module._process_tf(str(syminfo.period))
1205
+ if chart_mod in ('', 'S'):
1206
+ return timeframe_module.in_seconds(str(syminfo.period)) * 1000 - 1
1207
+ except (ValueError, AssertionError):
1208
+ pass
1209
+ return 0
1210
+
1211
+
1212
+ def _dg_trading_day():
1213
+ """
1214
+ The current bar's trading day (``datetime.date``).
1215
+
1216
+ Uses the tracker's record when it is active ('observed' symbols on at most
1217
+ daily charts); otherwise derives it from ``_datetime`` — the bar's
1218
+ exchange-local datetime — advanced to the bar's last instant
1219
+ (:func:`_chart_span_off_ms`), with the overnight roll (a bar reaching its
1220
+ weekday's overnight open belongs to the next calendar day). The tracker's
1221
+ configuration pass runs on the first bar of every run, so
1222
+ ``_dg_overnight`` is populated whenever real data is streaming.
1223
+
1224
+ :return: Trading day of the current bar
1225
+ """
1226
+ if _dg_day is not None:
1227
+ return _dg_day
1228
+ dt_loc = _datetime
1229
+ off = _chart_span_off_ms()
1230
+ if off:
1231
+ dt_loc = dt_loc + timedelta(milliseconds=off)
1232
+ d = dt_loc.date()
1233
+ if _dg_overnight:
1234
+ t0 = _dg_overnight.get(dt_loc.weekday())
1235
+ if t0 is not None and dt_loc.time() >= t0:
1236
+ d += timedelta(days=1)
1237
+ return d
1238
+
1239
+
1240
+ # noinspection PyProtectedMember
1241
+ def _dwm_bar_time(resampler: Resampler, modifier: str, multiplier: int,
1242
+ current_time_ms: int) -> int:
1243
+ """
1244
+ Multi-period (nD/nW/nM) bar open time on the scheduled grid.
1245
+
1246
+ 'calendar'/'weekday' symbols resolve arithmetically; 'observed' symbols
1247
+ look up the tracker's records and fall back to the weekday grid for
1248
+ timestamps outside the tracked window (pre-data or future times).
1249
+
1250
+ ``current_time_ms`` is a chart bar open; the bar is resolved by its *last*
1251
+ instant (:func:`_chart_span_off_ms`), so the bar containing a session open
1252
+ counts as the new trading day's first bar.
1253
+
1254
+ :param resampler: Resampler of the requested timeframe
1255
+ :param modifier: 'D', 'W' or 'M'
1256
+ :param multiplier: Period multiplier (> 1)
1257
+ :param current_time_ms: Chart bar open to resolve, in milliseconds
1258
+ :return: Bar opening time in milliseconds
1259
+ """
1260
+ eff_ms = current_time_ms + _chart_span_off_ms()
1261
+ if _dg_mode != 'observed' or _dg_next_roll == _math.inf:
1262
+ # Pure arithmetic — also the fallback when the tracker is inactive
1263
+ # (chart resolution above daily)
1264
+ return resampler.get_bar_time(
1265
+ eff_ms, _dg_tz, syminfo._session_starts,
1266
+ syminfo._opening_hours, _dg_mode or None)
1267
+
1268
+ if current_time_ms == _time and _dg_day is not None:
1269
+ td = _dg_day
1270
+ else:
1271
+ td = _trading_day(eff_ms // 1000, _dg_tz, _dg_overnight)
1272
+
1273
+ if modifier == 'D':
1274
+ ordinal = _dg_ord_by_day.get(td)
1275
+ if ordinal is not None:
1276
+ base = (ordinal // multiplier) * multiplier
1277
+ days = _dg_day_starts.get(td.year)
1278
+ if days is not None:
1279
+ for i in range(base, ordinal + 1):
1280
+ ms = days.get(i)
1281
+ if ms is not None:
1282
+ return ms
1283
+ elif modifier == 'W':
1284
+ wy, week = _observed_week_key(td)
1285
+ base = (week // multiplier) * multiplier
1286
+ for i in range(base, week + 1):
1287
+ ms = _dg_week_first.get((wy, i))
1288
+ if ms is not None:
1289
+ return ms
1290
+ else: # 'M'
1291
+ m0 = ((td.month - 1) // multiplier) * multiplier + 1
1292
+ for m in range(m0, td.month + 1):
1293
+ ms = _dg_month_first.get((td.year, m))
1294
+ if ms is not None:
1295
+ return ms
1296
+
1297
+ # Outside the tracked window — weekday-grid approximation
1298
+ return resampler.get_bar_time(
1299
+ eff_ms, _dg_tz, syminfo._session_starts,
1300
+ syminfo._opening_hours, 'weekday')
1301
+
1302
+
1303
+ # Single-day ("D"/"1D") bar open cache: TradingView daily bars open at the
1304
+ # trading day's session open (FX Monday opens Sunday 17:00, TSE 09:00), not at
1305
+ # the calendar-midnight floor. Rebuilt when the session template is replaced
1306
+ # (identity guard, like the ``_ttd``/``_tdc`` machinery).
1307
+ _dbt_guard: tuple | None = None # (opening_hours, session_starts) identities
1308
+ _dbt_tz = None
1309
+ _dbt_on: dict = {}
1310
+ _dbt_starts: list | None = None
1311
+
1312
+
1313
+ # noinspection PyProtectedMember
1314
+ def _d_bar_time(current_time_ms: int) -> int:
1315
+ """
1316
+ Daily ("D") bar open time: the session open of the bar's trading day.
1317
+
1318
+ The bar is resolved by its *last* instant (:func:`_chart_span_off_ms`), so
1319
+ the chart bar containing a session open counts as the new trading day's
1320
+ first bar. Falls back to the trading day's local midnight when no session
1321
+ template is known.
1322
+
1323
+ :param current_time_ms: Chart bar open to resolve, in milliseconds
1324
+ :return: Bar opening time in milliseconds
1325
+ """
1326
+ global _dbt_guard, _dbt_tz, _dbt_on, _dbt_starts
1327
+ oh = syminfo._opening_hours
1328
+ ss = syminfo._session_starts
1329
+ if _dbt_guard is None or _dbt_guard[0] is not oh or _dbt_guard[1] is not ss:
1330
+ tz_name = getattr(syminfo, 'timezone', None)
1331
+ _dbt_tz = _parse_timezone(tz_name) if tz_name else None
1332
+ _dbt_on = _overnight_opens(oh or None, ss or None)
1333
+ _dbt_starts = ss or None
1334
+ _dbt_guard = (oh, ss)
1335
+ eff_sec = (current_time_ms + _chart_span_off_ms()) // 1000
1336
+ td = _trading_day(eff_sec, _dbt_tz, _dbt_on)
1337
+ return _trading_day_open_sec(td, _dbt_tz, _dbt_starts, _dbt_on) * 1000
1338
+
1339
+
1340
+ @module_function_property
1341
+ def time(timeframe: str | None = None, session: str | int | None = None,
1342
+ timezone: str | None = None, bars_back: int = 0) -> PyneInt:
1343
+ """
1344
+ The time function returns the UNIX time of the current bar for the specified timeframe
1345
+ and session or NA if the time point is out of session.
1346
+
1347
+ Usage examples:
1348
+ - time() - Current bar time
1349
+ - time("60") - Current 1-hour bar start time
1350
+ - time("1D", "0930-1600") - Daily bar time if within session
1351
+ - time("60", "0930-1600:23456", "America/New_York") - With timezone
1352
+ - time("60", -1) - Expected start time of the next 1-hour bar
1353
+
1354
+ :param timeframe: The timeframe to get the time for (e.g., "D", "60", "240").
1355
+ An empty string selects the chart's timeframe.
1356
+ If None, returns current bar time.
1357
+ :param session: Session specification string (e.g., "0930-1600", "0000-0000:23456").
1358
+ Format: "HHMM-HHMM" or "HHMM-HHMM:days" where days are 1234567 (1=Sun, 7=Sat).
1359
+ An int value here is treated as ``bars_back`` (Pine's
1360
+ ``time(timeframe, bars_back)`` overload).
1361
+ :param timezone: Timezone for the session (e.g., "GMT+2", "America/New_York").
1362
+ If None, uses exchange timezone.
1363
+ :param bars_back: Bar offset on the chart's timeframe: positive values refer to past
1364
+ bars, negative values to the expected times of future bars. The offset
1365
+ is computed on a continuous time grid (exact for 24/7 markets).
1366
+ :return: UNIX time in milliseconds or NA if bar is outside session or invalid parameters
1367
+ """
1368
+ # Pine overload: time(timeframe, bars_back) -- an int second argument is a bar offset
1369
+ if isinstance(session, int) and not isinstance(session, bool):
1370
+ bars_back = session
1371
+ session = None
1372
+
1373
+ if timeframe is None:
1374
+ return _time
1375
+
1376
+ # An empty string selects the chart's timeframe
1377
+ if timeframe == '':
1378
+ timeframe = str(syminfo.period)
1379
+
1380
+ # Get resampler for the requested timeframe
1381
+ try:
1382
+ resampler = Resampler.get_resampler(timeframe)
1383
+ except ValueError:
1384
+ # Invalid timeframe
1385
+ return NA(int)
1386
+
1387
+ # Get the current bar time for the requested timeframe
1388
+ current_time_ms = _time
1389
+ if bars_back:
1390
+ try:
1391
+ current_time_ms -= bars_back * timeframe_module.in_seconds(str(syminfo.period)) * 1000
1392
+ except (ValueError, AssertionError):
1393
+ return NA(int)
1394
+ # noinspection PyProtectedMember
1395
+ modifier, multiplier = timeframe_module._process_tf(timeframe)
1396
+ if modifier in ('D', 'W', 'M') and multiplier > 1:
1397
+ # noinspection PyProtectedMember
1398
+ if (modifier, multiplier) == timeframe_module._process_tf(str(syminfo.period)):
1399
+ # The chart's own bars are the requested grid
1400
+ bar_time = current_time_ms
1401
+ else:
1402
+ bar_time = _dwm_bar_time(resampler, modifier, multiplier, current_time_ms)
1403
+ elif modifier == 'D':
1404
+ # noinspection PyProtectedMember
1405
+ if ('D', 1) == timeframe_module._process_tf(str(syminfo.period)):
1406
+ bar_time = current_time_ms
1407
+ else:
1408
+ # TradingView daily bars open at the trading day's session open
1409
+ # (the previous evening for overnight markets), not at midnight
1410
+ bar_time = _d_bar_time(current_time_ms)
1411
+ else:
1412
+ bar_time = resampler.get_bar_time(current_time_ms, *_intraday_session_args(timeframe))
1413
+
1414
+ if session is None:
1415
+ # No session specified, return the bar time
1416
+ return bar_time
1417
+ if not isinstance(session, str):
1418
+ # A bool slips past the int(bars_back) overload guard (bool is an int):
1419
+ # it is not a valid session specification.
1420
+ return NA(int)
1421
+
1422
+ # Parse session string
1423
+ try:
1424
+ session_info = _parse_session_string(session, timezone)
1425
+ except ValueError:
1426
+ # Invalid session string
1427
+ return NA(int)
1428
+
1429
+ # Check if the bar is within the session
1430
+ try:
1431
+ if _is_bar_in_session(bar_time, session_info, timeframe):
1432
+ return bar_time
1433
+ else:
1434
+ return NA(int)
1435
+ except TimezoneNotFoundError:
1436
+ # A missing/unresolvable timezone is a configuration error: surface it with
1437
+ # the actionable message instead of silently treating every bar as closed.
1438
+ raise
1439
+ except Exception: # noqa
1440
+ # Error during session validation
1441
+ return NA(int)
1442
+
1443
+
1444
+ @module_property
1445
+ def timenow():
1446
+ """
1447
+ Current time in UNIX format. It is the number of milliseconds that have elapsed since 00:00:00 UTC, 1 January 1970.
1448
+
1449
+ :return: Current time in milliseconds
1450
+ """
1451
+ # Get current UTC time and convert to milliseconds since Unix epoch
1452
+ return int(datetime.now(UTC).timestamp() * 1000)
1453
+
1454
+
1455
+ # ``time_tradingday`` cache. The strategy engine calls the property on every bar
1456
+ # (intraday risk day-rollover), so the result is memoized per bar, keyed by the
1457
+ # identity of ``_datetime`` — the function's actual input. Every bar installs a
1458
+ # fresh (immutable) datetime object, so an identity hit guarantees an identical
1459
+ # result; anything that swaps ``_datetime`` (including tests driving it
1460
+ # directly) misses the memo and recomputes. NOT keyed by calendar date, which
1461
+ # would be wrong for overnight sessions where bars before/after the session
1462
+ # open on the same date belong to different trading days. The session-structure
1463
+ # table is rebuilt whenever ``syminfo._opening_hours`` is replaced
1464
+ # (``_set_lib_syminfo_properties`` always assigns a fresh list) or
1465
+ # ``syminfo.period`` changes.
1466
+ _ttd_memo_dt: datetime | None = None
1467
+ _ttd_memo_result: int = 0
1468
+ _ttd_session_hours: list | None = None
1469
+ _ttd_session_period: str | None = None
1470
+ _ttd_overnight_by_wd: dict[int, list[dt_time]] = {}
1471
+ _ttd_period_delta: timedelta = timedelta()
1472
+ _EPOCH_ORDINAL = 719163 # date(1970, 1, 1).toordinal()
1473
+
1474
+
1475
+ # noinspection PyProtectedMember
1476
+ @module_function_property
1477
+ def time_tradingday() -> PyneInt:
1478
+ """
1479
+ The beginning time of the trading day the current bar belongs to, as a UNIX
1480
+ timestamp in milliseconds. It is 00:00 UTC of the calendar date — expressed in
1481
+ the exchange timezone — on which the bar's trading session ends.
1482
+
1483
+ For symbols whose session crosses midnight (e.g. forex and futures overnight
1484
+ sessions) a bar that reaches into the session start belongs to the next calendar
1485
+ day's trading day, matching TradingView — including the boundary bar whose window
1486
+ merely contains the open (a 17:00-18:00 bar for a 17:05 open). For symbols whose
1487
+ session stays within a single calendar day (stocks, 24/7 crypto) it is simply
1488
+ 00:00 UTC of the bar's exchange-timezone date.
1489
+
1490
+ :return: UNIX time in milliseconds of 00:00 UTC on the trading day's date
1491
+ """
1492
+ global _ttd_memo_dt, _ttd_memo_result, _ttd_session_hours, _ttd_session_period, \
1493
+ _ttd_overnight_by_wd, _ttd_period_delta
1494
+
1495
+ opening_hours = syminfo._opening_hours
1496
+ period = syminfo.period
1497
+ if opening_hours is not _ttd_session_hours or period != _ttd_session_period:
1498
+ # Session structure changed — rebuild the per-weekday table of overnight
1499
+ # session opens (the only entries that can roll the trading day).
1500
+ _ttd_overnight_by_wd = _overnight_starts_by_weekday(opening_hours)
1501
+ _ttd_period_delta = timedelta(seconds=timeframe_module.in_seconds(period))
1502
+ _ttd_session_hours = opening_hours
1503
+ _ttd_session_period = period
1504
+ _ttd_memo_dt = None
1505
+
1506
+ if _datetime is _ttd_memo_dt:
1507
+ return _ttd_memo_result
1508
+
1509
+ local_dt = _datetime # already expressed in the exchange timezone
1510
+ trade_date = local_dt.date()
1511
+
1512
+ # Roll into the next trading day when the bar overlaps the evening portion of an
1513
+ # overnight session. A bar whose window merely *contains* the session open — e.g.
1514
+ # a 17:00-18:00 bar when the session opens at 17:05 — already belongs to the new
1515
+ # trading day, matching TradingView and ``session.isfirstbar_regular``. Comparing
1516
+ # the bar's *end* against the open captures that boundary bar; comparing only the
1517
+ # bar's start would leave it in the previous day whenever the open does not land
1518
+ # exactly on a bar boundary.
1519
+ overnight_starts = _ttd_overnight_by_wd.get(local_dt.weekday())
1520
+ if overnight_starts:
1521
+ bar_end = local_dt + _ttd_period_delta
1522
+ for start in overnight_starts:
1523
+ session_open = local_dt.replace(
1524
+ hour=start.hour, minute=start.minute, second=start.second, microsecond=0)
1525
+ if bar_end > session_open:
1526
+ trade_date += timedelta(days=1)
1527
+ break
1528
+
1529
+ # 00:00 UTC of the trading day's date — pure ordinal arithmetic (UTC has no
1530
+ # DST, so this is exactly ``datetime(y, m, d, tzinfo=UTC).timestamp() * 1000``).
1531
+ result = (trade_date.toordinal() - _EPOCH_ORDINAL) * 86_400_000
1532
+ _ttd_memo_dt = local_dt
1533
+ _ttd_memo_result = result
1534
+ return result
1535
+
1536
+
1537
+ # Trading-day close cap for ``time_close``. TradingView closes a bar at
1538
+ # ``min(bar open + timeframe span, end of the bar's trading day)``: the last —
1539
+ # possibly shortened — bar of the day closes when the trading day ends, while
1540
+ # intra-day gaps (lunch breaks) and continuous overnight sessions do not cap.
1541
+ # ``_tdc_by_wd`` maps a trading day's weekday to its closing instant as a
1542
+ # (time-of-day, calendar-day offset from the trading-day date) pair; rebuilt
1543
+ # whenever ``syminfo._opening_hours`` is replaced (identity guard, like the
1544
+ # ``_ttd`` machinery above).
1545
+ _tdc_hours: list | None = None # identity guard
1546
+ _tdc_by_wd: dict[int, tuple[dt_time, int]] = {} # weekday -> (end tod, +days)
1547
+ _tdc_overnight_by_wd: dict[int, list[dt_time]] = {}
1548
+ _tdc_tz = None
1549
+
1550
+
1551
+ def _tdc_rebuild(opening_hours: list) -> None:
1552
+ """
1553
+ Rebuild the per-weekday trading-day close table from ``opening_hours``.
1554
+
1555
+ Each interval's end instant is assigned to the trading day it closes —
1556
+ rolled to the next day when the instant lies inside an overnight session —
1557
+ and the latest end per trading day wins (the lunch-break morning end loses
1558
+ to the afternoon close).
1559
+
1560
+ :param opening_hours: ``syminfo._opening_hours`` (``SymInfoInterval`` list)
1561
+ """
1562
+ global _tdc_hours, _tdc_by_wd, _tdc_overnight_by_wd, _tdc_tz
1563
+
1564
+ _tdc_overnight_by_wd = _overnight_starts_by_weekday(opening_hours)
1565
+ _tdc_by_wd = _close_table_by_weekday(opening_hours, _tdc_overnight_by_wd)
1566
+ tz_name = getattr(syminfo, 'timezone', None)
1567
+ _tdc_tz = _parse_timezone(tz_name) if tz_name else None
1568
+ _tdc_hours = opening_hours
1569
+
1570
+
1571
+ # noinspection PyProtectedMember
1572
+ def _tdc_cap_ms(bar_open_ms: int, bar_close_ms: int) -> int:
1573
+ """
1574
+ Cap a computed bar close at the end of the bar's trading day.
1575
+
1576
+ :param bar_open_ms: Bar opening time (UNIX ms)
1577
+ :param bar_close_ms: Uncapped close, i.e. open + timeframe span (UNIX ms)
1578
+ :return: ``min(bar_close_ms, trading day end)``; ``bar_close_ms`` unchanged
1579
+ when no session template is known or none ends on the bar's day
1580
+ """
1581
+ opening_hours = syminfo._opening_hours
1582
+ if not opening_hours:
1583
+ return bar_close_ms
1584
+ if opening_hours is not _tdc_hours:
1585
+ _tdc_rebuild(opening_hours)
1586
+ if not _tdc_by_wd:
1587
+ return bar_close_ms
1588
+
1589
+ # The current chart bar (the hot path) reuses the runner-installed local
1590
+ # datetime instead of converting again.
1591
+ dt_local = _datetime if bar_open_ms == _time \
1592
+ else datetime.fromtimestamp(bar_open_ms / 1000, tz=_tdc_tz)
1593
+ trade_date = dt_local.date()
1594
+
1595
+ # Overnight roll: a bar whose window reaches into a session opening this
1596
+ # calendar day and crossing midnight belongs to the next trading day
1597
+ # (same rule as ``time_tradingday``).
1598
+ opens = _tdc_overnight_by_wd.get(dt_local.weekday())
1599
+ if opens:
1600
+ for o in opens:
1601
+ session_open = dt_local.replace(
1602
+ hour=o.hour, minute=o.minute, second=o.second, microsecond=0)
1603
+ if bar_close_ms > session_open.timestamp() * 1000:
1604
+ trade_date += timedelta(days=1)
1605
+ break
1606
+
1607
+ entry = _tdc_by_wd.get(trade_date.weekday())
1608
+ if entry is None:
1609
+ return bar_close_ms
1610
+ end_tod, offset = entry
1611
+ end_date = trade_date + timedelta(days=offset)
1612
+ day_end_ms = int(datetime(
1613
+ end_date.year, end_date.month, end_date.day,
1614
+ end_tod.hour, end_tod.minute, end_tod.second,
1615
+ tzinfo=_tdc_tz,
1616
+ ).timestamp() * 1000)
1617
+ if day_end_ms <= bar_open_ms: # degenerate template — never close before the open
1618
+ return bar_close_ms
1619
+ return min(bar_close_ms, day_end_ms)
1620
+
1621
+
1622
+ @module_function_property
1623
+ def time_close(timeframe: str | None = None, session: str | int | None = None,
1624
+ timezone: str | None = None, bars_back: int = 0) -> PyneInt:
1625
+ """
1626
+ The time_close function returns the UNIX time of the current bar's close for the specified timeframe
1627
+ and session or NA if the time point is outside the session.
1628
+
1629
+ Usage examples:
1630
+ - time_close() - Current bar close time
1631
+ - time_close("60") - Current 1-hour bar close time
1632
+ - time_close("1D", "0930-1600") - Daily bar close time if within session
1633
+ - time_close("60", "0930-1600:23456", "America/New_York") - With timezone
1634
+ - time_close("60", -1) - Expected close time of the next 1-hour bar
1635
+
1636
+ :param timeframe: The timeframe to get the close time for (e.g., "D", "60", "240").
1637
+ An empty string selects the chart's timeframe.
1638
+ If None, returns current bar close time.
1639
+ :param session: Session specification string (e.g., "0930-1600", "0000-0000:23456").
1640
+ Format: "HHMM-HHMM" or "HHMM-HHMM:days" where days are 1234567 (1=Sun, 7=Sat).
1641
+ An int value here is treated as ``bars_back`` (Pine's
1642
+ ``time_close(timeframe, bars_back)`` overload).
1643
+ :param timezone: Timezone for the session (e.g., "GMT+2", "America/New_York").
1644
+ If None, uses exchange timezone.
1645
+ :param bars_back: Bar offset on the chart's timeframe: positive values refer to past
1646
+ bars, negative values to the expected times of future bars. The offset
1647
+ is computed on a continuous time grid (exact for 24/7 markets).
1648
+ :return: UNIX time in milliseconds of bar close or NA if bar is outside session or invalid parameters
1649
+ """
1650
+ # Pine overload: time_close(timeframe, bars_back) -- an int second argument is a bar offset
1651
+ if isinstance(session, int) and not isinstance(session, bool):
1652
+ bars_back = session
1653
+ session = None
1654
+
1655
+ if timeframe is None:
1656
+ # Close time of the current chart bar — capped at the trading-day end,
1657
+ # because the last bar of a session may be shortened
1658
+ try:
1659
+ close_ms = _time + timeframe_module.in_seconds(str(syminfo.period)) * 1000
1660
+ # noinspection PyProtectedMember
1661
+ chart_mod, chart_mult = timeframe_module._process_tf(str(syminfo.period))
1662
+ except (ValueError, AssertionError):
1663
+ return NA(int)
1664
+ if chart_mod in ('', 'S') or (chart_mod == 'D' and chart_mult == 1):
1665
+ close_ms = _tdc_cap_ms(_time, close_ms)
1666
+ return close_ms
1667
+
1668
+ # An empty string selects the chart's timeframe
1669
+ if timeframe == '':
1670
+ timeframe = str(syminfo.period)
1671
+
1672
+ # Get resampler for the requested timeframe
1673
+ try:
1674
+ resampler = Resampler.get_resampler(timeframe)
1675
+ except ValueError:
1676
+ # Invalid timeframe
1677
+ return NA(int)
1678
+
1679
+ # Get the current bar time for the requested timeframe
1680
+ current_time_ms = _time
1681
+ if bars_back:
1682
+ try:
1683
+ current_time_ms -= bars_back * timeframe_module.in_seconds(str(syminfo.period)) * 1000
1684
+ except (ValueError, AssertionError):
1685
+ return NA(int)
1686
+ # noinspection PyProtectedMember
1687
+ modifier, multiplier = timeframe_module._process_tf(timeframe)
1688
+ if modifier in ('D', 'W', 'M') and multiplier > 1:
1689
+ # noinspection PyProtectedMember
1690
+ if (modifier, multiplier) == timeframe_module._process_tf(str(syminfo.period)):
1691
+ bar_start_time = current_time_ms
1692
+ else:
1693
+ bar_start_time = _dwm_bar_time(resampler, modifier, multiplier, current_time_ms)
1694
+ elif modifier == 'D':
1695
+ # noinspection PyProtectedMember
1696
+ if ('D', 1) == timeframe_module._process_tf(str(syminfo.period)):
1697
+ bar_start_time = current_time_ms
1698
+ else:
1699
+ # TradingView daily bars open at the trading day's session open
1700
+ # (the previous evening for overnight markets), not at midnight
1701
+ bar_start_time = _d_bar_time(current_time_ms)
1702
+ else:
1703
+ bar_start_time = resampler.get_bar_time(current_time_ms, *_intraday_session_args(timeframe))
1704
+
1705
+ # Calculate bar close time by adding timeframe duration
1706
+ try:
1707
+ tf_seconds = timeframe_module.in_seconds(timeframe)
1708
+ bar_close_time = bar_start_time + (tf_seconds * 1000) # Convert to milliseconds
1709
+ except (ValueError, AssertionError):
1710
+ return NA(int)
1711
+
1712
+ if modifier in ('', 'S') or (modifier == 'D' and multiplier == 1):
1713
+ # TradingView closes the (possibly shortened) last bar of the day at
1714
+ # the trading-day end; weekly/monthly and multi-period close times
1715
+ # are not session-capped.
1716
+ bar_close_time = _tdc_cap_ms(bar_start_time, bar_close_time)
1717
+
1718
+ if session is None:
1719
+ # No session specified, return the bar close time
1720
+ return bar_close_time
1721
+ if not isinstance(session, str):
1722
+ # A bool slips past the int(bars_back) overload guard (bool is an int):
1723
+ # it is not a valid session specification.
1724
+ return NA(int)
1725
+
1726
+ # Parse session string
1727
+ try:
1728
+ session_info = _parse_session_string(session, timezone)
1729
+ except ValueError:
1730
+ # Invalid session string
1731
+ return NA(int)
1732
+
1733
+ # Check if the bar is within the session (using bar start time for session validation)
1734
+ try:
1735
+ if _is_bar_in_session(bar_start_time, session_info, timeframe):
1736
+ return bar_close_time
1737
+ else:
1738
+ return NA(int)
1739
+ except TimezoneNotFoundError:
1740
+ # A missing/unresolvable timezone is a configuration error: surface it with
1741
+ # the actionable message instead of silently treating every bar as closed.
1742
+ raise
1743
+ except Exception: # noqa
1744
+ # Error during session validation
1745
+ return NA(int)
1746
+
1747
+
1748
+ # noinspection PyShadowingNames
1749
+ @module_function_property
1750
+ def weekofyear(time: int | None = None, timezone: str | None = None) -> int:
1751
+ """
1752
+ Week of the year
1753
+
1754
+ :param time: The time to get the week of the year from, if None the current time is used
1755
+ :param timezone: The timezone of the time, if not specified the exchange timezone is used
1756
+ :return: The week of the year
1757
+ """
1758
+ return _get_dt(time, timezone).isocalendar()[1]
1759
+
1760
+
1761
+ # noinspection PyShadowingNames
1762
+ @module_function_property
1763
+ def year(time: int | None = None, timezone: str | None = None) -> int:
1764
+ """
1765
+ Year
1766
+
1767
+ :param time: The time to get the year from, if None the current time is used
1768
+ :param timezone: The timezone of the time, if not specified the exchange timezone is used
1769
+ :return: The year
1770
+ """
1771
+ return _get_dt(time, timezone).year