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/__init__.py
ADDED
|
@@ -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
|