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/string.py ADDED
@@ -0,0 +1,649 @@
1
+ from typing import Any
2
+ import re
3
+
4
+ from functools import lru_cache
5
+
6
+ from datetime import datetime, UTC
7
+ from decimal import Decimal, ROUND_HALF_UP
8
+
9
+ from ..types.na import NA, na_float
10
+ from ..types.pine_types import PyneFloat, PyneInt, PyneStr, PyneBool
11
+
12
+ from ..types.format import Format
13
+ from . import format as _format
14
+ from . import syminfo as _syminfo
15
+ from .. import lib
16
+ from ..core import safe_convert
17
+
18
+ from pynecore.core.datetime import parse_timezone as _parse_timezone
19
+
20
+ __all__ = ['contains', 'endswith', 'format', 'format_time', 'length', 'lower', 'match', 'pos', 'repeat', 'replace',
21
+ 'replace_all', 'split', 'startswith', 'substring', 'tonumber', 'tostring', 'trim', 'upper']
22
+
23
+
24
+ #
25
+ # Private helper functions
26
+ #
27
+
28
+ # noinspection PyProtectedMember
29
+ def _format_number(value: float | int | NA, fmt_type: str = '', precision: str = '#.###') -> str:
30
+ """
31
+ Format a number according to Pine rules.
32
+
33
+ Format strings use # for optional digits and 0 for required digits:
34
+ - #.## -> removes trailing zeros after decimal
35
+ - #.00 -> keeps trailing zeros
36
+ - # -> rounds to integer
37
+ - 000.00 -> adds leading zeros, keeps trailing zeros
38
+
39
+ Special formats:
40
+ - integer: rounds to integer
41
+ - currency: $X.XX format
42
+ - percent: adds % and multiplies by 100
43
+ - mintick: rounds to symbol's mintick
44
+ - volume: adds K/M/B suffixes
45
+ - price: same as currency
46
+ - inherit: uses script's precision
47
+
48
+ :param value: Value to format
49
+ :param fmt_type: Format type (integer, currency, percent, mintick, volume, price, inherit)
50
+ :param precision: Custom precision format string (like '#.##')
51
+ :return: Formatted string
52
+ """
53
+ if isinstance(value, NA) or value is None or value != value: # NA object or native nan
54
+ return "NaN"
55
+
56
+ # Handle special formats first
57
+ if fmt_type == _format.mintick:
58
+ tick_size = _syminfo.mintick
59
+ value = round(value / tick_size) * tick_size # type: ignore
60
+ # Get decimal places from mintick
61
+ tick_str = str(tick_size)
62
+ if 'e' in tick_str or 'E' in tick_str:
63
+ # Handle scientific notation
64
+ # Convert to decimal format to count decimal places
65
+ tick_decimal = f"{tick_size:.20f}".rstrip('0')
66
+ if '.' in tick_decimal:
67
+ dec_str = tick_decimal.split('.')[1]
68
+ else:
69
+ dec_str = ''
70
+ else:
71
+ # Handle regular decimal notation
72
+ if '.' in tick_str:
73
+ dec_str = tick_str.rstrip('0').split('.')[1]
74
+ else:
75
+ dec_str = ''
76
+ precision = '#.' + '#' * len(dec_str)
77
+
78
+ elif fmt_type == _format.inherit:
79
+ assert lib._script is not None and lib._script.precision is not None
80
+ precision = '#.' + '#' * int(lib._script.precision)
81
+
82
+ elif fmt_type == _format.volume:
83
+ if value >= 1_000_000_000:
84
+ return f"{value / 1_000_000_000:.2f}B"
85
+ elif value >= 1_000_000:
86
+ return f"{value / 1_000_000:.2f}M"
87
+ elif value >= 1_000:
88
+ return f"{value / 1_000:.2f}K"
89
+ return str(int(value))
90
+
91
+ elif fmt_type == 'integer':
92
+ return str(int(round(value)))
93
+
94
+ elif fmt_type == _format.percent:
95
+ return f"{value * 100:.0f}%"
96
+
97
+ # Convert to Decimal for precise handling
98
+ d = Decimal(str(value))
99
+
100
+ if fmt_type == 'price' or fmt_type == 'currency':
101
+ # Format as currency with 2 decimals
102
+ d = d.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
103
+ return f"${d:,.2f}"
104
+
105
+ # Pine number patterns follow Java DecimalFormat: an optional negative
106
+ # subpattern after ';', and literal prefix/suffix (e.g. '+', '-', '$', 'R')
107
+ # around the digit pattern. Pick the subpattern by sign, format the
108
+ # magnitude with the bare digit pattern, and re-attach the affixes.
109
+ prefix = ''
110
+ suffix = ''
111
+ subpatterns = precision.split(';')
112
+ if value < 0:
113
+ value = -value
114
+ if len(subpatterns) > 1:
115
+ chosen = subpatterns[1]
116
+ else:
117
+ chosen = subpatterns[0]
118
+ prefix = '-'
119
+ else:
120
+ chosen = subpatterns[0]
121
+
122
+ pattern_chars = '#0.,'
123
+ digit_idx = [i for i, c in enumerate(chosen) if c in pattern_chars]
124
+ if digit_idx:
125
+ lo, hi = digit_idx[0], digit_idx[-1]
126
+ prefix += chosen[:lo]
127
+ suffix = chosen[hi + 1:]
128
+ precision = chosen[lo:hi + 1]
129
+
130
+ # Parse format string
131
+ if '.' in precision:
132
+ before, after = precision.split('.')
133
+ else:
134
+ before, after = precision, ''
135
+
136
+ # Count required digits before decimal
137
+ required_before = len([c for c in before if c == '0'])
138
+
139
+ # Count required vs optional digits after decimal
140
+ required_decimals = len([c for c in after if c == '0'])
141
+ max_decimals = len(after)
142
+
143
+ # Format the number
144
+ if max_decimals == 0:
145
+ # Integer format
146
+ result = str(int(round(value)))
147
+ else:
148
+ # Float format
149
+ formatted = f"{value:.{max_decimals}f}"
150
+ if required_decimals == 0:
151
+ # Remove trailing zeros if all places are optional (#)
152
+ formatted = formatted.rstrip('0').rstrip('.')
153
+ else:
154
+ # Keep required number of decimal places
155
+ decimal_part = formatted.split('.')[1]
156
+ if len(decimal_part) > required_decimals:
157
+ # Remove trailing zeros after required places
158
+ decimal_part = decimal_part[:required_decimals].rstrip('0')
159
+ if decimal_part:
160
+ formatted = f"{formatted.split('.')[0]}.{decimal_part}"
161
+ else:
162
+ formatted = formatted.split('.')[0]
163
+
164
+ # Handle required decimal places
165
+ if '.' in formatted and required_decimals > 0:
166
+ decimal_part = formatted.split('.')[1]
167
+ while len(decimal_part) < required_decimals:
168
+ decimal_part += '0'
169
+ result = f"{formatted.split('.')[0]}.{decimal_part}"
170
+ else:
171
+ result = formatted
172
+
173
+ # Handle leading zeros if needed (the sign, if any, is carried by ``prefix``)
174
+ if required_before > 0:
175
+ int_part = result.split('.')[0]
176
+ while len(int_part) < required_before:
177
+ int_part = '0' + int_part
178
+ if '.' in result:
179
+ result = f"{int_part}.{result.split('.')[1]}"
180
+ else:
181
+ result = int_part
182
+
183
+ return prefix + result + suffix
184
+
185
+
186
+ def _format_value(value: Any, _no_format_numbers=False) -> str:
187
+ """ Format a value in Pine-compatible way """
188
+ if isinstance(value, list):
189
+ res = f"[{', '.join(_format_value(x, _no_format_numbers=True) for x in value)}]"
190
+ return res
191
+ elif isinstance(value, str):
192
+ return value
193
+ elif isinstance(value, float):
194
+ # Use default formatting for floats
195
+ return _format_number(value) if not _no_format_numbers else str(value)
196
+ elif isinstance(value, bool):
197
+ return str(value).lower()
198
+ elif isinstance(value, NA) or value is None:
199
+ return "NaN"
200
+ return str(value)
201
+
202
+
203
+ @lru_cache(maxsize=32)
204
+ def _datatime_fmt_tv2py(fmt: str) -> str:
205
+ """
206
+ Convert Pine format to Python format
207
+
208
+ :param fmt: Pine format string
209
+ :return: Python format string
210
+ """
211
+ # Handle escaped parts first
212
+ escaped_parts = {}
213
+ i = 0
214
+ while "'" in fmt:
215
+ start = fmt.find("'")
216
+ end = fmt.find("'", start + 1)
217
+ if end == -1:
218
+ break
219
+ key = f"__ESC{i}__"
220
+ escaped_parts[key] = fmt[start + 1:end]
221
+ fmt = fmt[:start] + key + fmt[end + 1:]
222
+ i += 1
223
+
224
+ # Format mapping
225
+ mapping = {
226
+ 'yyyy': '%Y', # Year
227
+ 'yy': '%y',
228
+ 'MM': '%m', # Month
229
+ 'dd': '%d', # Day
230
+ 'HH': '%H', # Hour
231
+ 'hh': '%I', # Hour (12)
232
+ 'mm': '%M', # Minute
233
+ 'ss': '%S', # Second
234
+ 'SSS': '%f', # Milliseconds
235
+ 'aa': '%p', # AM/PM
236
+ 'A': '%p', # AM/PM
237
+ 'E': '%a', # Weekday abbr
238
+ 'EEE': '%a', # Weekday abbr
239
+ 'EEEE': '%A', # Weekday
240
+ 'MMM': '%b', # Month abbr
241
+ 'MMMM': '%B', # Month name
242
+ 'z': '@', # Timezone name (temp)
243
+ 'Z': '%z', # Timezone
244
+ '@': '%Z', # Timezone name
245
+ }
246
+
247
+ # Sort by length for proper replacement
248
+ patterns = sorted(mapping.keys(), key=len, reverse=True)
249
+
250
+ # Replace patterns
251
+ result = fmt
252
+ for pattern in patterns:
253
+ result = result.replace(pattern, mapping[pattern])
254
+
255
+ # Restore escaped parts
256
+ for key, value in escaped_parts.items():
257
+ result = result.replace(key, value)
258
+
259
+ return result
260
+
261
+
262
+ #
263
+ # Exported functions
264
+ #
265
+
266
+ def contains(source: str | NA[str], str_: str | NA[str]) -> PyneBool:
267
+ """
268
+ Returns true if the source string contains the str substring, false otherwise.
269
+
270
+ :param source: Source string
271
+ :param str_: Substring to search for
272
+ :return: True if the source string contains the str substring, na if either
273
+ argument is na.
274
+ """
275
+ # na-propagation (Pine): a na source/substring yields na, never a search.
276
+ # Without this guard ``str_ in na`` would loop forever, since NA.__getitem__
277
+ # returns self for every index and the ``in`` operator falls back to the
278
+ # sequence protocol.
279
+ if isinstance(source, NA) or source is None or isinstance(str_, NA) or str_ is None:
280
+ return NA(bool)
281
+ return str_ in source
282
+
283
+
284
+ def endswith(source: str, str_: str) -> bool:
285
+ """
286
+ Returns true if the source string ends with the substring specified in str, false otherwise.
287
+
288
+ :param source: Source string
289
+ :param str_: Substring to search for
290
+ :return: True if the source string ends with the substring specified in str, false otherwise.
291
+ """
292
+ return source.endswith(str_)
293
+
294
+
295
+ # noinspection PyPep8Naming,PyShadowingBuiltins
296
+ def format(formatString: str, *args: Any) -> str:
297
+ """
298
+ Converts the formatting string and value(s) into a formatted string.
299
+ Supports:
300
+ - Basic placeholders: {0}, {1}, etc
301
+ - Number formats: {0,number,integer}, {0,number,currency}, {0,number,percent}
302
+ - Custom precision: {0,number,#.#}
303
+ - Pine-style array formatting: [item1, item2] without quotes for strings
304
+ - Special quote handling: single quotes are removed if unpaired, converted to single quote if paired
305
+
306
+ :param formatString: Format pattern
307
+ :param args: Values to format
308
+ :return: Formatted string, or na if the format pattern is na
309
+ """
310
+ # na-propagation (Pine): a na format pattern yields na (e.g. the TV
311
+ # Technical Ratings idiom ``str.repeat("\n", 0) + "{0}"`` — the repeat
312
+ # returns na, the concat propagates it, and str.format passes it through).
313
+ if isinstance(formatString, NA) or formatString is None:
314
+ return NA(str)
315
+
316
+ # Pre-process quotes before handling placeholders
317
+ def process_quotes(s: str) -> str:
318
+ result = []
319
+ i = 0
320
+ while i < len(s):
321
+ if s[i] == "'":
322
+ # Look ahead for another quote
323
+ if i + 1 < len(s) and s[i + 1] == "'":
324
+ result.append("'")
325
+ i += 2 # Skip both quotes
326
+ else:
327
+ # Single quote - remove it
328
+ i += 1
329
+ continue
330
+ result.append(s[i])
331
+ i += 1
332
+ return ''.join(result)
333
+
334
+ formatString = process_quotes(formatString)
335
+
336
+ # noinspection PyShadowingNames
337
+ def replace_field(match: re.Match) -> str:
338
+ field = match.group(1).strip()
339
+ parts = [p.strip() for p in field.split(',')]
340
+
341
+ try:
342
+ index = int(parts[0])
343
+ value = args[index]
344
+ except (ValueError, IndexError):
345
+ raise ValueError(f"Invalid argument index: {parts[0]}")
346
+
347
+ if isinstance(value, NA) or value is None:
348
+ return "NaN"
349
+
350
+ if len(parts) >= 2 and parts[1] == 'number':
351
+ if len(parts) >= 3:
352
+ return _format_number(safe_convert.safe_float(value),
353
+ fmt_type=parts[2] if parts[2] in (
354
+ 'integer', 'currency',
355
+ _format.percent,
356
+ _format.mintick,
357
+ _format.volume,
358
+ _format.price,
359
+ _format.inherit
360
+ ) else '',
361
+ precision=parts[2])
362
+ return _format_number(safe_convert.safe_float(value))
363
+
364
+ return _format_value(value)
365
+
366
+ # Validate and balance curly braces
367
+ stack = []
368
+ in_placeholder = False
369
+ for c in formatString:
370
+ if c == '{' and not in_placeholder:
371
+ stack.append(c)
372
+ in_placeholder = True
373
+ elif c == '}' and in_placeholder:
374
+ if not stack:
375
+ raise ValueError("Unmatched closing brace '}'")
376
+ stack.pop()
377
+ in_placeholder = False
378
+ elif c == '}' and not in_placeholder:
379
+ raise ValueError("Unmatched closing brace '}'")
380
+
381
+ if stack:
382
+ raise ValueError("Unmatched opening brace '{'")
383
+
384
+ return re.sub(r'\{([^}]+)}', replace_field, formatString)
385
+
386
+
387
+ # noinspection PyProtectedMember
388
+ def format_time(time: int | NA[int], fmt: str | None = None,
389
+ timezone: str | None = None) -> str:
390
+ """
391
+ Format timestamp according to format string and timezone
392
+
393
+ :param time: UNIX timestamp in milliseconds
394
+ :param fmt: Format string (Pine format)
395
+ :param timezone: Timezone string (UTC±HHMM, GMT±HHMM or IANA name). Named
396
+ ``timezone`` to match Pine's ``str.format_time(time, format, timezone)``
397
+ keyword argument.
398
+ :return: Formatted time string, or na when ``time`` is na
399
+ """
400
+ # na timestamp formats to na (Pine na-propagation)
401
+ if isinstance(time, NA) or time is None:
402
+ return NA(str)
403
+
404
+ # Default format
405
+ fmt: str = fmt if fmt else "yyyy-MM-ddTHH:mm:ssZ"
406
+
407
+ # Convert timestamp to datetime
408
+ dt = datetime.fromtimestamp(time / 1000, UTC)
409
+
410
+ # Convert timezone using _parse_timezone
411
+ dt = dt.astimezone(_parse_timezone(timezone or _syminfo.timezone))
412
+
413
+ # Convert format and apply
414
+ py_fmt = _datatime_fmt_tv2py(fmt)
415
+ return dt.strftime(py_fmt)
416
+
417
+
418
+ def length(string: str) -> int:
419
+ """
420
+ Returns an integer corresponding to the amount of chars in that string.
421
+
422
+ :param string: String to get the length of
423
+ :return: Amount of chars in the string
424
+ """
425
+ return len(string)
426
+
427
+
428
+ def lower(source: str) -> str:
429
+ """
430
+ Returns a new string with all letters converted to lowercase.
431
+
432
+ :param source: Source string
433
+ :return: A new string with all letters converted to lowercase.
434
+ """
435
+ return source.lower()
436
+
437
+
438
+ # Matches a ``\z`` escape that is a real escape (an even number of preceding
439
+ # backslashes) rather than a literal ``\\z``. Captures the leading backslash
440
+ # pairs so they are preserved in the replacement.
441
+ _PINE_END_ANCHOR_RE = re.compile(r'(?<!\\)((?:\\\\)*)\\z')
442
+
443
+
444
+ def _pine_regex_to_python(regex: str) -> str:
445
+ """
446
+ Adapt a Pine (Java-flavoured) regular expression to Python's ``re`` syntax.
447
+
448
+ Pine's ``str.match`` uses Java regex semantics, which accept ``\\z`` (end of
449
+ input); Python's ``re`` has no ``\\z`` and raises ``PatternError: bad escape``
450
+ for it. Java's ``\\z`` maps to Python's ``\\Z`` (end of string). Only a real
451
+ escape is rewritten — a literal ``\\\\z`` is left untouched.
452
+
453
+ :param regex: The Pine regular expression.
454
+ :return: An equivalent Python regular expression.
455
+ """
456
+ return _PINE_END_ANCHOR_RE.sub(lambda m: m.group(1) + r'\Z', regex)
457
+
458
+
459
+ def match(source: str, regex: str) -> str:
460
+ """
461
+ Returns the new substring of the source string if it matches a regex regular expression, an empty string otherwise.
462
+
463
+ :param source: Source string
464
+ :param regex: Regular expression
465
+ :return: New substring of the source string if it matches a regex regular expression, an empty string otherwise.
466
+ """
467
+ m = re.match(_pine_regex_to_python(regex), source)
468
+ if m is None:
469
+ return ""
470
+ return m.group()
471
+
472
+
473
+ def pos(source: str, str_: str) -> PyneInt:
474
+ """
475
+ Returns the position of the first occurrence of the str string in the source string, 'na' otherwise.
476
+
477
+ :param source: Source string
478
+ :param str_: Subtring to search for
479
+ :return: Position of the first occurrence of the str string in the source string, 'na' otherwise.
480
+ """
481
+ res = source.find(str_)
482
+ if res == -1:
483
+ return NA(int)
484
+ return res
485
+
486
+
487
+ # noinspection PyShadowingNames
488
+ def repeat(source: str, repeat: int, separator: str = '') -> PyneStr:
489
+ """
490
+ Returns a new string consisting of the source string repeated the specified number of times,
491
+ separated by the separator string.
492
+
493
+ :param source: Source string
494
+ :param repeat: Number of times to repeat the source string
495
+ :param separator: Separator string
496
+ :return: New string consisting of the source string repeated the specified number of times,
497
+ separated by the separator string. A na source or repeat count — and a repeat
498
+ count of zero — yields na (TV-verified); a na separator behaves as ''.
499
+ """
500
+ # na-propagation (Pine): without this guard ``[source] * repeat`` with a na
501
+ # count evaluates through ``NA.__rmul__`` to na, and ``str.join(na)`` falls
502
+ # back to the sequence protocol, which never terminates (NA.__getitem__
503
+ # returns self for every index).
504
+ if isinstance(source, NA) or source is None or isinstance(repeat, NA) or repeat is None:
505
+ return NA(str)
506
+ if repeat <= 0:
507
+ return NA(str)
508
+ if isinstance(separator, NA) or separator is None:
509
+ separator = ''
510
+ return separator.join([source] * int(repeat))
511
+
512
+
513
+ def replace(source: str, target: str, replacement: str, occurence=0) -> PyneStr:
514
+ """
515
+ Replaces the nth occurence of target string with the replacement string in the source string.
516
+
517
+ :param source: Source string
518
+ :param target: Target string
519
+ :param replacement: Replacement string
520
+ :param occurence: Occurence to replace
521
+ :return: New string with the nth occurence of target string replaced with the replacement
522
+ string, or na if source or target is na.
523
+ """
524
+ # na-propagation (Pine): a na source would flow through ``source.split``
525
+ # (NA.__getattr__ -> na) into ``target.join(na)``, whose sequence-protocol
526
+ # fallback never terminates — same trap as in ``repeat``/``contains``.
527
+ if isinstance(source, NA) or source is None or isinstance(target, NA) or target is None:
528
+ return NA(str)
529
+ if occurence == 0:
530
+ return source.replace(target, replacement, 1)
531
+ a = source.split(target)
532
+ p1 = target.join(a[:occurence])
533
+ p2 = target.join(a[occurence:])
534
+ if p2 == "":
535
+ return source
536
+ return p1 + replacement + p2
537
+
538
+
539
+ def replace_all(source: str, target: str, replacement: str) -> str:
540
+ """
541
+ Replaces each occurrence of the target string in the source string with the replacement string.
542
+
543
+ :param source: The source string
544
+ :param target: Target string
545
+ :param replacement: Replacement string
546
+ :return: New string with each occurrence of the target string replaced with the replacement string.
547
+ """
548
+ return source.replace(target, replacement)
549
+
550
+
551
+ def split(string: str, separator: str) -> list[str]:
552
+ """
553
+ Divides a string into an array of substrings and returns its array id.
554
+
555
+ :param string: String to split
556
+ :param separator: Separator
557
+ :return: Array of substrings
558
+ """
559
+ # Pine splits into individual characters when the separator is empty,
560
+ # whereas Python's str.split("") raises "empty separator".
561
+ if separator == "":
562
+ return list(string)
563
+ return string.split(separator)
564
+
565
+
566
+ def startswith(source: str, str_: str) -> bool:
567
+ """
568
+ Returns true if the source string starts with the str substring, false otherwise.
569
+
570
+ :param source: The source string
571
+ :param str_: The substring to search for
572
+ :return: True if the source string starts with the str substring, false otherwise
573
+ """
574
+ return source.startswith(str_)
575
+
576
+
577
+ def substring(source: str, begin_pos: int, end_pos: int | None = None) -> str:
578
+ """
579
+ Returns a substring of the source string starting at the specified position and ending at the specified position.
580
+
581
+ :param source: The source string
582
+ :param begin_pos: The starting position
583
+ :param end_pos: The ending position
584
+ :return: The substring of the source string starting at the specified position and ending at the specified position
585
+ """
586
+ assert begin_pos >= 0, "Positions must be >= 0!"
587
+ if end_pos is not None:
588
+ assert end_pos >= begin_pos, "End position must be >= begin position!"
589
+ if begin_pos == end_pos:
590
+ return ""
591
+ if end_pos is None:
592
+ end_pos = len(source)
593
+ return source[begin_pos:end_pos]
594
+
595
+
596
+ def tonumber(string: str) -> PyneFloat:
597
+ """
598
+ Converts a value represented in string to its "float" equivalent, or `na` if the conversion is not possible.
599
+
600
+ :param string: Value to convert
601
+ :return: Float equivalent of the value or `na` if the conversion is not possible.
602
+ """
603
+ try:
604
+ return float(string)
605
+ except ValueError:
606
+ return na_float
607
+
608
+
609
+ # noinspection PyShadowingBuiltins,PyShadowingNames
610
+ def tostring(value: int | float | str | bool | NA, format: str | Format = '#.##########') -> str:
611
+ """
612
+ Convert value to string with optional formatting.
613
+ Replicates Pine's str.tostring function.
614
+
615
+ :param value: Value to convert (number, string, boolean or na)
616
+ :param format: Format string like '#.##' or Format instance
617
+ :return: String representation
618
+ """
619
+ if isinstance(value, NA) or value is None:
620
+ return "NaN"
621
+ if isinstance(value, bool):
622
+ return str(value).lower()
623
+ if isinstance(value, str):
624
+ return value
625
+ if isinstance(value, (int, float)):
626
+ if isinstance(format, Format):
627
+ return _format_number(safe_convert.safe_float(value), fmt_type=format)
628
+ return _format_number(safe_convert.safe_float(value), precision=format)
629
+ return str(value) # noqa: it may be reachable if it is used with unsupported types
630
+
631
+
632
+ def trim(source: str) -> str:
633
+ """
634
+ Removes leading and trailing whitespaces from the source string.
635
+
636
+ :param source: Source string
637
+ :return: Source string without leading and trailing whitespaces.
638
+ """
639
+ return source.strip()
640
+
641
+
642
+ def upper(source: str) -> str:
643
+ """
644
+ Returns a new string with all letters converted to uppercase.
645
+
646
+ :param source: Source string
647
+ :return: A new string with all letters converted to uppercase.
648
+ """
649
+ return source.upper()