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,547 @@
1
+ from __future__ import annotations
2
+ from typing import Iterator, Optional, Literal
3
+ import io
4
+ import mmap
5
+ import csv
6
+ import queue
7
+ import threading
8
+ from pathlib import Path
9
+ from datetime import datetime, UTC
10
+
11
+ from pynecore.types.ohlcv import OHLCV
12
+ from pynecore.types.na import NA
13
+
14
+ DO_NOTHING = -1
15
+ WRITE_TUPLE = 0
16
+ WRITE_DICT = 1
17
+ WRITE_OHLCV = 2
18
+ STOP = 3
19
+
20
+
21
+ class DialectLF(csv.excel):
22
+ """CSV dialect with line feed as newline character"""
23
+ lineterminator = '\n'
24
+
25
+
26
+ csv.register_dialect("lf", DialectLF)
27
+
28
+
29
+ class CSVWriter:
30
+ """
31
+ Fast CSV writer for OHLCV data with extra fields.
32
+ Uses a background thread and buffering for better performance.
33
+ """
34
+ __slots__ = ('path', '_file', '_buffer_size', '_float_fmt',
35
+ '_timestamp_as_iso', '_headers', '_queue',
36
+ '_worker', '_error', '_is_open', '_lock',
37
+ '_idle_time', '_dialect')
38
+
39
+ def __init__(self, path: Path, *,
40
+ buffer_size: int = 32768,
41
+ queue_size: int = 4096,
42
+ float_fmt: str = '.8g',
43
+ timestamp_as_iso: bool = True,
44
+ idle_time: float = 0.016,
45
+ dialect: Literal['lf', 'excel', 'excel-tab', 'unix'] = 'lf',
46
+ headers: tuple | list | None = None):
47
+ """
48
+ :param path: Output file path
49
+ :param buffer_size: Internal buffer size in bytes
50
+ :param queue_size: Size of the command queue
51
+ :param float_fmt: Format string for float values
52
+ :param timestamp_as_iso: If True, timestamps will be written as ISO datetime strings
53
+ :param idle_time: Idle time in seconds before flushing the buffer
54
+ :param dialect: CSV dialect, one of 'excel', 'excel-tab', 'unix'
55
+ :param headers: Optional list of headers to write
56
+ """
57
+ self.path = path
58
+ self._idle_time = idle_time
59
+ self._dialect = dialect
60
+
61
+ self._file: io.TextIOWrapper | None = None
62
+ self._buffer_size = buffer_size
63
+ self._float_fmt = float_fmt
64
+ self._timestamp_as_iso = timestamp_as_iso
65
+ self._headers = headers
66
+
67
+ # Thread-safe queue for commands
68
+ self._queue = queue.Queue(maxsize=queue_size)
69
+ self._worker = None
70
+ self._error = None
71
+ self._is_open = False
72
+ self._lock = threading.Lock()
73
+
74
+ def __enter__(self):
75
+ self.open()
76
+ return self
77
+
78
+ def __exit__(self, exc_type, exc_val, exc_tb):
79
+ self.close()
80
+
81
+ @property
82
+ def is_open(self) -> bool:
83
+ """Check if the writer is open"""
84
+ return self._is_open
85
+
86
+ def _worker_thread(self):
87
+ """Background worker thread for handling I/O operations"""
88
+ buffer = io.StringIO()
89
+ writer = csv.writer(buffer, dialect=self._dialect)
90
+
91
+ # Format string for float values
92
+ fmt = '{:' + self._float_fmt + '}'
93
+
94
+ def fmt_float(x: object) -> str:
95
+ # Canonicalize na to Pine's "NaN". A native nan would format lowercase
96
+ # ("nan") via fmt; an NA object formats as "NaN" already but we keep the
97
+ # branch representation-agnostic so both map to the same token.
98
+ if isinstance(x, NA) or x != x:
99
+ return "NaN"
100
+ return fmt.format(x)
101
+
102
+ row = []
103
+
104
+ assert self._file is not None
105
+
106
+ try:
107
+ while True:
108
+ try:
109
+ cmd, data = self._queue.get(timeout=self._idle_time)
110
+ except queue.Empty:
111
+ # Write buffer if idle
112
+ if buffer.tell() > self._buffer_size // 2:
113
+ self._file.write(buffer.getvalue())
114
+ self._file.flush()
115
+ buffer.truncate(0)
116
+ buffer.seek(0)
117
+ continue
118
+
119
+ if cmd == STOP:
120
+ break
121
+
122
+ # Write header if needed
123
+ if not self._headers:
124
+ if cmd == WRITE_DICT:
125
+ headers = list(data.keys())
126
+ writer.writerow(headers)
127
+ elif cmd == WRITE_OHLCV:
128
+ headers = ['time', 'open', 'high', 'low', 'close', 'volume']
129
+ if data.extra_fields:
130
+ headers.extend(data.extra_fields.keys())
131
+ writer.writerow(headers)
132
+ else:
133
+ raise ValueError(f"No headers provided!")
134
+ self._headers = headers
135
+
136
+ # Format Timestamp
137
+ row.clear()
138
+
139
+ # Raw dictionary data
140
+ if cmd == WRITE_DICT:
141
+ data = data.values()
142
+
143
+ # OHLCV data
144
+ if cmd == WRITE_OHLCV:
145
+ if self._timestamp_as_iso:
146
+ row.append(datetime.fromtimestamp(data.timestamp, UTC).isoformat())
147
+ else:
148
+ row.append(str(data.timestamp))
149
+
150
+ # Format OHLCV values
151
+ row.extend(fmt_float(x) for x in (data.open, data.high, data.low, data.close, data.volume))
152
+ # Format extra fields
153
+ if data.extra_fields:
154
+ for value in data.extra_fields.values():
155
+ if isinstance(value, float):
156
+ row.append(fmt_float(value))
157
+ elif isinstance(value, NA):
158
+ row.append("NaN")
159
+ else:
160
+ row.append(str(value))
161
+
162
+ # Tuple or dict data
163
+ else:
164
+ for value in data:
165
+ if isinstance(value, float):
166
+ row.append(fmt_float(value))
167
+ elif isinstance(value, datetime):
168
+ if self._timestamp_as_iso:
169
+ row.append(value.isoformat())
170
+ else:
171
+ row.append(str(value))
172
+ elif isinstance(value, NA):
173
+ row.append("NaN")
174
+ else:
175
+ row.append(str(value))
176
+
177
+ # Write row to buffer
178
+ writer.writerow(row)
179
+ self._queue.task_done()
180
+
181
+ # Write if buffer is half full
182
+ if buffer.tell() >= self._buffer_size // 2:
183
+ self._file.write(buffer.getvalue())
184
+ self._file.flush()
185
+ buffer.truncate(0)
186
+ buffer.seek(0)
187
+
188
+ except Exception as e:
189
+ self._error = e
190
+ finally:
191
+ # Final flush
192
+ if buffer.tell() > 0:
193
+ try:
194
+ self._file.write(buffer.getvalue())
195
+ self._file.flush()
196
+ except: # noqa
197
+ pass
198
+
199
+ def open(self) -> CSVWriter:
200
+ """Open the CSV file and start the worker thread"""
201
+ with self._lock:
202
+ if self._is_open:
203
+ return self
204
+
205
+ # Open file for writing
206
+ file = open(self.path, 'w', buffering=self._buffer_size)
207
+ self._file = file
208
+ self._is_open = True
209
+
210
+ # Write headers if provided
211
+ if self._headers:
212
+ writer = csv.writer(file, dialect=self._dialect)
213
+ writer.writerow(self._headers)
214
+
215
+ # Start the worker thread
216
+ self._worker = threading.Thread(target=self._worker_thread)
217
+ self._worker.daemon = True # Thread dies with the program
218
+ self._worker.start()
219
+
220
+ return self
221
+
222
+ def write_dict(self, data: dict[str, int | float | str], timeout: Optional[float] = None) -> bool:
223
+ """
224
+ Write a raw dict record.
225
+
226
+ :param data: The dict to write
227
+ :param timeout: Optional timeout in seconds
228
+ :return: True if write command was queued, False on timeout
229
+ :raises RuntimeError: If writer thread has died with an error
230
+ """
231
+ if not self._is_open:
232
+ raise RuntimeError("Writer not opened!")
233
+ if self._error:
234
+ raise RuntimeError(f"Writer thread error: {self._error}!")
235
+
236
+ try:
237
+ self._queue.put((WRITE_DICT, data), timeout=timeout)
238
+ return True
239
+ except queue.Full:
240
+ return False
241
+
242
+ def write(self, *data: object, timeout: Optional[float] = None) -> bool:
243
+ """
244
+ Write raw data
245
+
246
+ :param data: the data to write
247
+ :param timeout: Optional timeout in seconds
248
+ :return: True if write command was queued, False on timeout
249
+ :raises RuntimeError: If writer thread has died with an error
250
+ """
251
+ if not self._is_open:
252
+ raise RuntimeError("Writer not opened!")
253
+ if self._error:
254
+ raise RuntimeError(f"Writer thread error: {self._error}!")
255
+
256
+ try:
257
+ self._queue.put((WRITE_TUPLE, data), timeout=timeout)
258
+ return True
259
+ except queue.Full:
260
+ return False
261
+
262
+ def write_ohlcv(self, candle: OHLCV, timeout: Optional[float] = None) -> bool:
263
+ """
264
+ Write a single OHLCV record.
265
+
266
+ :param candle: The OHLCV record to write
267
+ :param timeout: Optional timeout in seconds
268
+ :return: True if write command was queued, False on timeout
269
+ :raises RuntimeError: If writer thread has died with an error
270
+ """
271
+ if not self._is_open:
272
+ raise RuntimeError("Writer not opened!")
273
+ if self._error:
274
+ raise RuntimeError(f"Writer thread error: {self._error}!")
275
+
276
+ try:
277
+ self._queue.put((WRITE_OHLCV, candle), timeout=timeout)
278
+ return True
279
+ except queue.Full:
280
+ return False
281
+
282
+ def flush(self):
283
+ """
284
+ Wait for all pending writes to be processed by the worker thread.
285
+ """
286
+ if self._is_open and self._queue is not None:
287
+ self._queue.join()
288
+
289
+ def close(self, timeout: Optional[float] = None):
290
+ """
291
+ Close the CSV file and stop the worker thread.
292
+
293
+ :param timeout: Optional timeout in seconds to wait for remaining writes
294
+ """
295
+ with self._lock:
296
+ if not self._is_open:
297
+ return
298
+
299
+ # Signal the worker to stop
300
+ try:
301
+ self._queue.put((STOP, None), timeout=timeout)
302
+ except queue.Full:
303
+ pass # We'll stop anyway
304
+
305
+ # Wait for the worker to finish
306
+ if self._worker:
307
+ self._worker.join(timeout=timeout)
308
+ self._worker = None
309
+
310
+ # Close the file
311
+ if self._file:
312
+ self._file.close()
313
+ self._file = None
314
+
315
+ self._is_open = False
316
+
317
+ # Re-raise any worker thread error
318
+ if self._error:
319
+ raise RuntimeError(f"Writer thread error: {self._error}")
320
+
321
+
322
+ class CSVReader:
323
+ """
324
+ Simple CSV reader for OHLCV data with support for extra fields.
325
+ Sequential access only.
326
+ """
327
+
328
+ __slots__ = ('path', '_file', '_mmap', '_headers', '_dialect', '_has_headers',
329
+ '_field_indices', '_extra_fields', '_is_valid_ohlcv')
330
+
331
+ def __init__(self, path: Path):
332
+ self.path: Path = path
333
+ self._file: io.BufferedReader | None = None
334
+ self._headers: list[str] | None = None
335
+ self._dialect: type[csv.Dialect] | None = None
336
+ self._has_headers: bool = True
337
+ self._field_indices: dict[str, int] | None = None
338
+ self._extra_fields: dict[str, int] | None = None
339
+ self._mmap: mmap.mmap | None = None
340
+ self._is_valid_ohlcv: bool = False
341
+
342
+ def __enter__(self):
343
+ self.open()
344
+ return self
345
+
346
+ def __exit__(self, exc_type, exc_val, exc_tb):
347
+ self.close()
348
+
349
+ def open(self) -> CSVReader:
350
+ """Open the CSV file"""
351
+ # Open file in binary mode for memory mapping
352
+ file = open(self.path, 'rb')
353
+ self._file = file
354
+ mm = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ)
355
+ self._mmap = mm
356
+
357
+ # Read first line to detect CSV format and headers
358
+ first_line = mm.readline().decode('utf-8')
359
+
360
+ # Detect dialect
361
+ dialect = csv.Sniffer().sniff(first_line) # type: ignore
362
+ self._dialect = dialect
363
+
364
+ # Check if we have headers
365
+ self._has_headers = csv.Sniffer().has_header(first_line)
366
+
367
+ _is_tv = False
368
+ if self._has_headers:
369
+ # Parse headers
370
+ headers = next(csv.reader([first_line], dialect=dialect))
371
+ _is_tv = (headers[0] == 'time' and headers[1] == 'open' and headers[2] == 'high'
372
+ and headers[3] == 'low' and headers[4] == 'close')
373
+ else:
374
+ # Default headers for standard OHLCV
375
+ headers = ['time', 'open', 'high', 'low', 'close', 'volume']
376
+ self._headers = headers
377
+
378
+ # Reopen file to reset position
379
+ file.seek(0)
380
+
381
+ # Create case-insensitive header mapping
382
+ header_map = {h.lower(): i for i, h in enumerate(headers) if not _is_tv or i < 6 or h == "Volume"}
383
+
384
+ # Get field indices for OHLCV data with case-insensitive matching
385
+ try:
386
+ fi: dict[str, int] = {
387
+ # support both 'time' and 'timestamp'
388
+ 'time': header_map['time'] if 'time' in header_map else header_map['timestamp'],
389
+ # OHLCV fields
390
+ 'open': header_map['open'],
391
+ 'high': header_map['high'],
392
+ 'low': header_map['low'],
393
+ 'close': header_map['close'],
394
+ 'volume': header_map['volume']
395
+ }
396
+ self._is_valid_ohlcv = True
397
+ except KeyError:
398
+ fi = {
399
+ 'time': header_map.get('data/time', 0),
400
+ }
401
+ self._field_indices = fi
402
+
403
+ # Get extra field indices
404
+ self._extra_fields = {
405
+ name: idx for idx, name in enumerate(headers)
406
+ if idx not in fi.values()
407
+ }
408
+
409
+ return self
410
+
411
+ def _parse_extra_fields(self, row: list[str]) -> dict:
412
+ """Parse extra fields from a row"""
413
+ extra = {}
414
+ if self._extra_fields is None:
415
+ return extra
416
+
417
+ for name, idx in self._extra_fields.items():
418
+ name = name.replace('&quot;', '"') # Handle HTML quote entities
419
+ try:
420
+ value = row[idx]
421
+ if value == "NaN" or value == "na" or value == "nan":
422
+ extra[name] = NA()
423
+ else:
424
+ try:
425
+ # Try converting the value to an integer
426
+ extra[name] = int(value)
427
+ except ValueError:
428
+ try:
429
+ # Fallback to converting the value to a float
430
+ extra[name] = float(value)
431
+ except ValueError:
432
+ # Value is not a valid numeric representation
433
+ extra[name] = value
434
+ except (ValueError, IndexError):
435
+ continue
436
+ return extra
437
+
438
+ def _read_records(self, target_pos: Optional[int] = None) -> Iterator[tuple[int, OHLCV]]:
439
+ """
440
+ Internal method to read records, optionally stopping at target_pos.
441
+ Returns (position, candle) tuples.
442
+ """
443
+ if not self._file:
444
+ raise RuntimeError("File not opened!")
445
+ assert self._mmap is not None
446
+ assert self._field_indices is not None
447
+ assert self._dialect is not None
448
+ fi = self._field_indices
449
+ dialect = self._dialect
450
+
451
+ # Reset position
452
+ self._mmap.seek(0)
453
+
454
+ # Create a text IO wrapper for the mmap object
455
+ text_io = io.TextIOWrapper(io.BytesIO(self._mmap))
456
+ reader = csv.reader(text_io, dialect=dialect)
457
+
458
+ # Skip header if needed
459
+ if self._has_headers:
460
+ next(reader)
461
+
462
+ for pos, row in enumerate(reader):
463
+ # Stop if we reached target position
464
+ if target_pos is not None and pos > target_pos:
465
+ break
466
+
467
+ if not row: # Skip empty rows
468
+ continue
469
+
470
+ # Parse timestamp
471
+ time_field = row[fi['time']]
472
+ if time_field.isdigit():
473
+ timestamp = int(time_field)
474
+ else:
475
+ try:
476
+ dt = datetime.fromisoformat(time_field).astimezone(UTC)
477
+ timestamp = int(dt.timestamp())
478
+ except ValueError:
479
+ raise ValueError(f"Invalid time format: {time_field}")
480
+
481
+ # Create OHLCV object
482
+ try:
483
+ if self._is_valid_ohlcv:
484
+ candle = OHLCV(
485
+ timestamp=timestamp,
486
+ open=float(row[fi['open']]),
487
+ high=float(row[fi['high']]),
488
+ low=float(row[fi['low']]),
489
+ close=float(row[fi['close']]),
490
+ volume=float(row[fi['volume']]),
491
+ extra_fields=self._parse_extra_fields(row)
492
+ )
493
+ else:
494
+ candle = OHLCV(
495
+ timestamp=timestamp,
496
+ open=float('nan'),
497
+ high=float('nan'),
498
+ low=float('nan'),
499
+ close=float('nan'),
500
+ volume=float('nan'),
501
+ extra_fields=self._parse_extra_fields(row)
502
+ )
503
+
504
+ except (ValueError, IndexError) as e:
505
+ raise ValueError(f"Invalid data in row {pos + 1}: {e}")
506
+
507
+ yield pos, candle
508
+
509
+ def read(self, position: int) -> OHLCV:
510
+ """
511
+ Read a single candle at given position.
512
+ Must read sequentially from the start to reach the position.
513
+ """
514
+ if position < 0:
515
+ raise IndexError("Position cannot be negative")
516
+
517
+ for pos, candle in self._read_records(position):
518
+ if pos == position:
519
+ return candle
520
+
521
+ raise IndexError("Position out of range")
522
+
523
+ def read_from(self, start_timestamp: int, end_timestamp: int | None = None) -> Iterator[OHLCV]:
524
+ """
525
+ Read bars starting from timestamp.
526
+ Must read sequentially until finding matching timestamps.
527
+ """
528
+ for _, candle in self._read_records():
529
+ if candle.timestamp >= start_timestamp:
530
+ if end_timestamp is None or candle.timestamp <= end_timestamp:
531
+ yield candle
532
+ else:
533
+ break
534
+
535
+ def __iter__(self) -> Iterator[OHLCV]:
536
+ """Iterate through all candles"""
537
+ for _, candle in self._read_records():
538
+ yield candle
539
+
540
+ def close(self):
541
+ """Close file and memory mapping"""
542
+ if self._mmap:
543
+ self._mmap.close()
544
+ self._mmap = None
545
+ if self._file:
546
+ self._file.close()
547
+ self._file = None