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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global workdir-level symbol map for ``request.security()`` resolution.
|
|
3
|
+
|
|
4
|
+
Scripts use TradingView-canonical symbols (``NASDAQ:AAPL``); the user's own
|
|
5
|
+
data files carry provider-native symbols (``capitalcom:AAPL``, or a
|
|
6
|
+
multi-broker ``ccxt:BYBIT:BTC/USDT:USDT``). The global map
|
|
7
|
+
(``<workdir>/config/symbol_map.toml``) translates one into the other so both
|
|
8
|
+
backtest (file resolution) and live (``PluginSymbol`` construction) can find
|
|
9
|
+
the right data without an explicit ``--security`` mapping.
|
|
10
|
+
|
|
11
|
+
File schema::
|
|
12
|
+
|
|
13
|
+
[symbol_map]
|
|
14
|
+
"BINANCE:BTCUSDT" = "ccxt:BYBIT:BTC/USDT:USDT"
|
|
15
|
+
"NASDAQ:AAPL" = "capitalcom:AAPL"
|
|
16
|
+
"NASDAQ:AAPL:60" = "capitalcom:AAPL" # optional per-timeframe override
|
|
17
|
+
|
|
18
|
+
The KEY is the TradingView-style ``PREFIX:TICKER`` written in the script, with
|
|
19
|
+
an optional ``:TF`` suffix for a per-timeframe override. The VALUE is a
|
|
20
|
+
provider-qualified NATIVE symbol string in the same format as the ``[download]``
|
|
21
|
+
provider string (``provider:rest``, where ``rest`` may itself contain colons).
|
|
22
|
+
It is NOT a filename — the backtest ``.ohlcv`` path is derived deterministically
|
|
23
|
+
via :meth:`ProviderPlugin.get_ohlcv_path`.
|
|
24
|
+
|
|
25
|
+
A missing or malformed file yields an empty map (never crashes a run); parse
|
|
26
|
+
errors are logged as warnings.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import tomllib
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
__all__ = ['MappedSymbol', 'SymbolMap', 'SYMBOL_MAP_FILENAME']
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
#: Name of the global symbol map file inside ``<workdir>/config``.
|
|
41
|
+
SYMBOL_MAP_FILENAME = 'symbol_map.toml'
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class MappedSymbol:
|
|
46
|
+
"""A resolved global-map entry: a provider-qualified native symbol.
|
|
47
|
+
|
|
48
|
+
:ivar provider: Plugin entry-point name (e.g. ``"ccxt"``, ``"capitalcom"``).
|
|
49
|
+
:ivar native_symbol: The provider-native symbol (the ``rest`` of the
|
|
50
|
+
``provider:rest`` value; may itself contain further colons, e.g.
|
|
51
|
+
``"BYBIT:BTC/USDT:USDT"`` for a multi-broker provider).
|
|
52
|
+
"""
|
|
53
|
+
provider: str
|
|
54
|
+
native_symbol: str
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def parse(cls, value: str) -> 'MappedSymbol | None':
|
|
58
|
+
"""Parse a ``"provider:rest"`` value into a :class:`MappedSymbol`.
|
|
59
|
+
|
|
60
|
+
:param value: The provider-qualified native symbol string.
|
|
61
|
+
:return: The parsed :class:`MappedSymbol`, or ``None`` when the value
|
|
62
|
+
is malformed (no colon, empty provider, or empty native symbol).
|
|
63
|
+
"""
|
|
64
|
+
provider, sep, rest = value.partition(':')
|
|
65
|
+
if not sep or not provider or not rest:
|
|
66
|
+
return None
|
|
67
|
+
return cls(provider=provider, native_symbol=rest)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class SymbolMap:
|
|
72
|
+
"""The parsed ``[symbol_map]`` table of ``config/symbol_map.toml``."""
|
|
73
|
+
|
|
74
|
+
entries: dict[str, MappedSymbol] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def load(cls, config_dir: 'Path | str | None') -> 'SymbolMap':
|
|
78
|
+
"""Load the global symbol map from ``config_dir/symbol_map.toml``.
|
|
79
|
+
|
|
80
|
+
A missing file, an unreadable file, or a malformed ``[symbol_map]``
|
|
81
|
+
table degrades to an empty map (a run is never crashed by the map).
|
|
82
|
+
Parse errors and individual malformed entries are logged as warnings.
|
|
83
|
+
|
|
84
|
+
:param config_dir: The workdir ``config`` directory, or ``None``.
|
|
85
|
+
:return: A :class:`SymbolMap` (empty when nothing could be loaded).
|
|
86
|
+
"""
|
|
87
|
+
if config_dir is None:
|
|
88
|
+
return cls()
|
|
89
|
+
path = Path(config_dir) / SYMBOL_MAP_FILENAME
|
|
90
|
+
if not path.is_file():
|
|
91
|
+
return cls()
|
|
92
|
+
try:
|
|
93
|
+
with open(path, 'rb') as f:
|
|
94
|
+
data = tomllib.load(f)
|
|
95
|
+
except (OSError, tomllib.TOMLDecodeError) as e:
|
|
96
|
+
logger.warning("Could not read symbol map %s: %s", path, e)
|
|
97
|
+
return cls()
|
|
98
|
+
table = data.get('symbol_map')
|
|
99
|
+
if not isinstance(table, dict):
|
|
100
|
+
if table is not None:
|
|
101
|
+
logger.warning("Ignoring [symbol_map] in %s: expected a table", path)
|
|
102
|
+
return cls()
|
|
103
|
+
entries: dict[str, MappedSymbol] = {}
|
|
104
|
+
for key, value in table.items():
|
|
105
|
+
if not isinstance(value, str):
|
|
106
|
+
logger.warning("Ignoring non-string symbol_map entry %r in %s", key, path)
|
|
107
|
+
continue
|
|
108
|
+
mapped = MappedSymbol.parse(value)
|
|
109
|
+
if mapped is None:
|
|
110
|
+
logger.warning(
|
|
111
|
+
"Ignoring malformed symbol_map entry %r = %r in %s "
|
|
112
|
+
"(expected 'provider:native_symbol')", key, value, path)
|
|
113
|
+
continue
|
|
114
|
+
entries[key] = mapped
|
|
115
|
+
return cls(entries=entries)
|
|
116
|
+
|
|
117
|
+
def resolve(self, symbol: str, timeframe: 'str | None' = None) -> 'MappedSymbol | None':
|
|
118
|
+
"""Resolve a TradingView-style symbol to a provider-native mapping.
|
|
119
|
+
|
|
120
|
+
Key precedence mirrors ``_resolve_security_data``: a ``"SYMBOL:TF"``
|
|
121
|
+
per-timeframe override wins over a bare ``"SYMBOL"`` entry.
|
|
122
|
+
|
|
123
|
+
:param symbol: The TradingView-style ``PREFIX:TICKER`` from the script.
|
|
124
|
+
:param timeframe: The security timeframe (used for the ``:TF`` override).
|
|
125
|
+
:return: The :class:`MappedSymbol`, or ``None`` when unmapped.
|
|
126
|
+
"""
|
|
127
|
+
if timeframe:
|
|
128
|
+
tf_hit = self.entries.get(f"{symbol}:{timeframe}")
|
|
129
|
+
if tf_hit is not None:
|
|
130
|
+
return tf_hit
|
|
131
|
+
return self.entries.get(symbol)
|
|
132
|
+
|
|
133
|
+
def __bool__(self) -> bool:
|
|
134
|
+
return bool(self.entries)
|
pynecore/core/syminfo.py
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
from typing import Literal, NamedTuple, Self
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import time, date, datetime
|
|
5
|
+
|
|
6
|
+
SymInfoInterval = NamedTuple("SymInfoInterval", [('day', int), ('start', time), ('end', time)])
|
|
7
|
+
SymInfoSession = NamedTuple("SymInfoSession", [('day', int), ('time', time)])
|
|
8
|
+
SymInfoScheduleVariant = NamedTuple("SymInfoScheduleVariant", [
|
|
9
|
+
('effective_from', date),
|
|
10
|
+
('opening_hours', list[SymInfoInterval]),
|
|
11
|
+
('session_starts', list[SymInfoSession]),
|
|
12
|
+
('session_ends', list[SymInfoSession]),
|
|
13
|
+
])
|
|
14
|
+
"""One era of a symbol's trading schedule, in effect from ``effective_from``
|
|
15
|
+
(an exchange-local trading-day date) until the next variant's date. See
|
|
16
|
+
:attr:`SymInfo.session_schedules`."""
|
|
17
|
+
|
|
18
|
+
# Commented TOML appended by ``save_toml`` when a symbol has no schedule history,
|
|
19
|
+
# so anyone opening the file sees how to add one. Valid TOML if uncommented.
|
|
20
|
+
_SESSION_SCHEDULE_EXAMPLE_COMMENT = """\
|
|
21
|
+
# Effective-dated session history (optional).
|
|
22
|
+
#
|
|
23
|
+
# Markets occasionally change their trading hours. A single static schedule then
|
|
24
|
+
# mis-confirms the part of a backtest that falls on the other side of the change.
|
|
25
|
+
# Add [[session_schedules]] blocks below -- oldest first, each self-contained --
|
|
26
|
+
# to describe that history. `effective_from` MUST be the first line of its block
|
|
27
|
+
# (an exchange-local trading-day date). A bar whose trading day is on or after a
|
|
28
|
+
# block's date uses that block; a date before the earliest block uses the
|
|
29
|
+
# earliest block. The flat opening_hours / session_starts / session_ends above
|
|
30
|
+
# are regenerated from the NEWEST variant on save, so edit the variants here, not
|
|
31
|
+
# the flat blocks.
|
|
32
|
+
#
|
|
33
|
+
# Example -- a market whose night session END moved 23:30 -> 23:00 on 2026-01-12:
|
|
34
|
+
#
|
|
35
|
+
# [[session_schedules]]
|
|
36
|
+
# effective_from = 2025-06-01
|
|
37
|
+
# [[session_schedules.opening_hours]]
|
|
38
|
+
# day = 0
|
|
39
|
+
# start = "10:00:00"
|
|
40
|
+
# end = "18:00:00"
|
|
41
|
+
# [[session_schedules.opening_hours]]
|
|
42
|
+
# day = 0
|
|
43
|
+
# start = "21:00:00"
|
|
44
|
+
# end = "23:30:00"
|
|
45
|
+
# [[session_schedules.session_starts]]
|
|
46
|
+
# day = 0
|
|
47
|
+
# time = "10:00:00"
|
|
48
|
+
# [[session_schedules.session_ends]]
|
|
49
|
+
# day = 0
|
|
50
|
+
# time = "23:30:00"
|
|
51
|
+
#
|
|
52
|
+
# [[session_schedules]]
|
|
53
|
+
# effective_from = 2026-01-12
|
|
54
|
+
# [[session_schedules.opening_hours]]
|
|
55
|
+
# day = 0
|
|
56
|
+
# start = "10:00:00"
|
|
57
|
+
# end = "18:00:00"
|
|
58
|
+
# [[session_schedules.opening_hours]]
|
|
59
|
+
# day = 0
|
|
60
|
+
# start = "21:00:00"
|
|
61
|
+
# end = "23:00:00"
|
|
62
|
+
# [[session_schedules.session_starts]]
|
|
63
|
+
# day = 0
|
|
64
|
+
# time = "10:00:00"
|
|
65
|
+
# [[session_schedules.session_ends]]
|
|
66
|
+
# day = 0
|
|
67
|
+
# time = "23:00:00\""""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def default_mincontract(sym_type: str, basecurrency: str | None = None) -> float:
|
|
71
|
+
"""
|
|
72
|
+
Heuristic minimum order quantity step for symbols whose data source does
|
|
73
|
+
not expose one.
|
|
74
|
+
|
|
75
|
+
Crypto exchanges quote fractional lot steps (BTC pairs commonly 1e-5,
|
|
76
|
+
other coins 1e-4); everything else trades in whole contracts. This is the
|
|
77
|
+
last resort of the ``mincontract`` resolution chain (exchange value ->
|
|
78
|
+
volume-data analysis -> heuristic), and it also fills the gap when
|
|
79
|
+
loading symbol info saved before ``mincontract`` existed.
|
|
80
|
+
|
|
81
|
+
:param sym_type: The symbol type (``SymInfo.type``).
|
|
82
|
+
:param basecurrency: The symbol's base currency, if known.
|
|
83
|
+
:return: The estimated minimum quantity step.
|
|
84
|
+
"""
|
|
85
|
+
if sym_type == 'crypto':
|
|
86
|
+
return 1e-05 if basecurrency == 'BTC' else 1e-04
|
|
87
|
+
return 1.0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(kw_only=True, slots=True)
|
|
91
|
+
class SymInfo:
|
|
92
|
+
"""
|
|
93
|
+
Symbol information dataclass
|
|
94
|
+
|
|
95
|
+
It is stored in TOML format in the working directory. It is initially from the provider, but
|
|
96
|
+
users can edit according to their needs to make it compatible with the TradingView platform.
|
|
97
|
+
It is almost impossible to make providers fully compatible, this is why users may need to
|
|
98
|
+
edit the symbol information in very specific cases.
|
|
99
|
+
"""
|
|
100
|
+
prefix: str
|
|
101
|
+
description: str
|
|
102
|
+
ticker: str
|
|
103
|
+
currency: str
|
|
104
|
+
basecurrency: str | None = None
|
|
105
|
+
period: str
|
|
106
|
+
type: Literal[
|
|
107
|
+
"stock", "fund", "dr", "right", "bond", "warrant", "structured", "index", "forex",
|
|
108
|
+
"futures", "spread", "economic", "fundamental", "crypto", "spot", "swap", "option",
|
|
109
|
+
"commodity", "other"
|
|
110
|
+
]
|
|
111
|
+
volumetype: Literal["base", "quote", "tick", "n/a"] = 'base'
|
|
112
|
+
mintick: float
|
|
113
|
+
pricescale: int
|
|
114
|
+
minmove: int = 1
|
|
115
|
+
pointvalue: float
|
|
116
|
+
mincontract: float
|
|
117
|
+
"""Minimum order quantity step (Pine ``syminfo.mincontract``); order sizes
|
|
118
|
+
are truncated to this grid. Always positive: filled from the exchange when
|
|
119
|
+
available, otherwise estimated from volume data or the
|
|
120
|
+
:func:`default_mincontract` heuristic."""
|
|
121
|
+
opening_hours: list[SymInfoInterval]
|
|
122
|
+
session_starts: list[SymInfoSession]
|
|
123
|
+
session_ends: list[SymInfoSession]
|
|
124
|
+
session_schedules: list[SymInfoScheduleVariant] = field(default_factory=list)
|
|
125
|
+
"""Optional effective-dated session history (oldest first). When non-empty the
|
|
126
|
+
flat ``opening_hours`` / ``session_starts`` / ``session_ends`` mirror the
|
|
127
|
+
newest variant; consumers that need a schedule for a specific date resolve it
|
|
128
|
+
via :meth:`schedule_for` / :meth:`schedule_index_for`. Empty for the common
|
|
129
|
+
case of a symbol whose hours never changed."""
|
|
130
|
+
timezone: str = 'UTC'
|
|
131
|
+
|
|
132
|
+
avg_spread: float | None = None
|
|
133
|
+
taker_fee: float | None = None
|
|
134
|
+
maker_fee: float | None = None
|
|
135
|
+
|
|
136
|
+
# Reference data (None when the data source does not expose it)
|
|
137
|
+
country: str | None = None
|
|
138
|
+
sector: str | None = None
|
|
139
|
+
industry: str | None = None
|
|
140
|
+
isin: str | None = None
|
|
141
|
+
|
|
142
|
+
# Futures contract information
|
|
143
|
+
expiration_date: int | None = None # UNIX timestamp
|
|
144
|
+
current_contract: str | None = None
|
|
145
|
+
|
|
146
|
+
# Fundamentals (stocks only, None elsewhere)
|
|
147
|
+
employees: int | None = None
|
|
148
|
+
shareholders: int | None = None
|
|
149
|
+
shares_outstanding_total: float | None = None
|
|
150
|
+
shares_outstanding_float: float | None = None
|
|
151
|
+
|
|
152
|
+
# Analyst recommendation counts
|
|
153
|
+
recommendations_buy: int | None = None
|
|
154
|
+
recommendations_buy_strong: int | None = None
|
|
155
|
+
recommendations_date: int | None = None # UNIX timestamp
|
|
156
|
+
recommendations_hold: int | None = None
|
|
157
|
+
recommendations_sell: int | None = None
|
|
158
|
+
recommendations_sell_strong: int | None = None
|
|
159
|
+
recommendations_total: int | None = None
|
|
160
|
+
|
|
161
|
+
# Analyst price target information (added 2025-07-08)
|
|
162
|
+
target_price_average: float | None = None
|
|
163
|
+
target_price_high: float | None = None
|
|
164
|
+
target_price_low: float | None = None
|
|
165
|
+
target_price_median: float | None = None
|
|
166
|
+
target_price_date: int | None = None # UNIX timestamp
|
|
167
|
+
target_price_estimates: int | None = None
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def load_toml(cls, path: Path) -> Self:
|
|
171
|
+
"""
|
|
172
|
+
Load SymInfo object from TOML file.
|
|
173
|
+
|
|
174
|
+
:param path: Path to the TOML file
|
|
175
|
+
:return: SymInfo instance
|
|
176
|
+
:raises ValueError: If required fields are missing or invalid
|
|
177
|
+
"""
|
|
178
|
+
import tomllib
|
|
179
|
+
|
|
180
|
+
with open(path, 'rb') as f:
|
|
181
|
+
data = tomllib.load(f)
|
|
182
|
+
|
|
183
|
+
if 'symbol' not in data:
|
|
184
|
+
raise ValueError("Missing [symbol] section in TOML")
|
|
185
|
+
|
|
186
|
+
symbol = data['symbol']
|
|
187
|
+
|
|
188
|
+
# Parse time strings in arrays
|
|
189
|
+
# noinspection PyShadowingNames
|
|
190
|
+
def parse_time(time_str: str) -> time:
|
|
191
|
+
"""Parse time string in HH:MM:SS fmt"""
|
|
192
|
+
h, m, s = map(int, time_str.split(':'))
|
|
193
|
+
return time(h, m, s)
|
|
194
|
+
|
|
195
|
+
# Convert opening hours
|
|
196
|
+
opening_hours = []
|
|
197
|
+
for oh in data.get('opening_hours', []):
|
|
198
|
+
opening_hours.append(SymInfoInterval(
|
|
199
|
+
day=oh['day'],
|
|
200
|
+
start=parse_time(oh['start']),
|
|
201
|
+
end=parse_time(oh['end'])
|
|
202
|
+
))
|
|
203
|
+
|
|
204
|
+
# Convert session times
|
|
205
|
+
session_starts = []
|
|
206
|
+
for s in data.get('session_starts', []):
|
|
207
|
+
session_starts.append(SymInfoSession(
|
|
208
|
+
day=s['day'],
|
|
209
|
+
time=parse_time(s['time'])
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
session_ends = []
|
|
213
|
+
for s in data.get('session_ends', []):
|
|
214
|
+
session_ends.append(SymInfoSession(
|
|
215
|
+
day=s['day'],
|
|
216
|
+
time=parse_time(s['time'])
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
# Effective-dated session history (optional). Each variant is a
|
|
220
|
+
# self-contained schedule taking effect on its ``effective_from``
|
|
221
|
+
# exchange-local trading day. The list is sorted ascending and, when
|
|
222
|
+
# present, overwrites the flat fields above with the NEWEST variant so
|
|
223
|
+
# setup-time classification and the live "now" both see today's calendar.
|
|
224
|
+
# noinspection PyShadowingNames
|
|
225
|
+
def parse_effective_from(value: object) -> date:
|
|
226
|
+
"""Normalize a TOML local-date / local-datetime / ISO string to a date."""
|
|
227
|
+
if isinstance(value, datetime): # datetime first: it subclasses date
|
|
228
|
+
return value.date()
|
|
229
|
+
if isinstance(value, date):
|
|
230
|
+
return value
|
|
231
|
+
if isinstance(value, str):
|
|
232
|
+
return date.fromisoformat(value.replace(' ', 'T').split('T')[0])
|
|
233
|
+
raise ValueError("Invalid session_schedules effective_from type "
|
|
234
|
+
f"{type(value).__name__}: expected a date or YYYY-MM-DD string")
|
|
235
|
+
|
|
236
|
+
session_schedules: list[SymInfoScheduleVariant] = []
|
|
237
|
+
seen_effective: set[date] = set()
|
|
238
|
+
for sched in data.get('session_schedules', []):
|
|
239
|
+
if 'effective_from' not in sched:
|
|
240
|
+
raise ValueError("session_schedules entry is missing 'effective_from'")
|
|
241
|
+
eff = parse_effective_from(sched['effective_from'])
|
|
242
|
+
if eff in seen_effective:
|
|
243
|
+
raise ValueError(f"Duplicate session_schedules effective_from: {eff}")
|
|
244
|
+
seen_effective.add(eff)
|
|
245
|
+
session_schedules.append(SymInfoScheduleVariant(
|
|
246
|
+
effective_from=eff,
|
|
247
|
+
opening_hours=[SymInfoInterval(day=oh['day'], start=parse_time(oh['start']),
|
|
248
|
+
end=parse_time(oh['end']))
|
|
249
|
+
for oh in sched.get('opening_hours', [])],
|
|
250
|
+
session_starts=[SymInfoSession(day=s['day'], time=parse_time(s['time']))
|
|
251
|
+
for s in sched.get('session_starts', [])],
|
|
252
|
+
session_ends=[SymInfoSession(day=s['day'], time=parse_time(s['time']))
|
|
253
|
+
for s in sched.get('session_ends', [])],
|
|
254
|
+
))
|
|
255
|
+
session_schedules.sort(key=lambda v: v.effective_from)
|
|
256
|
+
if session_schedules:
|
|
257
|
+
# History is the source of truth: the flat blocks mirror the newest.
|
|
258
|
+
newest = session_schedules[-1]
|
|
259
|
+
opening_hours = newest.opening_hours
|
|
260
|
+
session_starts = newest.session_starts
|
|
261
|
+
session_ends = newest.session_ends
|
|
262
|
+
|
|
263
|
+
# Create instance with all fields
|
|
264
|
+
return cls(
|
|
265
|
+
prefix=symbol['prefix'],
|
|
266
|
+
description=symbol['description'],
|
|
267
|
+
ticker=symbol['ticker'],
|
|
268
|
+
currency=symbol['currency'],
|
|
269
|
+
basecurrency=symbol['basecurrency'] if 'basecurrency' in symbol else None,
|
|
270
|
+
period=symbol['period'],
|
|
271
|
+
type=symbol['type'],
|
|
272
|
+
mintick=symbol['mintick'],
|
|
273
|
+
pricescale=symbol['pricescale'],
|
|
274
|
+
minmove=symbol.get('minmove', 1),
|
|
275
|
+
pointvalue=symbol['pointvalue'],
|
|
276
|
+
# Files saved before mincontract existed fall back to the heuristic
|
|
277
|
+
mincontract=(float(symbol.get('mincontract', 0.0))
|
|
278
|
+
or default_mincontract(symbol['type'], symbol.get('basecurrency'))),
|
|
279
|
+
opening_hours=opening_hours,
|
|
280
|
+
session_starts=session_starts,
|
|
281
|
+
session_ends=session_ends,
|
|
282
|
+
session_schedules=session_schedules,
|
|
283
|
+
timezone=symbol.get('timezone', 'UTC'),
|
|
284
|
+
volumetype=symbol.get('volumetype', 'base'),
|
|
285
|
+
avg_spread=symbol.get('avg_spread'),
|
|
286
|
+
taker_fee=symbol.get('taker_fee'),
|
|
287
|
+
maker_fee=symbol.get('maker_fee'),
|
|
288
|
+
country=symbol.get('country'),
|
|
289
|
+
sector=symbol.get('sector'),
|
|
290
|
+
industry=symbol.get('industry'),
|
|
291
|
+
isin=symbol.get('isin'),
|
|
292
|
+
expiration_date=symbol.get('expiration_date'),
|
|
293
|
+
current_contract=symbol.get('current_contract'),
|
|
294
|
+
employees=symbol.get('employees'),
|
|
295
|
+
shareholders=symbol.get('shareholders'),
|
|
296
|
+
shares_outstanding_total=symbol.get('shares_outstanding_total'),
|
|
297
|
+
shares_outstanding_float=symbol.get('shares_outstanding_float'),
|
|
298
|
+
recommendations_buy=symbol.get('recommendations_buy'),
|
|
299
|
+
recommendations_buy_strong=symbol.get('recommendations_buy_strong'),
|
|
300
|
+
recommendations_date=symbol.get('recommendations_date'),
|
|
301
|
+
recommendations_hold=symbol.get('recommendations_hold'),
|
|
302
|
+
recommendations_sell=symbol.get('recommendations_sell'),
|
|
303
|
+
recommendations_sell_strong=symbol.get('recommendations_sell_strong'),
|
|
304
|
+
recommendations_total=symbol.get('recommendations_total'),
|
|
305
|
+
target_price_average=symbol.get('target_price_average'),
|
|
306
|
+
target_price_high=symbol.get('target_price_high'),
|
|
307
|
+
target_price_low=symbol.get('target_price_low'),
|
|
308
|
+
target_price_median=symbol.get('target_price_median'),
|
|
309
|
+
target_price_date=symbol.get('target_price_date'),
|
|
310
|
+
target_price_estimates=symbol.get('target_price_estimates')
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def save_toml(self, path: Path):
|
|
314
|
+
"""
|
|
315
|
+
Save SymInfo object to TOML-like fmt without dependencies.
|
|
316
|
+
Organizes data under [symbol] section.
|
|
317
|
+
None values are commented out with '#key ='
|
|
318
|
+
|
|
319
|
+
An existing [download] section (written by `pyne data download`, see
|
|
320
|
+
core.download_info) is preserved verbatim across the rewrite.
|
|
321
|
+
|
|
322
|
+
:param path: Path to save the file
|
|
323
|
+
"""
|
|
324
|
+
from .download_info import extract_download_section
|
|
325
|
+
|
|
326
|
+
preserved_download = None
|
|
327
|
+
if path.exists():
|
|
328
|
+
try:
|
|
329
|
+
preserved_download = extract_download_section(path.read_text(encoding='utf-8'))
|
|
330
|
+
except OSError:
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
def time_to_str(t):
|
|
334
|
+
"""Convert time object to string"""
|
|
335
|
+
return t.strftime("%H:%M:%S")
|
|
336
|
+
|
|
337
|
+
# noinspection PyShadowingNames
|
|
338
|
+
def format_field(key, value):
|
|
339
|
+
"""Format field to TOML string"""
|
|
340
|
+
if value is None:
|
|
341
|
+
return f"#{key} ="
|
|
342
|
+
if isinstance(value, str):
|
|
343
|
+
return f"{key} = \"{value}\""
|
|
344
|
+
if isinstance(value, bool):
|
|
345
|
+
return f"{key} = {str(value).lower()}"
|
|
346
|
+
if isinstance(value, float):
|
|
347
|
+
return f"{key} = {value:.8f}"
|
|
348
|
+
return f"{key} = {value}"
|
|
349
|
+
|
|
350
|
+
lines = ["[symbol]"] # Root table/section
|
|
351
|
+
|
|
352
|
+
# Basic fields
|
|
353
|
+
for key in ['prefix', 'description', 'ticker', 'currency', 'basecurrency',
|
|
354
|
+
'period', 'type', 'mintick', 'pricescale', 'minmove', 'pointvalue',
|
|
355
|
+
'mincontract', 'timezone', 'volumetype', 'avg_spread', 'taker_fee', 'maker_fee',
|
|
356
|
+
'country', 'sector', 'industry', 'isin',
|
|
357
|
+
'expiration_date', 'current_contract',
|
|
358
|
+
'employees', 'shareholders',
|
|
359
|
+
'shares_outstanding_total', 'shares_outstanding_float',
|
|
360
|
+
'recommendations_buy', 'recommendations_buy_strong', 'recommendations_date',
|
|
361
|
+
'recommendations_hold', 'recommendations_sell', 'recommendations_sell_strong',
|
|
362
|
+
'recommendations_total',
|
|
363
|
+
'target_price_average', 'target_price_high', 'target_price_low',
|
|
364
|
+
'target_price_median', 'target_price_date', 'target_price_estimates']:
|
|
365
|
+
lines.append(format_field(key, getattr(self, key)))
|
|
366
|
+
|
|
367
|
+
# Arrays of tables. With an effective-dated history the flat blocks mirror
|
|
368
|
+
# the NEWEST variant (the load-time invariant); regenerate them from it on
|
|
369
|
+
# save so a programmatic edit that left ``self.*`` stale cannot leak the old
|
|
370
|
+
# schedule into the flat block -- this is what the comment below promises.
|
|
371
|
+
if self.session_schedules:
|
|
372
|
+
newest = self.session_schedules[-1]
|
|
373
|
+
flat_opening_hours = newest.opening_hours
|
|
374
|
+
flat_session_starts = newest.session_starts
|
|
375
|
+
flat_session_ends = newest.session_ends
|
|
376
|
+
else:
|
|
377
|
+
flat_opening_hours = self.opening_hours
|
|
378
|
+
flat_session_starts = self.session_starts
|
|
379
|
+
flat_session_ends = self.session_ends
|
|
380
|
+
|
|
381
|
+
lines.append("\n# Opening hours")
|
|
382
|
+
for oh in flat_opening_hours:
|
|
383
|
+
lines.append("[[opening_hours]]")
|
|
384
|
+
lines.append(f"day = {oh.day}")
|
|
385
|
+
lines.append(f'start = "{time_to_str(oh.start)}"')
|
|
386
|
+
lines.append(f'end = "{time_to_str(oh.end)}"')
|
|
387
|
+
lines.append("")
|
|
388
|
+
|
|
389
|
+
lines.append("# Session starts")
|
|
390
|
+
for s in flat_session_starts:
|
|
391
|
+
lines.append("[[session_starts]]")
|
|
392
|
+
lines.append(f"day = {s.day}")
|
|
393
|
+
lines.append(f'time = "{time_to_str(s.time)}"')
|
|
394
|
+
lines.append("")
|
|
395
|
+
|
|
396
|
+
lines.append("# Session ends")
|
|
397
|
+
for s in flat_session_ends:
|
|
398
|
+
lines.append("[[session_ends]]")
|
|
399
|
+
lines.append(f"day = {s.day}")
|
|
400
|
+
lines.append(f'time = "{time_to_str(s.time)}"')
|
|
401
|
+
lines.append("")
|
|
402
|
+
|
|
403
|
+
# Effective-dated session history, or a commented example when absent.
|
|
404
|
+
if self.session_schedules:
|
|
405
|
+
lines.append("# Effective-dated session history (oldest first). The flat")
|
|
406
|
+
lines.append("# opening_hours / session_starts / session_ends above are regenerated")
|
|
407
|
+
lines.append("# from the newest variant on save -- edit the variants below.")
|
|
408
|
+
for variant in self.session_schedules:
|
|
409
|
+
lines.append("[[session_schedules]]")
|
|
410
|
+
lines.append(f"effective_from = {variant.effective_from.isoformat()}")
|
|
411
|
+
for oh in variant.opening_hours:
|
|
412
|
+
lines.append("[[session_schedules.opening_hours]]")
|
|
413
|
+
lines.append(f"day = {oh.day}")
|
|
414
|
+
lines.append(f'start = "{time_to_str(oh.start)}"')
|
|
415
|
+
lines.append(f'end = "{time_to_str(oh.end)}"')
|
|
416
|
+
for s in variant.session_starts:
|
|
417
|
+
lines.append("[[session_schedules.session_starts]]")
|
|
418
|
+
lines.append(f"day = {s.day}")
|
|
419
|
+
lines.append(f'time = "{time_to_str(s.time)}"')
|
|
420
|
+
for s in variant.session_ends:
|
|
421
|
+
lines.append("[[session_schedules.session_ends]]")
|
|
422
|
+
lines.append(f"day = {s.day}")
|
|
423
|
+
lines.append(f'time = "{time_to_str(s.time)}"')
|
|
424
|
+
lines.append("")
|
|
425
|
+
else:
|
|
426
|
+
lines.append(_SESSION_SCHEDULE_EXAMPLE_COMMENT)
|
|
427
|
+
lines.append("")
|
|
428
|
+
|
|
429
|
+
if preserved_download:
|
|
430
|
+
lines.append(preserved_download)
|
|
431
|
+
lines.append("")
|
|
432
|
+
|
|
433
|
+
# Write to file
|
|
434
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
435
|
+
f.write('\n'.join(lines))
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def has_schedule_history(self) -> bool:
|
|
439
|
+
"""``True`` when an effective-dated session history is present."""
|
|
440
|
+
return bool(self.session_schedules)
|
|
441
|
+
|
|
442
|
+
def schedule_index_for(self, d: date) -> int:
|
|
443
|
+
"""
|
|
444
|
+
Index into :attr:`session_schedules` of the variant effective on ``d``.
|
|
445
|
+
|
|
446
|
+
The chain is sorted ascending by ``effective_from`` at load time, so this
|
|
447
|
+
is the last variant with ``effective_from <= d``. A date before the first
|
|
448
|
+
variant clamps to index ``0`` (the oldest variant), never to the flat
|
|
449
|
+
fields -- those mirror the NEWEST schedule and would reintroduce the stale
|
|
450
|
+
divergence. Requires a non-empty :attr:`session_schedules`.
|
|
451
|
+
|
|
452
|
+
:param d: Exchange-local trading-day date.
|
|
453
|
+
:return: Index into :attr:`session_schedules`.
|
|
454
|
+
"""
|
|
455
|
+
idx = 0
|
|
456
|
+
for i, variant in enumerate(self.session_schedules):
|
|
457
|
+
if variant.effective_from <= d:
|
|
458
|
+
idx = i
|
|
459
|
+
else:
|
|
460
|
+
break
|
|
461
|
+
return idx
|
|
462
|
+
|
|
463
|
+
def schedule_for(self, d: date) -> tuple[
|
|
464
|
+
list[SymInfoInterval], list[SymInfoSession], list[SymInfoSession]]:
|
|
465
|
+
"""
|
|
466
|
+
Resolve the session schedule effective on date ``d``.
|
|
467
|
+
|
|
468
|
+
Without history this returns the flat :attr:`opening_hours` /
|
|
469
|
+
:attr:`session_starts` / :attr:`session_ends`, so a migrated caller is
|
|
470
|
+
bit-identical to the pre-history behaviour. With history it returns the
|
|
471
|
+
variant selected by :meth:`schedule_index_for`.
|
|
472
|
+
|
|
473
|
+
:param d: Exchange-local trading-day date.
|
|
474
|
+
:return: ``(opening_hours, session_starts, session_ends)`` effective on ``d``.
|
|
475
|
+
"""
|
|
476
|
+
if not self.session_schedules:
|
|
477
|
+
return self.opening_hours, self.session_starts, self.session_ends
|
|
478
|
+
v = self.session_schedules[self.schedule_index_for(d)]
|
|
479
|
+
return v.opening_hours, v.session_starts, v.session_ends
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def mintick_decimals(mintick: float) -> int:
|
|
483
|
+
"""
|
|
484
|
+
Number of decimal places implied by a symbol's ``mintick``.
|
|
485
|
+
|
|
486
|
+
Derived from ``str(mintick)`` (Python's shortest round-trip repr), so
|
|
487
|
+
``0.05`` yields ``2`` and ``0.025`` yields ``3`` without exposing float
|
|
488
|
+
dust (``f"{0.05:.20f}"`` would be ``"0.05000000000000000278"``). This
|
|
489
|
+
mirrors the ``format.mintick`` logic in :mod:`pynecore.lib.string` and is
|
|
490
|
+
correct for fractional tick grids, where ``pricescale`` may be
|
|
491
|
+
``round(1 / mintick)`` (e.g. ``20`` for ``0.05``) rather than a power of
|
|
492
|
+
ten.
|
|
493
|
+
|
|
494
|
+
:param mintick: The symbol's minimum tick size.
|
|
495
|
+
:return: Decimal place count, ``0`` for non-positive or integer ticks.
|
|
496
|
+
"""
|
|
497
|
+
if not mintick or mintick <= 0:
|
|
498
|
+
return 0
|
|
499
|
+
tick_str = str(mintick)
|
|
500
|
+
if 'e' in tick_str or 'E' in tick_str:
|
|
501
|
+
# Scientific notation (very small ticks): expand without float dust.
|
|
502
|
+
tick_str = f"{mintick:.20f}".rstrip('0')
|
|
503
|
+
if '.' not in tick_str:
|
|
504
|
+
return 0
|
|
505
|
+
return len(tick_str.rstrip('0').split('.')[1])
|