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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- 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()
|