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,262 @@
1
+ """
2
+ Currency rate provider for request.currency_rate().
3
+
4
+ Sources of exchange-rate data, in priority order:
5
+
6
+ * the chart symbol itself — when the chart is a currency pair (e.g. EURUSD
7
+ chart), ``lib.close`` is the up-to-the-second rate.
8
+ * a security context already attached to the run — backtest or live —
9
+ whose ``(basecurrency, currency)`` syminfo matches the requested pair.
10
+ The provider reads the security's latest ``close`` from its
11
+ :class:`ResultBlock` (the same channel ``__sec_read__`` uses).
12
+ * legacy file path: a sibling ``.toml`` describing a static
13
+ ``.ohlcv`` file. Only relevant when older callers still pass plain
14
+ paths; live runs never carry these.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import struct
19
+ import bisect
20
+ from math import isnan
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING
23
+
24
+ from .ohlcv_file import RECORD_SIZE
25
+ from .security_shm import ResultReader
26
+
27
+ if TYPE_CHECKING:
28
+ from .syminfo import SymInfo
29
+ from .security_shm import SyncBlock
30
+
31
+
32
+ class CurrencyRateProvider:
33
+ """
34
+ Provides currency exchange rates from running security contexts.
35
+
36
+ Walks two input sources at construction time:
37
+ * ``sec_syminfos`` — per-sec_id :class:`SymInfo` for active security
38
+ contexts (the chart-side prefetch in live mode, the backtest
39
+ ``.toml`` cache otherwise).
40
+ * ``security_data`` — legacy file paths kept for backward compatibility
41
+ with callers that have not been migrated yet.
42
+
43
+ The chart's own OHLCV is also recognized — when the chart symbol is
44
+ itself a currency pair (e.g. EURUSD), the provider returns
45
+ ``lib.close`` directly instead of consulting any security context.
46
+ """
47
+
48
+ __slots__ = (
49
+ '_pair_map', '_chart_pair',
50
+ '_sync_block', '_result_readers',
51
+ '_file_rate_cache',
52
+ )
53
+
54
+ def __init__(
55
+ self,
56
+ security_data: 'dict[str, str | Path] | None' = None,
57
+ chart_syminfo: 'SymInfo | None' = None,
58
+ sec_syminfos: 'dict[str, SymInfo] | None' = None,
59
+ sync_block: 'SyncBlock | None' = None,
60
+ ):
61
+ """
62
+ :param security_data: Legacy mapping of user keys to OHLCV file paths.
63
+ Used only when no matching ``sec_syminfo`` is
64
+ available (backtest fallback for callers that
65
+ have not provided pre-loaded syminfos).
66
+ :param chart_syminfo: Chart's SymInfo — if it carries basecurrency,
67
+ the chart itself becomes a rate source.
68
+ :param sec_syminfos: Per-sec_id :class:`SymInfo` for security
69
+ contexts active in this run. The provider
70
+ builds its ``(basecurrency, currency) →
71
+ sec_id`` lookup primarily from these.
72
+ :param sync_block: The shared :class:`SyncBlock` used by the
73
+ security infrastructure. Required when any rate
74
+ source is a sec_id (so :class:`ResultReader`
75
+ can fetch the latest pickled close from the
76
+ security's :class:`ResultBlock`).
77
+ """
78
+ # Tagged source: ``('sec', sec_id)`` for in-process security
79
+ # contexts, ``('file', path)`` for legacy file-backed sources.
80
+ self._pair_map: dict[tuple[str, str], tuple[str, str]] = {}
81
+ self._chart_pair: tuple[str, str] | None = None
82
+ self._sync_block: 'SyncBlock | None' = sync_block
83
+ self._result_readers: dict[str, ResultReader] = {}
84
+ self._file_rate_cache: dict[str, tuple[list[int], list[float]]] = {}
85
+
86
+ self._build_pair_map(
87
+ security_data or {},
88
+ chart_syminfo,
89
+ sec_syminfos or {},
90
+ )
91
+
92
+ def _build_pair_map(
93
+ self,
94
+ security_data: 'dict[str, str | Path]',
95
+ chart_syminfo: 'SymInfo | None',
96
+ sec_syminfos: 'dict[str, SymInfo]',
97
+ ) -> None:
98
+ """
99
+ Index every available rate source by ``(basecurrency, currency)``.
100
+
101
+ Priority: chart pair > running security context > static file.
102
+ On collisions within a priority class the first entry wins; this
103
+ keeps the mapping deterministic across runs.
104
+ """
105
+ if chart_syminfo and chart_syminfo.basecurrency:
106
+ self._chart_pair = (chart_syminfo.basecurrency, chart_syminfo.currency)
107
+
108
+ for sec_id, syminfo in sec_syminfos.items():
109
+ if not syminfo or not syminfo.basecurrency:
110
+ continue
111
+ pair = (syminfo.basecurrency, syminfo.currency)
112
+ self._pair_map.setdefault(pair, ('sec', sec_id))
113
+
114
+ from .syminfo import SymInfo
115
+ for _key, path in security_data.items():
116
+ ohlcv_path = self._resolve_ohlcv_path(path)
117
+ if ohlcv_path is None:
118
+ continue
119
+ toml_path = Path(ohlcv_path).with_suffix('.toml')
120
+ if not toml_path.exists():
121
+ continue
122
+ try:
123
+ syminfo = SymInfo.load_toml(toml_path)
124
+ except (ValueError, KeyError):
125
+ continue
126
+ if not syminfo.basecurrency:
127
+ continue
128
+ pair = (syminfo.basecurrency, syminfo.currency)
129
+ existing = self._pair_map.get(pair)
130
+ if existing is not None:
131
+ # Sec-backed entries (live or in-process backtest contexts)
132
+ # outrank static files; among files, the larger one wins to
133
+ # match historical behaviour.
134
+ if existing[0] == 'sec':
135
+ continue
136
+ if self._get_ohlcv_bar_count(ohlcv_path) <= \
137
+ self._get_ohlcv_bar_count(existing[1]):
138
+ continue
139
+ self._pair_map[pair] = ('file', ohlcv_path)
140
+
141
+ def get_rate(self, from_cur: str, to_cur: str, timestamp: int) -> float:
142
+ """
143
+ Get exchange rate for a currency pair at a given timestamp.
144
+
145
+ :param from_cur: Source currency code (e.g. "EUR")
146
+ :param to_cur: Target currency code (e.g. "USD")
147
+ :param timestamp: UNIX timestamp (seconds)
148
+ :return: Exchange rate, or NaN if unavailable
149
+ """
150
+ if from_cur == "NONE" or to_cur == "NONE":
151
+ return float('nan')
152
+ if from_cur == to_cur:
153
+ return 1.0
154
+
155
+ if self._chart_pair == (from_cur, to_cur):
156
+ from .. import lib
157
+ return float(lib.close)
158
+ if self._chart_pair == (to_cur, from_cur):
159
+ from .. import lib
160
+ close = float(lib.close)
161
+ return 1.0 / close if close and not isnan(close) and close != 0.0 else float('nan')
162
+
163
+ if (from_cur, to_cur) in self._pair_map:
164
+ return self._lookup(self._pair_map[(from_cur, to_cur)], timestamp)
165
+
166
+ if (to_cur, from_cur) in self._pair_map:
167
+ rate = self._lookup(self._pair_map[(to_cur, from_cur)], timestamp)
168
+ return 1.0 / rate if rate and not isnan(rate) and rate != 0.0 else float('nan')
169
+
170
+ return float('nan')
171
+
172
+ def _lookup(self, source: tuple[str, str], timestamp: int) -> float:
173
+ """Dispatch on the tagged source type."""
174
+ kind, value = source
175
+ if kind == 'sec':
176
+ return self._lookup_sec(value)
177
+ return self._lookup_file(value, timestamp)
178
+
179
+ def _lookup_sec(self, sec_id: str) -> float:
180
+ """Read the latest pickled close from the security's result block."""
181
+ if self._sync_block is None:
182
+ return float('nan')
183
+ reader = self._result_readers.get(sec_id)
184
+ if reader is None:
185
+ reader = ResultReader(sec_id)
186
+ self._result_readers[sec_id] = reader
187
+ try:
188
+ value = reader.read(self._sync_block, default=None)
189
+ except (FileNotFoundError, OSError):
190
+ # Result block not attached yet — subprocess hasn't written
191
+ # its first bar. Caller treats NaN as "not ready" and emits
192
+ # NA for this bar.
193
+ return float('nan')
194
+ if value is None:
195
+ return float('nan')
196
+ if isinstance(value, (int, float)):
197
+ return float(value)
198
+ if isinstance(value, tuple) and value and isinstance(value[0], (int, float)):
199
+ return float(value[0])
200
+ return float('nan')
201
+
202
+ def _lookup_file(self, ohlcv_path: str, timestamp: int) -> float:
203
+ """Static-file path (backtest only)."""
204
+ if ohlcv_path not in self._file_rate_cache:
205
+ self._load_ohlcv(ohlcv_path)
206
+ timestamps, closes = self._file_rate_cache[ohlcv_path]
207
+ if not timestamps:
208
+ return float('nan')
209
+ idx = bisect.bisect_right(timestamps, timestamp) - 1
210
+ if idx < 0:
211
+ return float('nan')
212
+ return closes[idx]
213
+
214
+ def _load_ohlcv(self, ohlcv_path: str) -> None:
215
+ """Load timestamps and close prices from binary OHLCV file."""
216
+ timestamps: list[int] = []
217
+ closes: list[float] = []
218
+ path = Path(ohlcv_path)
219
+ if not path.exists():
220
+ self._file_rate_cache[ohlcv_path] = (timestamps, closes)
221
+ return
222
+ file_size = path.stat().st_size
223
+ bar_count = file_size // RECORD_SIZE
224
+ with open(path, 'rb') as f:
225
+ data = f.read()
226
+ for i in range(bar_count):
227
+ offset = i * RECORD_SIZE
228
+ ts = struct.unpack_from('I', data, offset)[0]
229
+ close = struct.unpack_from('f', data, offset + 16)[0]
230
+ timestamps.append(ts)
231
+ closes.append(close)
232
+ self._file_rate_cache[ohlcv_path] = (timestamps, closes)
233
+
234
+ def close(self) -> None:
235
+ """Close every attached :class:`ResultReader`."""
236
+ for r in self._result_readers.values():
237
+ r.close()
238
+ self._result_readers.clear()
239
+
240
+ @staticmethod
241
+ def _get_ohlcv_bar_count(path: str) -> int:
242
+ """Get the number of bars in an OHLCV file without opening it."""
243
+ try:
244
+ return Path(path).stat().st_size // RECORD_SIZE
245
+ except OSError:
246
+ return 0
247
+
248
+ @staticmethod
249
+ def _resolve_ohlcv_path(path: str | Path) -> str | None:
250
+ """Resolve OHLCV file path, adding the ``.ohlcv`` extension if needed.
251
+
252
+ A dot inside the name belongs to the symbol (e.g. a perpetual
253
+ ``BTCUSDT.P``), so append by name rather than ``with_suffix`` which
254
+ would clobber the symbol's own dotted tail.
255
+ """
256
+ p = Path(path)
257
+ if p.name.endswith('.ohlcv'):
258
+ return str(p) if p.exists() else None
259
+ ohlcv_p = p.with_name(p.name + '.ohlcv')
260
+ if ohlcv_p.exists():
261
+ return str(ohlcv_p)
262
+ return str(p) if p.exists() else None