opencode-pyneruntime 6.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- pynecore/utils/stdlib_checker.py +17 -0
|
@@ -0,0 +1,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('"', '"') # 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
|