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,664 @@
1
+ from typing import Any, Callable
2
+ from dataclasses import dataclass
3
+ import re
4
+ from datetime import datetime, UTC, timedelta, time
5
+ from pathlib import Path
6
+ import tomllib
7
+
8
+ from pynecore.core.plugin import override, ProviderError, Broker
9
+ from pynecore.core.plugin.live_provider import LiveProviderConfig, LiveProviderPlugin
10
+ from pynecore.core.syminfo import SymInfo, SymInfoInterval, SymInfoSession
11
+ from ..types.ohlcv import OHLCV
12
+
13
+ __all__ = ['CCXTProvider', 'CCXTError']
14
+
15
+
16
+ class CCXTError(ProviderError):
17
+ """Raised when a CCXT library call fails in a user-actionable way.
18
+
19
+ Wraps the underlying :class:`ccxt.BaseError` (and the unknown-exchange
20
+ ``AttributeError``) raised during client creation, market loading and
21
+ candle downloads so the ``pyne data`` CLI reports an exchange / network /
22
+ auth failure as a clean one-line error instead of a traceback. The live
23
+ streaming path is intentionally left unwrapped so its errors still reach
24
+ the :class:`LiveProviderPlugin` reconnect logic.
25
+ """
26
+
27
+ _KNOWN_LIMITS = {
28
+ 'binance': 1000,
29
+ 'bitget': {
30
+ '1w': 12,
31
+ '1d': 300,
32
+ '4h': 1000,
33
+ 'default': 200
34
+ },
35
+ 'bitmex': 500,
36
+ 'bybit': 200,
37
+ 'coinbase': 300,
38
+ 'kraken': 720,
39
+ 'kucoin': 1500,
40
+ 'okex': 200,
41
+ 'huobi': 2000,
42
+ }
43
+
44
+ _PYNECORE_ONLY_CONFIG_KEYS: frozenset[str] = frozenset({
45
+ 'sandbox',
46
+ 'default_type',
47
+ 'symbol_map',
48
+ })
49
+
50
+ # Fallback tick size when an exchange exposes no usable price precision at all.
51
+ # Eight decimals is the common crypto default; an over-fine tick only adds
52
+ # display precision, while a too-coarse one would silently round away real
53
+ # price moves, so erring fine is the safe direction.
54
+ _DEFAULT_MINTICK: float = 1e-8
55
+
56
+
57
+ def add_space_before_uppercase(s):
58
+ return re.sub(r'(?<!^)([A-Z])', r' \1', s)
59
+
60
+
61
+ @dataclass
62
+ class CCXTConfig(LiveProviderConfig):
63
+ """CCXT provider configuration.
64
+
65
+ Fields map to CCXT constructor keyword arguments, so adding a new
66
+ exchange-specific setting is usually as simple as declaring it here
67
+ with the same name CCXT uses — ``vars(config)`` is filtered for
68
+ truthy values and spread into the CCXT client constructor. Inherits
69
+ ``symbol_map`` from :class:`LiveProviderConfig` so ``ccxt.toml`` can
70
+ declare TradingView→native symbol translations for
71
+ :meth:`ProviderPlugin.resolve_symbol`.
72
+ """
73
+
74
+ apiKey: str = ""
75
+ """Default API key for all exchanges"""
76
+
77
+ secret: str = ""
78
+ """Default API secret for all exchanges"""
79
+
80
+ password: str = ""
81
+ """Default API password (required by some exchanges like KuCoin)"""
82
+
83
+ sandbox: bool = False
84
+ """Enable the exchange's testnet / demo endpoint via ``set_sandbox_mode``."""
85
+
86
+ default_type: str = "swap"
87
+ """Default market type — ``"spot"`` / ``"swap"`` / ``"future"`` / ``"margin"``."""
88
+
89
+
90
+ class CCXTProvider(LiveProviderPlugin[CCXTConfig]):
91
+ """CCXT-based market-data provider for ~100 crypto exchanges.
92
+
93
+ Wraps the CCXT library for symbol discovery and historical candle
94
+ downloads, and CCXT Pro for real-time WebSocket OHLCV streaming.
95
+ One plugin instance serves every CCXT-registered exchange — the
96
+ exchange is selected from the symbol prefix (`BYBIT:BTCUSDT` etc.).
97
+
98
+ **Supported**
99
+
100
+ - Symbol discovery and market metadata across all CCXT exchanges
101
+ - Historical candle downloads with per-exchange bar-limit hints
102
+ - Real-time OHLCV WebSocket streaming (CCXT Pro)
103
+ - Sandbox / testnet endpoints via the `sandbox` config flag
104
+ - Forwards any extra field on `CCXTConfig` as a CCXT constructor
105
+ kwarg — add exchange-specific settings without touching the plugin
106
+
107
+ **Limitations**
108
+
109
+ - **No order execution** — this plugin is market-data only. For live
110
+ trading use a dedicated exchange `BrokerPlugin` (Capital.com,
111
+ Interactive Brokers, Bybit, Binance).
112
+ - API credentials are per-exchange; the defaults on `CCXTConfig`
113
+ apply to every exchange unless overridden via CCXT's own routing.
114
+ """
115
+
116
+ plugin_name = "CCXT"
117
+ Config = CCXTConfig
118
+ multi_broker = True
119
+
120
+ # ``watch_ohlcv`` is trade-driven on many exchanges (ccxt.pro builds
121
+ # candles from trades where no native kline stream pushes idle
122
+ # updates), so an illiquid 24/7 pair can legitimately stay silent for
123
+ # several bars. The framework default (3 bars) would reconnect-churn
124
+ # on every quiet stretch; 30 bars keeps the dead-feed safety net while
125
+ # making false positives rare.
126
+ feed_timeout_bars = 30
127
+
128
+ @classmethod
129
+ @override
130
+ def get_list_of_brokers(cls) -> list[Broker]:
131
+ """Return the exchanges CCXT can serve, with their display names.
132
+
133
+ The id is the CCXT exchange id (e.g. ``"binance"``); the name is the
134
+ exchange's human-readable title (e.g. ``"Binance"``), read from a
135
+ throwaway instance — construction is offline and cheap (~100 exchanges
136
+ in a fraction of a second).
137
+ """
138
+ try:
139
+ import ccxt
140
+ except ImportError:
141
+ raise ImportError("CCXT is not installed. Please install it using `pip install ccxt`.")
142
+ brokers: list[Broker] = []
143
+ for exchange_id in ccxt.exchanges:
144
+ try:
145
+ name = getattr(ccxt, exchange_id)().name or ""
146
+ except Exception: # noqa: BLE001 - a single bad exchange must not drop the list
147
+ name = ""
148
+ brokers.append(Broker(id=exchange_id, name=name))
149
+ return brokers
150
+
151
+ @classmethod
152
+ @override
153
+ def to_tradingview_timeframe(cls, timeframe: str) -> str:
154
+ """
155
+ Convert CCXT timeframe fmt to TradingView fmt.
156
+
157
+ :param timeframe: Timeframe in CCXT fmt (e.g. "1m", "5m", "1h", "1d", "1w", "1M")
158
+ :return: Timeframe in TradingView fmt (e.g. "1", "5", "60", "1D", "1W", "1M")
159
+ :raises ValueError: If timeframe fmt is invalid
160
+ """
161
+ if len(timeframe) < 2:
162
+ raise ValueError(f"Invalid timeframe fmt: {timeframe}")
163
+
164
+ unit = timeframe[-1]
165
+ value = timeframe[:-1]
166
+
167
+ if not value.isdigit() or int(value) <= 0:
168
+ raise ValueError(f"Invalid timeframe value: {value}")
169
+
170
+ if unit == 'm':
171
+ return value
172
+ elif unit == 'h':
173
+ return str(int(value) * 60)
174
+ elif unit == 'd':
175
+ return f"{value}D"
176
+ elif unit == 'w':
177
+ return f"{value}W"
178
+ elif unit == 'M':
179
+ return f"{value}M"
180
+ else:
181
+ raise ValueError(f"Invalid timeframe fmt: {timeframe}")
182
+
183
+ @classmethod
184
+ @override
185
+ def to_exchange_timeframe(cls, timeframe: str) -> str:
186
+ """
187
+ Convert TradingView timeframe fmt to CCXT fmt.
188
+
189
+ :param timeframe: Timeframe in TradingView fmt (e.g. "1", "5", "60", "1D", "1W", "1M")
190
+ :return: Timeframe in CCXT fmt (e.g. "1m", "5m", "1h", "1d", "1w", "1M")
191
+ :raises ValueError: If timeframe fmt is invalid
192
+ """
193
+ if timeframe.isdigit():
194
+ mins = int(timeframe)
195
+ if mins <= 0:
196
+ raise ValueError(f"Invalid timeframe value: {timeframe}")
197
+ if mins >= 60 and mins % 60 == 0:
198
+ return f"{mins // 60}h"
199
+ return f"{mins}m"
200
+
201
+ if len(timeframe) < 2:
202
+ raise ValueError(f"Invalid timeframe fmt: {timeframe}")
203
+
204
+ unit = timeframe[-1].upper()
205
+ value = timeframe[:-1]
206
+
207
+ if not value.isdigit() or int(value) <= 0:
208
+ raise ValueError(f"Invalid timeframe value: {value}")
209
+
210
+ if unit == 'D':
211
+ return f"{value}d"
212
+ elif unit == 'W':
213
+ return f"{value}w"
214
+ elif unit == 'M':
215
+ return f"{value}M"
216
+ else:
217
+ raise ValueError(f"Invalid timeframe fmt: {timeframe}")
218
+
219
+ @override
220
+ def __init__(self, *, symbol: str | None = None, timeframe: str | None = None,
221
+ ohlcv_dir: Path | None = None, config: object | None = None):
222
+ """
223
+ :param symbol: The symbol to get data for (e.g. "binance:BTC/USDT")
224
+ :param timeframe: The timeframe to get data for in TradingView fmt
225
+ :param ohlcv_dir: The directory to save OHLCV data
226
+ :param config: Pre-loaded CCXTConfig instance
227
+ """
228
+ try:
229
+ import ccxt
230
+ except ImportError:
231
+ raise ImportError("CCXT is not installed. Please install it using `pip install ccxt`.")
232
+
233
+ super().__init__(symbol=symbol, timeframe=timeframe, ohlcv_dir=ohlcv_dir, config=config)
234
+
235
+ # Check symbol fmt
236
+ try:
237
+ if symbol is None:
238
+ raise ValueError("Error: Symbol not provided!")
239
+ xchg, symbol = symbol.split(':', 1)
240
+ except (ValueError, AttributeError):
241
+ xchg = symbol
242
+ symbol = None
243
+
244
+ if not xchg:
245
+ raise ValueError("Error: Exchange name not provided! Use 'exchange:symbol' fmt! "
246
+ "(or simple exchange, if you want to list symbols)")
247
+
248
+ self.symbol = symbol
249
+ self._exchange_name = xchg.lower()
250
+ exchange_name = self._exchange_name
251
+
252
+ # Build exchange config from the Config dataclass + optional exchange-specific TOML sections
253
+ exchange_config = {}
254
+ if self.config:
255
+ exchange_config = {
256
+ k: v for k, v in vars(self.config).items()
257
+ if v and k not in _PYNECORE_ONLY_CONFIG_KEYS
258
+ }
259
+
260
+ # Check for exchange-specific override in the raw TOML
261
+ if self.ohlcv_path:
262
+ config_dir = self.ohlcv_path.parent.parent / 'config'
263
+ else:
264
+ config_dir = None
265
+
266
+ if config_dir:
267
+ toml_path = config_dir / 'plugins' / 'ccxt.toml'
268
+ if toml_path.exists():
269
+ with open(toml_path, 'rb') as f:
270
+ raw_toml = tomllib.load(f)
271
+ if exchange_name in raw_toml and isinstance(raw_toml[exchange_name], dict):
272
+ exchange_config = raw_toml[exchange_name]
273
+
274
+ self._async_client = None
275
+ self._last_bar_timestamp: int | None = None
276
+ self._last_bar_ohlcv: OHLCV | None = None
277
+ self._exchange_config: dict[str, Any] = dict(exchange_config)
278
+
279
+ # Create the CCXT client
280
+ try:
281
+ self._client: ccxt.Exchange = getattr(ccxt, exchange_name)({
282
+ 'enableRateLimit': True,
283
+ 'adjustForTimeDifference': True,
284
+ **exchange_config
285
+ })
286
+ except AttributeError:
287
+ raise CCXTError(
288
+ f"Unknown exchange: {exchange_name!r}. Run "
289
+ f"`pyne data download ccxt --list-brokers` to see available exchanges."
290
+ ) from None
291
+ if self.config and getattr(self.config, 'sandbox', False):
292
+ try:
293
+ self._client.set_sandbox_mode(True)
294
+ except ccxt.NotSupported:
295
+ pass
296
+
297
+ @override
298
+ def normalize_symbol(self, symbol: str) -> str:
299
+ """Strip exchange prefix: ``"binance:BTC/USDT"`` → ``"BTC/USDT"``."""
300
+ return self.symbol or symbol
301
+
302
+ @override
303
+ def get_list_of_symbols(self, *args, **kwargs) -> list[str]:
304
+ """Get list of symbols."""
305
+ import ccxt
306
+ try:
307
+ self._client.load_markets()
308
+ except ccxt.BaseError as exc:
309
+ raise CCXTError(f"{self._client.id}: {exc}") from exc
310
+ return self._client.symbols or []
311
+
312
+ @staticmethod
313
+ def _create_24_7_sessions() -> tuple[
314
+ list[SymInfoInterval], list[SymInfoSession], list[SymInfoSession]
315
+ ]:
316
+ """
317
+ Create 24/7 opening hours and sessions for crypto markets.
318
+
319
+ :return: Tuple of (opening_hours, session_starts, session_ends).
320
+ """
321
+ return cls._create_24_7_sessions()
322
+
323
+ @staticmethod
324
+ def _create_24_7_sessions() -> tuple[
325
+ list[SymInfoInterval], list[SymInfoSession], list[SymInfoSession]
326
+ ]:
327
+ """
328
+ Create 24/7 opening hours and sessions for crypto markets.
329
+
330
+ :return: Tuple of (opening_hours, session_starts, session_ends).
331
+ """
332
+ opening_hours = []
333
+ session_starts = []
334
+ session_ends = []
335
+ for i in range(7):
336
+ opening_hours.append(
337
+ SymInfoInterval(day=i, start=time(hour=0, minute=0),
338
+ end=time(hour=23, minute=59, second=59)))
339
+ session_starts.append(SymInfoSession(day=i, time=time(hour=0, minute=0)))
340
+ session_ends.append(SymInfoSession(day=i, time=time(hour=23, minute=59, second=59)))
341
+ return opening_hours, session_starts, session_ends
342
+
343
+ def _derive_mintick(self, market_details: dict) -> float:
344
+ """Derive the minimum price increment (tick size) for a market.
345
+
346
+ CCXT exposes price precision in three different ``precisionMode``
347
+ flavours, and not every exchange populates ``precision.price`` at all:
348
+
349
+ * ``TICK_SIZE`` -- ``precision.price`` already *is* the tick size.
350
+ * ``DECIMAL_PLACES`` -- ``precision.price`` is a decimal-place count.
351
+ * ``SIGNIFICANT_DIGITS`` -- ``precision.price`` cannot map to a fixed
352
+ tick (it depends on the price magnitude) and is often ``None`` (e.g.
353
+ bitvavo).
354
+
355
+ For the latter two we fall back to the raw ``info['tickSize']`` the
356
+ exchange ships, then to ``limits.price.min``, and finally to a
357
+ conservative default so symbol info never crashes on a missing or
358
+ ambiguous precision.
359
+
360
+ :param market_details: A CCXT market dict.
361
+ :return: The tick size as a positive float.
362
+ """
363
+ import ccxt
364
+
365
+ precision_price = market_details.get('precision', {}).get('price')
366
+ mode = self._client.precisionMode
367
+
368
+ if precision_price is not None:
369
+ if mode == ccxt.TICK_SIZE:
370
+ return float(precision_price)
371
+ if mode == ccxt.DECIMAL_PLACES:
372
+ return 10.0 ** -int(precision_price)
373
+ # SIGNIFICANT_DIGITS: not a fixed tick -- fall through to raw values.
374
+
375
+ for raw in (market_details.get('info', {}).get('tickSize'),
376
+ market_details.get('limits', {}).get('price', {}).get('min')):
377
+ if raw is None:
378
+ continue
379
+ try:
380
+ tick = float(raw)
381
+ except (TypeError, ValueError):
382
+ continue
383
+ if tick > 0.0:
384
+ return tick
385
+
386
+ import logging
387
+ logging.getLogger(__name__).warning(
388
+ "%s: no usable price precision for %r; defaulting tick size to %g",
389
+ self._client.id, self.symbol, _DEFAULT_MINTICK,
390
+ )
391
+ return _DEFAULT_MINTICK
392
+
393
+ def _derive_qty_step(self, market_details: dict) -> float:
394
+ """Derive the minimum order quantity step (``mincontract``) for a market.
395
+
396
+ Mirrors :meth:`_derive_mintick` on the amount axis: with ``TICK_SIZE``
397
+ precision ``precision.amount`` already *is* the step, with
398
+ ``DECIMAL_PLACES`` it is a decimal-place count, and
399
+ ``SIGNIFICANT_DIGITS`` cannot map to a fixed step. The raw fallback is
400
+ ``limits.amount.min`` — TV defines ``mincontract`` as the smallest
401
+ tradable amount, which on most exchanges equals the amount step (e.g.
402
+ Binance ``LOT_SIZE`` ``stepSize`` / ``minQty``).
403
+
404
+ :param market_details: A CCXT market dict.
405
+ :return: The quantity step, or ``0.0`` when the exchange does not
406
+ expose one (the symbol info chain then falls back to volume
407
+ analysis / heuristics).
408
+ """
409
+ import ccxt
410
+
411
+ precision_amount = market_details.get('precision', {}).get('amount')
412
+ mode = self._client.precisionMode
413
+
414
+ if precision_amount is not None:
415
+ if mode == ccxt.TICK_SIZE:
416
+ return float(precision_amount)
417
+ if mode == ccxt.DECIMAL_PLACES:
418
+ return 10.0 ** -int(precision_amount)
419
+ # SIGNIFICANT_DIGITS: not a fixed step -- fall through to raw values.
420
+
421
+ raw = market_details.get('limits', {}).get('amount', {}).get('min')
422
+ try:
423
+ step = float(raw)
424
+ except (TypeError, ValueError):
425
+ return 0.0
426
+ return step if step > 0.0 else 0.0
427
+
428
+ @override
429
+ def update_symbol_info(self) -> SymInfo:
430
+ """Update symbol info from the exchange."""
431
+ import ccxt
432
+ try:
433
+ self._client.load_markets()
434
+ except ccxt.BaseError as exc:
435
+ raise CCXTError(f"{self._client.id}: {exc}") from exc
436
+ assert self._client.markets
437
+ try:
438
+ market_details = self._client.markets[self.symbol]
439
+ except KeyError:
440
+ raise CCXTError(
441
+ f"Unknown symbol {self.symbol!r} on {self._client.id}."
442
+ ) from None
443
+
444
+ opening_hours, session_starts, session_ends = self._create_24_7_sessions()
445
+
446
+ # Derive pricescale from the tick size. ``round_to_mintick`` divides by
447
+ # pricescale directly and ignores minmove, so the tick must be an
448
+ # integer reciprocal and minmove must stay 1.
449
+ mintick = self._derive_mintick(market_details)
450
+ pricescale = max(1, int(round(1.0 / mintick)))
451
+ if abs(mintick * pricescale - 1.0) > 1e-9:
452
+ raise CCXTError(
453
+ f"{self._client.id}: price tick {mintick} for {self.symbol!r} "
454
+ f"is not an integer reciprocal; cannot derive pricescale."
455
+ )
456
+ minmove = 1
457
+
458
+ try:
459
+ ticker = market_details['info']['symbol']
460
+ except KeyError:
461
+ try:
462
+ ticker = market_details['symbol']
463
+ except KeyError:
464
+ ticker = market_details['id']
465
+
466
+ assert self._client.id
467
+ return SymInfo(
468
+ prefix=self._client.id.upper(),
469
+ description=f"{market_details['base']} / {market_details['quote']} "
470
+ f"{add_space_before_uppercase(market_details['info'].get('contractType', 'Spot'))}",
471
+ ticker=ticker,
472
+ currency=market_details['quote'],
473
+ basecurrency=market_details['base'],
474
+ period=self.timeframe or "1D",
475
+ type="crypto",
476
+ mintick=mintick,
477
+ pricescale=pricescale,
478
+ minmove=minmove,
479
+ pointvalue=market_details.get('contractSize') or 1.0,
480
+ mincontract=self._derive_qty_step(market_details),
481
+ timezone=self.timezone,
482
+ opening_hours=opening_hours,
483
+ session_starts=session_starts,
484
+ session_ends=session_ends,
485
+ taker_fee=market_details.get('taker'),
486
+ maker_fee=market_details.get('maker'),
487
+ )
488
+
489
+ @override
490
+ def download_ohlcv(self, time_from: datetime, time_to: datetime,
491
+ on_progress: Callable[[datetime], None] | None = None,
492
+ limit: int | None = None, with_extra: bool = False):
493
+ """
494
+ Download OHLCV data.
495
+
496
+ :param time_from: The start time.
497
+ :param time_to: The end time.
498
+ :param on_progress: Optional callback to call on progress.
499
+ :param limit: Override the automatic chunk size.
500
+ :param with_extra: Ignored; CCXT OHLCV has no extra per-bar fields.
501
+ """
502
+ import ccxt
503
+
504
+ assert self.symbol is not None
505
+ assert self.xchg_timeframe is not None
506
+
507
+ tf: datetime = time_from.replace(tzinfo=None)
508
+ tt: datetime = time_to.replace(tzinfo=None)
509
+
510
+ if limit is None:
511
+ assert self._client.id
512
+ limit_config = _KNOWN_LIMITS.get(self._client.id, 100)
513
+
514
+ if isinstance(limit_config, dict):
515
+ limit = limit_config.get(self.xchg_timeframe, limit_config.get('default', 100))
516
+ else:
517
+ limit = limit_config
518
+
519
+ try:
520
+ while tf < tt:
521
+ if on_progress:
522
+ on_progress(tf)
523
+
524
+ try:
525
+ res: list = self._client.fetch_ohlcv(
526
+ symbol=self.symbol,
527
+ limit=limit,
528
+ timeframe=self.xchg_timeframe,
529
+ since=self._client.parse8601(tf.isoformat())
530
+ )
531
+ except ccxt.BaseError as exc:
532
+ raise CCXTError(f"{self._client.id}: {exc}") from exc
533
+
534
+ if not res:
535
+ tf += timedelta(days=1)
536
+
537
+ for r in res:
538
+ t = int(r[0] / 1000)
539
+ dt = datetime.fromtimestamp(t, UTC).replace(tzinfo=None)
540
+ if dt > tt:
541
+ raise StopIteration
542
+
543
+ ohlcv = OHLCV(
544
+ timestamp=t,
545
+ open=float(r[1]),
546
+ high=float(r[2]),
547
+ low=float(r[3]),
548
+ close=float(r[4]),
549
+ volume=float(r[5]),
550
+ )
551
+
552
+ self.save_ohlcv_data(ohlcv)
553
+ tf = dt + timedelta(minutes=1)
554
+
555
+ except StopIteration:
556
+ pass
557
+
558
+ if on_progress:
559
+ on_progress(tt)
560
+
561
+ # --- LiveProviderPlugin methods ---
562
+
563
+ @override
564
+ async def connect(self) -> None:
565
+ """Establish async CCXT Pro connection for live OHLCV streaming."""
566
+ try:
567
+ import ccxt.pro as ccxtpro
568
+ except ImportError:
569
+ raise ImportError(
570
+ "CCXT Pro is required for live data. Install it with: pip install ccxt"
571
+ )
572
+
573
+ # ``connect()`` runs on every reconnect too, so the candle-close
574
+ # tracker must be wiped: a leftover ``_last_bar_*`` from the dead
575
+ # websocket would make the first ``watch_ohlcv`` after a forced
576
+ # reconnect (e.g. the stale-feed watchdog) emit that pre-outage
577
+ # partial bar as a confirmed close once a new-timestamp candle
578
+ # arrives.
579
+ self._last_bar_timestamp = None
580
+ self._last_bar_ohlcv = None
581
+
582
+ exchange_name = self._client.id
583
+
584
+ exchange_config: dict[str, Any] = {'enableRateLimit': True}
585
+ exchange_config.update(self._exchange_config)
586
+
587
+ if self.config:
588
+ exchange_config.update({
589
+ k: v for k, v in vars(self.config).items()
590
+ if v and k not in _PYNECORE_ONLY_CONFIG_KEYS
591
+ })
592
+
593
+ self._async_client = getattr(ccxtpro, exchange_name)(exchange_config)
594
+
595
+ if self.config and self.config.sandbox:
596
+ try:
597
+ self._async_client.set_sandbox_mode(True)
598
+ except Exception as exc: # noqa: BLE001
599
+ import logging
600
+ logging.getLogger(__name__).warning(
601
+ "Exchange %r does not support sandbox mode: %s",
602
+ exchange_name, exc,
603
+ )
604
+
605
+ if self.config and self.config.default_type:
606
+ self._async_client.options = {
607
+ **getattr(self._async_client, 'options', {}),
608
+ 'defaultType': self.config.default_type,
609
+ }
610
+
611
+ @override
612
+ async def disconnect(self) -> None:
613
+ """Close the async CCXT connection."""
614
+ if hasattr(self, '_async_client') and self._async_client:
615
+ await self._async_client.close()
616
+ self._async_client = None
617
+
618
+ @property
619
+ @override
620
+ def is_connected(self) -> bool:
621
+ """Whether the async CCXT connection is active."""
622
+ return hasattr(self, '_async_client') and self._async_client is not None
623
+
624
+ @override
625
+ async def watch_ohlcv(self, symbol: str, timeframe: str) -> OHLCV:
626
+ """
627
+ Wait for the next OHLCV update from the exchange websocket.
628
+
629
+ Detects bar closure by tracking timestamp changes: when a new bar
630
+ timestamp appears, the previous bar is returned as closed. Intra-bar
631
+ updates (same timestamp) are returned with ``is_closed=False``.
632
+
633
+ :param symbol: Symbol in CCXT format (e.g. "BTC/USDT:USDT").
634
+ :param timeframe: Timeframe in TradingView format (e.g. "1D", "1", "4H").
635
+ :return: OHLCV with ``is_closed=True`` for a final bar, ``False`` for intra-bar updates.
636
+ """
637
+ xchg_tf = self.to_exchange_timeframe(timeframe)
638
+
639
+ while True:
640
+ candles = await self._async_client.watch_ohlcv(symbol, xchg_tf)
641
+ last = candles[-1]
642
+ timestamp = int(last[0] / 1000)
643
+
644
+ current_ohlcv = OHLCV(
645
+ timestamp=timestamp,
646
+ open=float(last[1]),
647
+ high=float(last[2]),
648
+ low=float(last[3]),
649
+ close=float(last[4]),
650
+ volume=float(last[5]),
651
+ is_closed=False,
652
+ )
653
+
654
+ if (self._last_bar_timestamp is not None
655
+ and timestamp != self._last_bar_timestamp):
656
+ assert self._last_bar_ohlcv is not None
657
+ closed_bar = self._last_bar_ohlcv._replace(is_closed=True)
658
+ self._last_bar_timestamp = timestamp
659
+ self._last_bar_ohlcv = current_ohlcv
660
+ return closed_bar
661
+
662
+ self._last_bar_timestamp = timestamp
663
+ self._last_bar_ohlcv = current_ohlcv
664
+ return current_ohlcv