Qubx 0.1.3__tar.gz → 0.1.5__tar.gz

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.

Potentially problematic release.


This version of Qubx might be problematic. Click here for more details.

Files changed (38) hide show
  1. {qubx-0.1.3 → qubx-0.1.5}/PKG-INFO +1 -1
  2. {qubx-0.1.3 → qubx-0.1.5}/pyproject.toml +1 -1
  3. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/_nb_magic.py +4 -0
  4. qubx-0.1.5/src/qubx/data/readers.py +513 -0
  5. qubx-0.1.5/src/qubx/pandaz/__init__.py +4 -0
  6. qubx-0.1.5/src/qubx/pandaz/ta.py +2527 -0
  7. qubx-0.1.3/src/qubx/utils/pandas.py → qubx-0.1.5/src/qubx/pandaz/utils.py +15 -2
  8. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/utils/marketdata/binance.py +1 -1
  9. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/utils/misc.py +1 -1
  10. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/utils/runner.py +2 -1
  11. qubx-0.1.3/src/qubx/data/readers.py +0 -515
  12. {qubx-0.1.3 → qubx-0.1.5}/README.md +0 -0
  13. {qubx-0.1.3 → qubx-0.1.5}/build.py +0 -0
  14. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/__init__.py +0 -0
  15. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/__init__.py +0 -0
  16. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/account.py +0 -0
  17. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/basics.py +0 -0
  18. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/helpers.py +0 -0
  19. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/loggers.py +0 -0
  20. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/lookups.py +0 -0
  21. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/series.pxd +0 -0
  22. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/series.pyx +0 -0
  23. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/strategy.py +0 -0
  24. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/core/utils.pyx +0 -0
  25. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/impl/ccxt_connector.py +0 -0
  26. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/impl/ccxt_customizations.py +0 -0
  27. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/impl/ccxt_trading.py +0 -0
  28. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/impl/ccxt_utils.py +0 -0
  29. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/math/__init__.py +0 -0
  30. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/math/stats.py +0 -0
  31. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/ta/__init__.py +0 -0
  32. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/ta/indicators.pyx +0 -0
  33. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/trackers/__init__.py +0 -0
  34. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/trackers/rebalancers.py +0 -0
  35. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/utils/__init__.py +0 -0
  36. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/utils/_pyxreloader.py +0 -0
  37. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  38. {qubx-0.1.3 → qubx-0.1.5}/src/qubx/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Qubx
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Qubx - quantitative trading framework
5
5
  Home-page: https://github.com/dmarienko/Qubx
6
6
  Author: Dmitry Marienko
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Qubx"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  description = "Qubx - quantitative trading framework"
5
5
  authors = ["Dmitry Marienko <dmitry@gmail.com>"]
6
6
  readme = "README.md"
@@ -32,13 +32,17 @@ if runtime_env() in ['notebook', 'shell']:
32
32
  from tqdm.auto import tqdm
33
33
 
34
34
  # - - - - TA stuff and indicators - - - -
35
+ import qubx.pandaz.ta as pta
36
+
35
37
  # - - - - Portfolio analysis - - - -
36
38
  # - - - - Simulator stuff - - - -
37
39
  # - - - - Learn stuff - - - -
38
40
  # - - - - Charting stuff - - - -
39
41
  from matplotlib import pyplot as plt
40
42
  from qubx.utils.charting.mpl_helpers import fig, subplot, sbp
43
+
41
44
  # - - - - Utils - - - -
45
+ from qubx.pandaz.utils import scols, srows, ohlc_resample, continuous_periods, generate_equal_date_ranges
42
46
 
43
47
  # - setup short numpy output format
44
48
  np_fmt_short()
@@ -0,0 +1,513 @@
1
+ import re, os
2
+ from typing import Callable, List, Union, Optional, Iterable, Any
3
+ from os.path import exists, join
4
+ import numpy as np
5
+ import pandas as pd
6
+ import pyarrow as pa
7
+ from pyarrow import csv
8
+ import psycopg as pg
9
+ from functools import wraps
10
+
11
+ from qubx import logger
12
+ from qubx.core.series import TimeSeries, OHLCV, time_as_nsec, Quote, Trade
13
+ from qubx.utils.time import infer_series_frequency, handle_start_stop
14
+
15
+ _DT = lambda x: pd.Timedelta(x).to_numpy().item()
16
+ D1, H1 = _DT('1D'), _DT('1h')
17
+
18
+ DEFAULT_DAILY_SESSION = (_DT('00:00:00.100'), _DT('23:59:59.900'))
19
+ STOCK_DAILY_SESSION = (_DT('9:30:00.100'), _DT('15:59:59.900'))
20
+ CME_FUTURES_DAILY_SESSION = (_DT('8:30:00.100'), _DT('15:14:59.900'))
21
+
22
+
23
+ def _recognize_t(t: Union[int, str], defaultvalue, timeunit) -> int:
24
+ if isinstance(t, (str, pd.Timestamp)):
25
+ try:
26
+ return np.datetime64(t, timeunit)
27
+ except:
28
+ pass
29
+ return defaultvalue
30
+
31
+
32
+ def _find_column_index_in_list(xs, *args):
33
+ xs = [x.lower() for x in xs]
34
+ for a in args:
35
+ ai = a.lower()
36
+ if ai in xs:
37
+ return xs.index(ai)
38
+ raise IndexError(f"Can't find any from {args} in list: {xs}")
39
+
40
+
41
+ class DataTransformer:
42
+
43
+ def __init__(self) -> None:
44
+ self.buffer = []
45
+ self._column_names = []
46
+
47
+ def start_transform(self, name: str, column_names: List[str]):
48
+ self._column_names = column_names
49
+ self.buffer = []
50
+
51
+ def process_data(self, rows_data: Iterable) -> Any:
52
+ if rows_data is not None:
53
+ self.buffer.extend(rows_data)
54
+
55
+ def collect(self) -> Any:
56
+ return self.buffer
57
+
58
+
59
+ class DataReader:
60
+
61
+ def get_names(self) -> List[str] :
62
+ raise NotImplemented()
63
+
64
+ def read(self, data_id: str, start: str | None=None, stop: str | None=None,
65
+ transform: DataTransformer = DataTransformer(),
66
+ chunksize=0,
67
+ **kwargs
68
+ ) -> Iterable | List:
69
+ raise NotImplemented()
70
+
71
+
72
+ class CsvStorageDataReader(DataReader):
73
+ """
74
+ Data reader for timeseries data stored as csv files in the specified directory
75
+ """
76
+
77
+ def __init__(self, path: str) -> None:
78
+ if not exists(path):
79
+ raise ValueError(f"Folder is not found at {path}")
80
+ self.path = path
81
+
82
+ def __find_time_idx(self, arr: pa.ChunkedArray, v) -> int:
83
+ ix = arr.index(v).as_py()
84
+ if ix < 0:
85
+ for c in arr.iterchunks():
86
+ a = c.to_numpy()
87
+ ix = np.searchsorted(a, v, side='right')
88
+ if ix > 0 and ix < len(c):
89
+ ix = arr.index(a[ix]).as_py() - 1
90
+ break
91
+ return ix
92
+
93
+ def __check_file_name(self, name: str) -> str | None:
94
+ _f = join(self.path, name)
95
+ for sfx in ['.csv', '.csv.gz', '']:
96
+ if exists(p:=(_f + sfx)):
97
+ return p
98
+ return None
99
+
100
+ def read(self, data_id: str, start: str | None=None, stop: str | None=None,
101
+ transform: DataTransformer = DataTransformer(),
102
+ chunksize=0,
103
+ timestamp_formatters = None
104
+ ) -> Iterable | Any:
105
+
106
+ f_path = self.__check_file_name(data_id)
107
+ if not f_path:
108
+ ValueError(f"Can't find any csv data for {data_id} in {self.path} !")
109
+
110
+ convert_options = None
111
+ if timestamp_formatters is not None:
112
+ convert_options=csv.ConvertOptions(timestamp_parsers=timestamp_formatters)
113
+
114
+ table = csv.read_csv(
115
+ f_path,
116
+ parse_options=csv.ParseOptions(ignore_empty_lines=True),
117
+ convert_options=convert_options
118
+ )
119
+ fieldnames = table.column_names
120
+
121
+ # - try to find range to load
122
+ start_idx, stop_idx = 0, table.num_rows
123
+ try:
124
+ _time_field_idx = _find_column_index_in_list(fieldnames, 'time', 'timestamp', 'datetime', 'date')
125
+ _time_type = table.field(_time_field_idx).type
126
+ _time_unit = _time_type.unit if hasattr(_time_type, 'unit') else 's'
127
+ _time_data = table[_time_field_idx]
128
+
129
+ # - check if need convert time to primitive types (i.e. Date32 -> timestamp[x])
130
+ _time_cast_function = lambda xs: xs
131
+ if _time_type != pa.timestamp(_time_unit):
132
+ _time_cast_function = lambda xs: xs.cast(pa.timestamp(_time_unit))
133
+ _time_data = _time_cast_function(_time_data)
134
+
135
+ # - preprocessing start and stop
136
+ t_0, t_1 = handle_start_stop(start, stop, convert=lambda x: _recognize_t(x, None, _time_unit))
137
+
138
+ # - check requested range
139
+ if t_0:
140
+ start_idx = self.__find_time_idx(_time_data, t_0)
141
+ if start_idx >= table.num_rows:
142
+ # no data for requested start date
143
+ return None
144
+
145
+ if t_1:
146
+ stop_idx = self.__find_time_idx(_time_data, t_1)
147
+ if stop_idx < 0 or stop_idx < start_idx:
148
+ stop_idx = table.num_rows
149
+
150
+ except Exception as exc:
151
+ logger.warning(exc)
152
+ logger.info('loading whole file')
153
+
154
+ length = (stop_idx - start_idx + 1)
155
+ selected_table = table.slice(start_idx, length)
156
+
157
+ # - in this case we want to return iterable chunks of data
158
+ if chunksize > 0:
159
+ def _iter_chunks():
160
+ for n in range(0, length // chunksize + 1):
161
+ transform.start_transform(data_id, fieldnames)
162
+ raw_data = selected_table[n*chunksize : min((n+1)*chunksize, length)].to_pandas().to_numpy()
163
+ transform.process_data(raw_data)
164
+ yield transform.collect()
165
+ return _iter_chunks()
166
+
167
+ transform.start_transform(data_id, fieldnames)
168
+ raw_data = selected_table.to_pandas().to_numpy()
169
+ transform.process_data(raw_data)
170
+ return transform.collect()
171
+
172
+ def get_names(self) -> List[str] :
173
+ _n = []
174
+ for s in os.listdir(self.path):
175
+ if (m:=re.match(r'(.*)\.csv(.gz)?$', s)):
176
+ _n.append(m.group(1))
177
+ return _n
178
+
179
+
180
+ class AsPandasFrame(DataTransformer):
181
+ """
182
+ List of records to pandas dataframe transformer
183
+ """
184
+
185
+ def start_transform(self, name: str, column_names: List[str]):
186
+ self._time_idx = _find_column_index_in_list(column_names, 'time', 'timestamp', 'datetime', 'date')
187
+ self._column_names = column_names
188
+ self._frame = pd.DataFrame()
189
+
190
+ def process_data(self, rows_data: Iterable) -> Any:
191
+ self._frame
192
+ p = pd.DataFrame.from_records(rows_data, columns=self._column_names)
193
+ p.set_index(self._column_names[self._time_idx], drop=True, inplace=True)
194
+ p.sort_index(inplace=True)
195
+ self._frame = pd.concat((self._frame, p), axis=0, sort=True)
196
+ return p
197
+
198
+ def collect(self) -> Any:
199
+ return self._frame
200
+
201
+
202
+ class AsOhlcvSeries(DataTransformer):
203
+
204
+ def __init__(self, timeframe: str | None = None, timestamp_units='ns') -> None:
205
+ super().__init__()
206
+ self.timeframe = timeframe
207
+ self._series = None
208
+ self._data_type = None
209
+ self.timestamp_units = timestamp_units
210
+
211
+ def start_transform(self, name: str, column_names: List[str]):
212
+ self._time_idx = _find_column_index_in_list(column_names, 'time', 'timestamp', 'datetime', 'date')
213
+ self._volume_idx = None
214
+ self._b_volume_idx = None
215
+ try:
216
+ self._close_idx = _find_column_index_in_list(column_names, 'close')
217
+ self._open_idx = _find_column_index_in_list(column_names, 'open')
218
+ self._high_idx = _find_column_index_in_list(column_names, 'high')
219
+ self._low_idx = _find_column_index_in_list(column_names, 'low')
220
+
221
+ try:
222
+ self._volume_idx = _find_column_index_in_list(column_names, 'quote_volume', 'volume', 'vol')
223
+ except: pass
224
+
225
+ try:
226
+ self._b_volume_idx = _find_column_index_in_list(column_names, 'taker_buy_volume', 'taker_buy_quote_volume', 'buy_volume')
227
+ except: pass
228
+
229
+ self._data_type = 'ohlc'
230
+ except:
231
+ try:
232
+ self._ask_idx = _find_column_index_in_list(column_names, 'ask')
233
+ self._bid_idx = _find_column_index_in_list(column_names, 'bid')
234
+ self._data_type = 'quotes'
235
+ except:
236
+
237
+ try:
238
+ self._price_idx = _find_column_index_in_list(column_names, 'price')
239
+ self._size_idx = _find_column_index_in_list(column_names, 'quote_qty', 'qty', 'size', 'amount', 'volume')
240
+ self._taker_idx = None
241
+ try:
242
+ self._taker_idx = _find_column_index_in_list(column_names, 'is_buyer_maker', 'side', 'aggressive', 'taker', 'is_taker')
243
+ except: pass
244
+
245
+ self._data_type = 'trades'
246
+ except:
247
+ raise ValueError(f"Can't recognize data for update from header: {column_names}")
248
+
249
+ self._column_names = column_names
250
+ self._name = name
251
+ if self.timeframe:
252
+ self._series = OHLCV(self._name, self.timeframe)
253
+
254
+ def _time(self, t) -> int:
255
+ if self.timestamp_units == 'ns':
256
+ return np.datetime64(t, 'ns').item()
257
+ return np.datetime64(t, self.timestamp_units).astype('datetime64[ns]').item()
258
+
259
+ def _proc_ohlc(self, rows_data: List[List]):
260
+ for d in rows_data:
261
+ self._series.update_by_bar(
262
+ self._time(d[self._time_idx]),
263
+ d[self._open_idx], d[self._high_idx], d[self._low_idx], d[self._close_idx],
264
+ d[self._volume_idx] if self._volume_idx else 0,
265
+ d[self._b_volume_idx] if self._b_volume_idx else 0
266
+ )
267
+
268
+ def _proc_quotes(self, rows_data: List[List]):
269
+ for d in rows_data:
270
+ self._series.update(
271
+ self._time(d[self._time_idx]),
272
+ (d[self._ask_idx] + d[self._bid_idx])/2
273
+ )
274
+
275
+ def _proc_trades(self, rows_data: List[List]):
276
+ for d in rows_data:
277
+ a = d[self._taker_idx] if self._taker_idx else 0
278
+ s = d[self._size_idx]
279
+ b = s if a else 0
280
+ self._series.update(self._time(d[self._time_idx]), d[self._price_idx], s, b)
281
+
282
+ def process_data(self, rows_data: List[List]) -> Any:
283
+ if self._series is None:
284
+ ts = [t[self._time_idx] for t in rows_data[:100]]
285
+ self.timeframe = pd.Timedelta(infer_series_frequency(ts)).asm8.item()
286
+
287
+ # - create instance after first data received if
288
+ self._series = OHLCV(self._name, self.timeframe)
289
+
290
+ match self._data_type:
291
+ case 'ohlc':
292
+ self._proc_ohlc(rows_data)
293
+ case 'quotes':
294
+ self._proc_quotes(rows_data)
295
+ case 'trades':
296
+ self._proc_trades(rows_data)
297
+
298
+ return None
299
+
300
+ def collect(self) -> Any:
301
+ return self._series
302
+
303
+
304
+ class AsQuotes(DataTransformer):
305
+
306
+ def start_transform(self, name: str, column_names: List[str]):
307
+ self.buffer = list()
308
+ self._time_idx = _find_column_index_in_list(column_names, 'time', 'timestamp', 'datetime')
309
+ self._bid_idx = _find_column_index_in_list(column_names, 'bid')
310
+ self._ask_idx = _find_column_index_in_list(column_names, 'ask')
311
+ self._bidvol_idx = _find_column_index_in_list(column_names, 'bidvol', 'bid_vol', 'bidsize', 'bid_size')
312
+ self._askvol_idx = _find_column_index_in_list(column_names, 'askvol', 'ask_vol', 'asksize', 'ask_size')
313
+
314
+ def process_data(self, rows_data: Iterable) -> Any:
315
+ if rows_data is not None:
316
+ for d in rows_data:
317
+ t = d[self._time_idx]
318
+ b = d[self._bid_idx]
319
+ a = d[self._ask_idx]
320
+ bv = d[self._bidvol_idx]
321
+ av = d[self._askvol_idx]
322
+ self.buffer.append(Quote(t.as_unit('ns').asm8.item(), b, a, bv, av))
323
+
324
+
325
+ class RestoreTicksFromOHLC(DataTransformer):
326
+ """
327
+ Emulates quotes (and trades) from OHLC bars
328
+ """
329
+
330
+ def __init__(self,
331
+ trades: bool=False, # if we also wants 'trades'
332
+ default_bid_size=1e9, # default bid/ask is big
333
+ default_ask_size=1e9, # default bid/ask is big
334
+ daily_session_start_end=DEFAULT_DAILY_SESSION,
335
+ spread=0.0):
336
+ super().__init__()
337
+ self._trades = trades
338
+ self._bid_size = default_bid_size
339
+ self._ask_size = default_ask_size
340
+ self._s2 = spread / 2.0
341
+ self._d_session_start = daily_session_start_end[0]
342
+ self._d_session_end = daily_session_start_end[1]
343
+
344
+ def start_transform(self, name: str, column_names: List[str]):
345
+ self.buffer = []
346
+ # - it will fail if receive data doesn't look as ohlcv
347
+ self._time_idx = _find_column_index_in_list(column_names, 'time', 'timestamp', 'datetime', 'date')
348
+ self._open_idx = _find_column_index_in_list(column_names, 'open')
349
+ self._high_idx = _find_column_index_in_list(column_names, 'high')
350
+ self._low_idx = _find_column_index_in_list(column_names, 'low')
351
+ self._close_idx = _find_column_index_in_list(column_names, 'close')
352
+ self._volume_idx = None
353
+ self._freq = None
354
+ try:
355
+ self._volume_idx = _find_column_index_in_list(column_names, 'volume', 'vol')
356
+ except: pass
357
+
358
+ if self._volume_idx is None and self._trades:
359
+ logger.warning("Input OHLC data doesn't contain volume information so trades can't be emulated !")
360
+ self._trades = False
361
+
362
+ def process_data(self, rows_data:List[List]) -> Any:
363
+ if rows_data is None:
364
+ return
365
+
366
+ s2 = self._s2
367
+
368
+ if self._freq is None:
369
+ ts = [t[self._time_idx] for t in rows_data[:100]]
370
+ self._freq = infer_series_frequency(ts)
371
+
372
+ # - timestamps when we emit simulated quotes
373
+ dt = self._freq.astype('timedelta64[ns]').item()
374
+ if dt < D1:
375
+ self._t_start = dt // 10
376
+ self._t_mid1 = dt // 2 - dt // 10
377
+ self._t_mid2 = dt // 2 + dt // 10
378
+ self._t_end = dt - dt // 10
379
+ else:
380
+ self._t_start = self._d_session_start
381
+ self._t_mid1 = dt // 2 - H1
382
+ self._t_mid2 = dt // 2 + H1
383
+ self._t_end = self._d_session_end
384
+
385
+ # - input data
386
+ for data in rows_data:
387
+ ti = pd.Timestamp(data[self._time_idx]).as_unit('ns').asm8.item()
388
+ o = data[self._open_idx]
389
+ h= data[self._high_idx]
390
+ l = data[self._low_idx]
391
+ c = data[self._close_idx]
392
+ rv = data[self._volume_idx] if self._volume_idx else 0
393
+
394
+ # - opening quote
395
+ self.buffer.append(Quote(ti + self._t_start, o - s2, o + s2, self._bid_size, self._ask_size))
396
+
397
+ if c >= o:
398
+ if self._trades:
399
+ self.buffer.append(Trade(ti + self._t_start, o - s2, rv * (o - l))) # sell 1
400
+ self.buffer.append(Quote(ti + self._t_mid1, l - s2, l + s2, self._bid_size, self._ask_size))
401
+
402
+ if self._trades:
403
+ self.buffer.append(Trade(ti + self._t_mid1, l + s2, rv * (c - o))) # buy 1
404
+ self.buffer.append(Quote(ti + self._t_mid2, h - s2, h + s2, self._bid_size, self._ask_size))
405
+
406
+ if self._trades:
407
+ self.buffer.append(Trade(ti + self._t_mid2, h - s2, rv * (h - c))) # sell 2
408
+ else:
409
+ if self._trades:
410
+ self.buffer.append(Trade(ti + self._t_start, o + s2, rv * (h - o))) # buy 1
411
+ self.buffer.append(Quote(ti + self._t_mid1, h - s2, h + s2, self._bid_size, self._ask_size))
412
+
413
+ if self._trades:
414
+ self.buffer.append(Trade(ti + self._t_mid1, h - s2, rv * (o - c))) # sell 1
415
+ self.buffer.append(Quote(ti + self._t_mid2, l - s2, l + s2, self._bid_size, self._ask_size))
416
+
417
+ if self._trades:
418
+ self.buffer.append(Trade(ti + self._t_mid2, l + s2, rv * (c - l))) # buy 2
419
+
420
+ # - closing quote
421
+ self.buffer.append(Quote(ti + self._t_end, c - s2, c + s2, self._bid_size, self._ask_size))
422
+
423
+
424
+ def _retry(fn):
425
+ @wraps(fn)
426
+ def wrapper(*args, **kw):
427
+ cls = args[0]
428
+ for x in range(cls._reconnect_tries):
429
+ # print(x, cls._reconnect_tries)
430
+ try:
431
+ return fn(*args, **kw)
432
+ except (pg.InterfaceError, pg.OperationalError) as e:
433
+ logger.warning("Database Connection [InterfaceError or OperationalError]")
434
+ # print ("Idle for %s seconds" % (cls._reconnect_idle))
435
+ # time.sleep(cls._reconnect_idle)
436
+ cls._connect()
437
+ return wrapper
438
+
439
+
440
+ class QuestDBConnector(DataReader):
441
+ """
442
+ Very first version of QuestDB connector
443
+
444
+ # Connect to an existing QuestDB instance
445
+ >>> db = QuestDBConnector('user=admin password=quest host=localhost port=8812', OhlcvPandasDataProcessor())
446
+ >>> db.read('BINANCEF.ETHUSDT', '2024-01-01')
447
+ """
448
+ _reconnect_tries = 5
449
+ _reconnect_idle = 0.1 # wait seconds before retying
450
+
451
+ def __init__(self, connection_url: str) -> None:
452
+ self._connection = None
453
+ self._cursor = None
454
+ self.connection_url = connection_url
455
+ self._connect()
456
+
457
+ def _connect(self):
458
+ logger.info("Connecting to QuestDB ...")
459
+ self._connection = pg.connect(self.connection_url, autocommit=True)
460
+ self._cursor = self._connection.cursor()
461
+
462
+ @_retry
463
+ def read(self, data_id: str, start: str|None=None, stop: str|None=None,
464
+ transform: DataTransformer = DataTransformer(),
465
+ chunksize=0, # TODO: use self._cursor.fetchmany in this case !!!!
466
+ timeframe: str='1m') -> Any:
467
+ start, end = handle_start_stop(start, stop)
468
+ w0 = f"timestamp >= '{start}'" if start else ''
469
+ w1 = f"timestamp <= '{end}'" if end else ''
470
+ where = f'where {w0} and {w1}' if (w0 and w1) else f"where {(w0 or w1)}"
471
+
472
+ # just a temp hack - actually we need to discuss symbology etc
473
+ symbol = data_id#.split('.')[-1]
474
+
475
+ self._cursor.execute(
476
+ f"""
477
+ select timestamp,
478
+ first(open) as open,
479
+ max(high) as high,
480
+ min(low) as low,
481
+ last(close) as close,
482
+ sum(volume) as volume,
483
+ sum(quote_volume) as quote_volume,
484
+ sum(count) as count,
485
+ sum(taker_buy_volume) as taker_buy_volume,
486
+ sum(taker_buy_quote_volume) as taker_buy_quote_volume
487
+ from "{symbol.upper()}" {where}
488
+ SAMPLE by {timeframe};
489
+ """ # type: ignore
490
+ )
491
+ records = self._cursor.fetchall() # TODO: for chunksize > 0 use fetchmany etc
492
+ names = [d.name for d in self._cursor.description]
493
+
494
+ transform.start_transform(data_id, names)
495
+
496
+ # d = np.array(records)
497
+ transform.process_data(records)
498
+ return transform.collect()
499
+
500
+ @_retry
501
+ def get_names(self) -> List[str] :
502
+ self._cursor.execute("select table_name from tables()")
503
+ records = self._cursor.fetchall()
504
+ return [r[0] for r in records]
505
+
506
+ def __del__(self):
507
+ for c in (self._cursor, self._connection):
508
+ try:
509
+ logger.info("Closing connection")
510
+ c.close()
511
+ except:
512
+ pass
513
+
@@ -0,0 +1,4 @@
1
+ from .utils import (
2
+ srows, scols, continuous_periods, ohlc_resample, retain_columns_and_join, dict_to_frame,
3
+ generate_equal_date_ranges, rolling_forward_test_split
4
+ )