opencode-pyneruntime 6.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
@@ -0,0 +1,96 @@
1
+ from typing import TYPE_CHECKING, Any
2
+ from collections.abc import Callable, Sequence
3
+ from dataclasses import dataclass
4
+
5
+ from . import Plugin, ConfigT
6
+
7
+ if TYPE_CHECKING:
8
+ import typer
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class CLIOption:
13
+ """
14
+ Declarative description of a CLI option injected into a built-in command.
15
+
16
+ Deliberately backend-agnostic: the plugin describes the option, PyneCore
17
+ builds the concrete parser object. Plugins must not construct parser
18
+ objects themselves — Click is not a PyneCore dependency, and since Typer
19
+ 0.26 it is not a Typer dependency either (Typer vendors a reduced fork of
20
+ it), so an externally built ``click.Option`` is not even parseable by the
21
+ CLI anymore.
22
+
23
+ :ivar decls: Option string, or a tuple of them (``"--live"``,
24
+ ``("--output", "-o")``). The parameter name is derived from the first
25
+ long option, as usual.
26
+ :ivar help: Help text shown in ``--help``.
27
+ :ivar default: Value used when the option is not given on the command line.
28
+ :ivar is_flag: Boolean switch that takes no value.
29
+ :ivar type: Converter applied to the raw string value (``int``, ``float``,
30
+ ``pathlib.Path``, or any single-argument callable). Ignored when
31
+ ``choices`` is set.
32
+ :ivar choices: Restrict the value to this set.
33
+ :ivar metavar: Value placeholder in the help output.
34
+ :ivar required: Fail when the option is missing.
35
+ :ivar multiple: Allow the option to be repeated, collecting a tuple.
36
+ :ivar hidden: Keep the option out of the help output.
37
+ :ivar envvar: Environment variable to read the value from.
38
+ :ivar rich_help_panel: Help panel to group the option under.
39
+ """
40
+
41
+ decls: str | tuple[str, ...]
42
+ help: str = ""
43
+ default: Any = None
44
+ is_flag: bool = False
45
+ type: Callable[[str], Any] | None = None
46
+ choices: Sequence[str] | None = None
47
+ metavar: str | None = None
48
+ required: bool = False
49
+ multiple: bool = False
50
+ hidden: bool = False
51
+ envvar: str | None = None
52
+ rich_help_panel: str | None = None
53
+
54
+ def __post_init__(self) -> None:
55
+ if isinstance(self.decls, str):
56
+ self.decls = (self.decls,)
57
+ else:
58
+ self.decls = tuple(self.decls)
59
+ if not self.decls:
60
+ raise ValueError("CLIOption needs at least one option string")
61
+
62
+
63
+ class CLIPlugin(Plugin[ConfigT]):
64
+ """
65
+ Plugin that provides CLI commands and/or parameter hooks.
66
+
67
+ Override :meth:`cli` to add subcommands (``pyne <name> ...``).
68
+ Override :meth:`cli_params` to inject flags into existing commands.
69
+ """
70
+
71
+ @staticmethod
72
+ def cli() -> 'typer.Typer | None':
73
+ """
74
+ Return a Typer app for plugin subcommands.
75
+
76
+ Override to add commands like ``pyne <plugin_name> <subcommand>``.
77
+
78
+ :return: A Typer app, or ``None`` if the plugin has no CLI commands.
79
+ """
80
+ return None
81
+
82
+ # noinspection PyUnusedLocal
83
+ @staticmethod
84
+ def cli_params(command_name: str) -> list[CLIOption]:
85
+ """
86
+ Return extra parameters for an existing command.
87
+
88
+ Override to inject flags/options into commands like ``pyne run``.
89
+ Nested subcommands are addressed by their space-separated path, so
90
+ ``pyne data download`` is matched as ``"data download"``.
91
+
92
+ :param command_name: The command to extend (e.g. ``"run"`` or
93
+ ``"data download"``).
94
+ :return: List of option specs, or ``[]`` if no hooks for this command.
95
+ """
96
+ return []
@@ -0,0 +1,208 @@
1
+ from abc import abstractmethod, ABCMeta
2
+ from dataclasses import dataclass, field
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, TypeVar
6
+
7
+ from pynecore.types.ohlcv import OHLCV
8
+
9
+ from .provider import ProviderPlugin
10
+
11
+ if TYPE_CHECKING:
12
+ from ..syminfo import SymInfo
13
+
14
+
15
+ @dataclass
16
+ class LiveProviderConfig:
17
+ """Common base for all live-provider plugin configs.
18
+
19
+ Provides the symbol-translation map every live plugin shares so a Pine
20
+ script can keep using TradingView-style symbols (e.g. ``"FX:EURUSD"``)
21
+ even when the running provider exposes a different native identifier
22
+ (e.g. Capital.com's ``"EURUSD"`` epic). Subclasses inherit this field
23
+ via dataclass inheritance and add their own credentials/tunables on top.
24
+
25
+ Generated TOML example (a commented default appears in every live
26
+ plugin's config file)::
27
+
28
+ # Optional translation map. Keys are TradingView-style symbols as
29
+ # written in your Pine script (``request.security("FX:EURUSD", ...)``);
30
+ # values are the native identifier the plugin sends to the
31
+ # exchange API. Example for Capital.com::
32
+ #
33
+ # [symbol_map]
34
+ # "FX:EURUSD" = "EURUSD"
35
+ # "OANDA:XAUUSD" = "GOLD"
36
+ #
37
+ # When a Pine symbol is not in the map the plugin falls back to
38
+ # ``normalize_symbol()`` (provider-specific prefix-strip etc.).
39
+ #symbol_map = {}
40
+ """
41
+
42
+ symbol_map: dict[str, str] = field(default_factory=dict)
43
+ """Optional translation map. Keys are TradingView-style symbols as
44
+ written in Pine scripts (e.g. ``"FX:EURUSD"``); values are the native
45
+ identifier the plugin sends to the exchange API (e.g. ``"EURUSD"``).
46
+ Missing keys fall back to ``ProviderPlugin.normalize_symbol``."""
47
+
48
+
49
+ LiveProviderConfigT = TypeVar('LiveProviderConfigT', bound=LiveProviderConfig)
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class PluginSymbol:
54
+ """Live-mode descriptor for a :func:`request.security` data source.
55
+
56
+ Replaces the historical ``.ohlcv`` file path in live runs: the security
57
+ subprocess instantiates ``provider_name``'s :class:`LiveProviderPlugin`
58
+ itself, downloads warmup history in-memory, then streams live bars.
59
+ No intermediate ``.ohlcv`` file is written.
60
+
61
+ :ivar provider_name: Entry-point name for :func:`load_plugin`
62
+ (e.g. ``"capitalcom"``).
63
+ :ivar symbol: Plugin-native symbol (e.g. ``"EURUSD"``); already passed
64
+ through :meth:`ProviderPlugin.resolve_symbol`.
65
+ :ivar timeframe: Timeframe in TradingView format (e.g. ``"1D"``).
66
+ :ivar config: Pre-loaded plugin config dataclass instance. The chart
67
+ process runs :func:`ensure_config` once and hands the
68
+ resulting instance to every subprocess via spawn args —
69
+ subprocesses must not touch the TOML file themselves
70
+ because :func:`ensure_config` is not process-safe.
71
+ :ivar time_from: Optional warmup-window start. Defaults to ``None``,
72
+ which lets the subprocess fall back to its built-in
73
+ ``_DEFAULT_WARMUP_BARS`` heuristic; the chart passes
74
+ its own ``--from`` here so security contexts inherit
75
+ the same look-back range.
76
+ :ivar syminfo: Optional pre-fetched :class:`SymInfo`. The chart
77
+ process can call ``provider.update_symbol_info()`` once
78
+ and pass the result here, so the subprocess does not
79
+ have to repeat the REST round-trip on startup. ``None``
80
+ means the subprocess will fetch syminfo itself.
81
+ :ivar is_rate_source: When ``True`` the subprocess runs in a stripped
82
+ "close-only" loop instead of importing and executing the
83
+ Pine script. Used by the auto-spawned rate-source
84
+ contexts created from a ``request.security(..., currency=X)``
85
+ request when no explicit context already covers the
86
+ required currency pair.
87
+ :ivar ohlcv_dir: Optional path to the chart process's OHLCV data dir.
88
+ Forwarded to the subprocess so the provider can locate
89
+ workdir-side resources that live next to the data dir —
90
+ most notably per-exchange config overrides in
91
+ ``<workdir>/config/plugins/<provider>.toml`` (e.g. the
92
+ ``[binance]`` section of ``ccxt.toml``). Without it the
93
+ subprocess provider runs with default config while the
94
+ chart side runs with the override, breaking auth and
95
+ market-type selection for cross-symbol security feeds.
96
+ ``None`` means the subprocess provider is constructed
97
+ without an OHLCV directory (no file is written either way).
98
+ """
99
+
100
+ provider_name: str
101
+ symbol: str
102
+ timeframe: str
103
+ config: LiveProviderConfig | None = None
104
+ time_from: datetime | None = None
105
+ syminfo: 'SymInfo | None' = None
106
+ is_rate_source: bool = False
107
+ ohlcv_dir: Path | None = None
108
+
109
+
110
+ class LiveProviderPlugin(ProviderPlugin[LiveProviderConfigT], metaclass=ABCMeta):
111
+ """
112
+ WebSocket/streaming data provider extending :class:`ProviderPlugin`.
113
+
114
+ Adds real-time data streaming to the offline OHLCV download capability.
115
+ Subclasses must implement connection lifecycle and data streaming methods
116
+ in addition to the :class:`ProviderPlugin` abstract methods.
117
+
118
+ The async methods run in a background thread; the framework bridges them
119
+ to the synchronous :class:`ScriptRunner` via a :class:`queue.Queue`.
120
+ """
121
+
122
+ reconnect_delay: float = 1.0
123
+ """Initial delay in seconds before a reconnection attempt. Doubles per
124
+ consecutive failure (exponential backoff) up to :attr:`max_reconnect_delay`."""
125
+
126
+ max_reconnect_delay: float = 60.0
127
+ """Ceiling in seconds for the exponential reconnect backoff.
128
+
129
+ Reconnection is retried indefinitely — a live session must survive
130
+ arbitrarily long network / provider outages and resume on its own once
131
+ connectivity returns — so there is no attempt limit; the backoff simply
132
+ saturates at this delay.
133
+ """
134
+
135
+ feed_timeout_bars: int | None = 3
136
+ """Feed-liveness watchdog threshold, in timeframe periods.
137
+
138
+ When the provider reports a healthy connection but :meth:`watch_ohlcv`
139
+ has produced no update (closed bar or intra-bar) for this many timeframe
140
+ periods (floored at 90 s) during an open trading session, the framework
141
+ assumes the data feed is dead even though the transport looks alive
142
+ (half-open socket, lost server-side subscription) and drives a full
143
+ ``disconnect()`` → ``connect()`` cycle. ``None`` disables the watchdog —
144
+ only justified for providers whose feed legitimately stays silent for
145
+ long in-session stretches AND whose own liveness machinery already
146
+ covers the dead-feed case.
147
+ """
148
+
149
+ # --- Connection lifecycle ---
150
+
151
+ @abstractmethod
152
+ async def connect(self) -> None:
153
+ """Establish connection to the data source.
154
+
155
+ Called for the initial connection AND again on every reconnect (the
156
+ framework drives ``disconnect()`` → ``connect()`` after a connection
157
+ error or a stale feed). Implementations must therefore fully
158
+ re-initialize every piece of connection-scoped state here —
159
+ authentication, server-side subscriptions, partially accumulated
160
+ quote state — rather than assuming a first-call-only environment.
161
+ """
162
+
163
+ @abstractmethod
164
+ async def disconnect(self) -> None:
165
+ """Close connection cleanly."""
166
+
167
+ @property
168
+ @abstractmethod
169
+ def is_connected(self) -> bool:
170
+ """Whether the connection is currently active."""
171
+
172
+ # --- Data streaming ---
173
+
174
+ @abstractmethod
175
+ async def watch_ohlcv(self, symbol: str, timeframe: str) -> OHLCV:
176
+ """
177
+ Wait for and return the next OHLCV update.
178
+
179
+ Called in a loop by the framework. Each call blocks (awaits)
180
+ until new data arrives from the data source.
181
+
182
+ :param symbol: The symbol in provider-specific format.
183
+ :param timeframe: Timeframe in TradingView format (e.g. ``"1D"``, ``"1"``, ``"4H"``).
184
+ :return: An :class:`OHLCV` with ``is_closed=True`` for a final bar, ``False`` for intra-bar updates.
185
+ """
186
+
187
+ # --- Reconnection hooks (override for custom behavior) ---
188
+
189
+ async def on_disconnect(self) -> None:
190
+ """Called when the connection is unexpectedly lost."""
191
+
192
+ async def on_reconnect(self) -> None:
193
+ """Called after a successful reconnection."""
194
+
195
+ # --- Shutdown hooks ---
196
+
197
+ # noinspection PyMethodMayBeStatic
198
+ async def can_shutdown(self) -> bool:
199
+ """
200
+ Whether the provider is ready to shut down.
201
+
202
+ Override to delay shutdown while cleanup is in progress
203
+ (e.g. waiting for open orders to fill or positions to close).
204
+ Called every second during the graceful shutdown phase.
205
+
206
+ :return: True if ready to shut down, False to keep waiting.
207
+ """
208
+ return True
@@ -0,0 +1,331 @@
1
+ import logging
2
+ from typing import Callable, NamedTuple, TYPE_CHECKING
3
+ from abc import abstractmethod, ABCMeta
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+
7
+ from pynecore.types.ohlcv import OHLCV
8
+ from pynecore.core.syminfo import SymInfo, default_mincontract
9
+ from pynecore.core.ohlcv_file import OHLCVWriter, OHLCVReader
10
+
11
+ from . import Plugin, ConfigT
12
+
13
+ if TYPE_CHECKING:
14
+ from pynecore.core.symbol_map import SymbolMap
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class Broker(NamedTuple):
20
+ """A selectable broker / exchange of a :attr:`~ProviderPlugin.multi_broker` provider.
21
+
22
+ :ivar id: The canonical selector used in the provider string and the saved
23
+ filename (e.g. ``"pepperstoneuk"``, ``"binance"``). Must be space-free.
24
+ :ivar name: A human-readable display name (e.g. ``"Pepperstone - Europe"``,
25
+ ``"Binance"``), or ``""`` when the provider exposes none.
26
+ """
27
+ id: str
28
+ name: str = ""
29
+
30
+
31
+ class ProviderPlugin(Plugin[ConfigT], metaclass=ABCMeta):
32
+ """
33
+ Base class for all data providers.
34
+
35
+ Subclasses must implement the abstract methods and define a ``Config``
36
+ dataclass for configuration (used by :func:`pynecore.core.config.ensure_config`).
37
+ """
38
+
39
+ timezone: str = 'UTC'
40
+ """Default timezone of the provider."""
41
+
42
+ symbol: str | None = None
43
+ """Symbol of the provider."""
44
+
45
+ timeframe: str | None = None
46
+ """Timeframe of the provider."""
47
+
48
+ xchg_timeframe: str | None = None
49
+ """Exchange-specific timeframe format."""
50
+
51
+ ohlcv_path: Path | None = None
52
+ """Path to the OHLCV data file."""
53
+
54
+ syminfo: SymInfo | None = None
55
+ """Symbol info pre-fetched by the chart side (security subprocesses store
56
+ it here instead of issuing a second REST round-trip)."""
57
+
58
+ global_symbol_map: 'SymbolMap | None' = None
59
+ """Global workdir symbol map (``config/symbol_map.toml``), consulted by
60
+ :meth:`resolve_symbol` AFTER the plugin's own ``config.symbol_map``. Set by
61
+ the framework on the chart provider instance; ``None`` disables the fallback."""
62
+
63
+ provider_name: str | None = None
64
+ """This provider's entry-point name, used to gate global-map entries to the
65
+ running provider (an entry naming a different provider is warned + skipped,
66
+ pending multi-provider support)."""
67
+
68
+ fetch_all_by_default: bool = False
69
+ """If True, fetch all available data when no start date is given (instead of 1 year)."""
70
+
71
+ multi_broker: bool = False
72
+ """If True, this provider serves many brokers/exchanges and the first segment
73
+ of the provider string after the provider name selects the broker
74
+ (e.g. ``ccxt:BYBIT:BTC/USDT:USDT`` → broker ``BYBIT``). Single-broker
75
+ providers leave this ``False`` and treat the whole string as the symbol."""
76
+
77
+ mincontract_estimated: bool = False
78
+ """True when the last :meth:`get_symbol_info` fetch had to estimate
79
+ ``mincontract`` because the provider returned no exchange value. The
80
+ download flow then refines the estimate from the downloaded volume data."""
81
+
82
+ @classmethod
83
+ @abstractmethod
84
+ def to_tradingview_timeframe(cls, timeframe: str) -> str:
85
+ """
86
+ Convert timeframe to TradingView format.
87
+
88
+ :param timeframe: Timeframe in exchange format.
89
+ :return: Timeframe in TradingView format.
90
+ """
91
+
92
+ @classmethod
93
+ @abstractmethod
94
+ def to_exchange_timeframe(cls, timeframe: str) -> str:
95
+ """
96
+ Convert timeframe to exchange format.
97
+
98
+ :param timeframe: Timeframe in TradingView format.
99
+ :return: Timeframe in exchange format.
100
+ """
101
+
102
+ @classmethod
103
+ def get_ohlcv_path(cls, symbol: str, timeframe: str, ohlcv_dir: Path,
104
+ provider_name: str | None = None) -> Path:
105
+ """
106
+ Get the output path of the OHLCV data file.
107
+
108
+ :param symbol: Symbol name.
109
+ :param timeframe: Timeframe in TradingView format.
110
+ :param ohlcv_dir: Directory to save OHLCV data.
111
+ :param provider_name: Override provider name in filename.
112
+ :return: Path to the OHLCV file.
113
+ """
114
+ return ohlcv_dir / (f"{provider_name or cls.__name__.lower().replace('provider', '').replace('plugin', '')}"
115
+ f"_{symbol.replace('/', '_').replace(':', '_').upper()}"
116
+ f"_{timeframe}.ohlcv")
117
+
118
+ def __init__(self, *, symbol: str | None = None, timeframe: str | None = None,
119
+ ohlcv_dir: Path | None = None, config: ConfigT | None = None):
120
+ """
121
+ :param symbol: The symbol to get data for.
122
+ :param timeframe: The timeframe to get data for in TradingView format.
123
+ :param ohlcv_dir: The directory to save OHLCV data.
124
+ :param config: Pre-loaded config dataclass instance.
125
+ """
126
+ self.symbol = symbol
127
+ self.timeframe = timeframe
128
+ self.xchg_timeframe = self.to_exchange_timeframe(timeframe) if timeframe else None
129
+ if ohlcv_dir:
130
+ assert symbol and timeframe
131
+ self.ohlcv_path = self.get_ohlcv_path(symbol, timeframe, ohlcv_dir)
132
+ else:
133
+ self.ohlcv_path = None
134
+ self.ohlcv_file = OHLCVWriter(self.ohlcv_path) if self.ohlcv_path else None
135
+ self.config: ConfigT | None = config
136
+
137
+ def normalize_symbol(self, symbol: str) -> str:
138
+ """
139
+ Normalize a provider-format symbol to the exchange API format.
140
+
141
+ Called by the framework before passing ``symbol`` to :meth:`watch_ohlcv`
142
+ in the live runner. For historical methods (:meth:`download_ohlcv`,
143
+ :meth:`update_symbol_info`), providers use ``self.symbol`` directly —
144
+ handle any needed format conversion in ``__init__`` instead.
145
+
146
+ Override when the user-configured symbol includes prefixes or formatting
147
+ that the exchange API cannot accept
148
+ (e.g. stripping ``"binance:"`` from ``"binance:BTC/USDT"``).
149
+
150
+ :param symbol: Symbol as configured by the user.
151
+ :return: Symbol in the format the exchange API expects.
152
+ """
153
+ return symbol
154
+
155
+ def resolve_symbol(self, pine_key: str) -> str:
156
+ """
157
+ Translate a Pine-style symbol key to the plugin-native form.
158
+
159
+ Live ``request.security()`` calls hand the framework a TradingView-style
160
+ symbol (e.g. ``"FX:EURUSD"``). This method consults
161
+ ``config.symbol_map`` first (the per-plugin TOML translation table),
162
+ then the global workdir ``symbol_map.toml`` (:attr:`global_symbol_map`,
163
+ only for entries whose provider matches :attr:`provider_name` — an entry
164
+ naming a different provider is warned + skipped); if the key is not
165
+ mapped the default fallback is the identity, i.e. the Pine key is
166
+ forwarded unchanged on the assumption that the user already wrote a
167
+ plugin-native symbol.
168
+
169
+ ``normalize_symbol`` is deliberately **not** used as the fallback:
170
+ provider instances bind ``normalize_symbol`` to the chart's own
171
+ symbol (e.g. CCXT's returns ``self.symbol`` regardless of the
172
+ argument), so consulting it for a cross-symbol key would silently
173
+ resolve to the chart symbol and download wrong data.
174
+
175
+ Plugins that need real cross-symbol translation should override
176
+ :meth:`resolve_symbol` directly.
177
+
178
+ :param pine_key: Symbol as written in the Pine script.
179
+ :return: Symbol in the format the plugin's exchange API expects.
180
+ """
181
+ sm = getattr(self.config, 'symbol_map', None)
182
+ if sm and pine_key in sm:
183
+ return sm[pine_key]
184
+ gm = self.global_symbol_map
185
+ if gm:
186
+ mapped = gm.resolve(pine_key)
187
+ if mapped is not None:
188
+ if self.provider_name is not None and mapped.provider != self.provider_name:
189
+ logger.warning(
190
+ "Skipping global symbol_map entry %r -> %r: it targets "
191
+ "provider %r but the running provider is %r "
192
+ "(multi-provider resolution is not supported yet).",
193
+ pine_key, f"{mapped.provider}:{mapped.native_symbol}",
194
+ mapped.provider, self.provider_name)
195
+ else:
196
+ return mapped.native_symbol
197
+ return pine_key
198
+
199
+ @classmethod
200
+ def construct_pair_symbol(cls, from_cur: str, to_cur: str) -> str:
201
+ """
202
+ Build a Pine-style symbol for a currency pair.
203
+
204
+ Used by the auto-spawn rate-source path when a Pine script needs a
205
+ ``(from_cur, to_cur)`` rate that is not already exposed by the chart
206
+ or by an explicit ``request.security()`` context. The default
207
+ concatenation (``"EUR" + "USD" -> "EURUSD"``) matches the most common
208
+ FX symbol convention; plugins whose API expects a different shape
209
+ (e.g. ``"EUR-USD"`` or ``"EUR/USD"``) can override.
210
+
211
+ The returned key is fed through :meth:`resolve_symbol`, so users can
212
+ still keep TradingView prefixes (``"FX:EURUSD"``) in their
213
+ ``symbol_map`` instead of relying on the raw concatenation.
214
+ """
215
+ return f"{from_cur}{to_cur}"
216
+
217
+ def __enter__(self) -> OHLCVWriter:
218
+ assert self.ohlcv_file is not None
219
+ return self.ohlcv_file.open()
220
+
221
+ def __exit__(self, exc_type, exc_val, exc_tb):
222
+ assert self.ohlcv_file is not None
223
+ self.ohlcv_file.close()
224
+
225
+ @classmethod
226
+ def get_list_of_brokers(cls) -> list[Broker]:
227
+ """
228
+ Get the list of brokers/exchanges this provider can serve.
229
+
230
+ Only meaningful for :attr:`multi_broker` providers. Optional — the
231
+ default raises :class:`NotImplementedError`, which the ``pyne data``
232
+ CLI catches and reports gracefully. Implemented as a classmethod so it
233
+ can answer ``--list-brokers`` without a symbol-bound instance.
234
+
235
+ :return: List of :class:`Broker` records (``id`` selector + optional
236
+ human-readable ``name``).
237
+ :raises NotImplementedError: If the provider does not enumerate brokers.
238
+ """
239
+ raise NotImplementedError(
240
+ f"{cls.__name__} does not support listing brokers"
241
+ )
242
+
243
+ @abstractmethod
244
+ def get_list_of_symbols(self, *args, **kwargs) -> list[str]:
245
+ """
246
+ Get list of available symbols.
247
+
248
+ :return: List of symbol names.
249
+ """
250
+
251
+ @abstractmethod
252
+ def update_symbol_info(self) -> SymInfo:
253
+ """
254
+ Fetch and return symbol info from the exchange.
255
+
256
+ This should include opening hours and session data.
257
+
258
+ :return: Symbol information.
259
+ """
260
+
261
+ def is_symbol_info_exists(self) -> bool:
262
+ """
263
+ Check if the symbol info TOML file exists.
264
+
265
+ :return: True if the file exists.
266
+ """
267
+ assert self.ohlcv_path is not None
268
+ return self.ohlcv_path.with_suffix('.toml').exists()
269
+
270
+ def get_symbol_info(self, force_update=False) -> SymInfo:
271
+ """
272
+ Get symbol info, loading from cache or fetching from exchange.
273
+
274
+ :param force_update: Force update from exchange even if cached.
275
+ :return: Symbol information.
276
+ """
277
+ assert self.ohlcv_path is not None
278
+ toml_path = self.ohlcv_path.with_suffix('.toml')
279
+ if self.is_symbol_info_exists() and not force_update:
280
+ return SymInfo.load_toml(toml_path)
281
+
282
+ sym_info = self.update_symbol_info()
283
+ if sym_info.mincontract <= 0.0:
284
+ # No exchange value (providers signal that with 0.0): estimate it.
285
+ # The download flow refines the estimate from the downloaded
286
+ # volume data, see ``mincontract_estimated``.
287
+ sym_info.mincontract = default_mincontract(sym_info.type, sym_info.basecurrency)
288
+ self.mincontract_estimated = True
289
+ sym_info.save_toml(toml_path)
290
+ return sym_info
291
+
292
+ def save_ohlcv_data(self, data: OHLCV | list[OHLCV]):
293
+ """
294
+ Save OHLCV data to the file.
295
+
296
+ :param data: Single OHLCV record or list of records.
297
+ """
298
+ assert self.ohlcv_file is not None
299
+ if isinstance(data, OHLCV):
300
+ self.ohlcv_file.write(data)
301
+ else:
302
+ for candle in data:
303
+ self.ohlcv_file.write(candle)
304
+
305
+ @abstractmethod
306
+ def download_ohlcv(self, time_from: datetime, time_to: datetime,
307
+ on_progress: Callable[[datetime], None] | None = None,
308
+ limit: int | None = None, with_extra: bool = False):
309
+ """
310
+ Download OHLCV data from the exchange.
311
+
312
+ Use :meth:`save_ohlcv_data` to write records to the data file.
313
+
314
+ :param time_from: The start time. Use ``datetime.fromtimestamp(0)`` to fetch all available data.
315
+ :param time_to: The end time.
316
+ :param on_progress: Optional progress callback.
317
+ :param limit: Override the automatic chunk size (number of bars per API request).
318
+ :param with_extra: When ``True``, also fetch and persist the provider's
319
+ extra per-bar fields (e.g. ask/bid/spread) to the ``.extra.csv``
320
+ sidecar. Off by default: the extra fields cost extra requests to
321
+ fetch and slow every later backtest that loads the sidecar, so they
322
+ are only produced on request. Providers without extra fields ignore it.
323
+ """
324
+
325
+ def load_ohlcv_data(self) -> OHLCVReader:
326
+ """
327
+ Load OHLCV data from the file.
328
+
329
+ :return: An OHLCVReader instance.
330
+ """
331
+ return OHLCVReader(str(self.ohlcv_path))