Qubx 0.1.83__cp311-cp311-manylinux_2_35_x86_64.whl → 0.1.85__cp311-cp311-manylinux_2_35_x86_64.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.

Potentially problematic release.


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

qubx/__init__.py CHANGED
@@ -10,13 +10,20 @@ def formatter(record):
10
10
  end = record["extra"].get("end", "\n")
11
11
  fmt = "<lvl>{message}</lvl>%s" % end
12
12
  if record["level"].name in {"WARNING", "SNAKY"}:
13
- fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
13
+ fmt = (
14
+ "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
15
+ )
14
16
 
15
- prefix = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] " % record["level"].icon
17
+ prefix = (
18
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] "
19
+ % record["level"].icon
20
+ )
16
21
 
17
22
  if record["exception"] is not None:
18
23
  # stackprinter.set_excepthook(style='darkbg2')
19
- record["extra"]["stack"] = stackprinter.format(record["exception"], style="darkbg")
24
+ record["extra"]["stack"] = stackprinter.format(
25
+ record["exception"], style="darkbg"
26
+ )
20
27
  fmt += "\n{extra[stack]}\n"
21
28
 
22
29
  if record["level"].name in {"TEXT"}:
@@ -25,24 +32,43 @@ def formatter(record):
25
32
  return prefix + fmt
26
33
 
27
34
 
28
- config = {
29
- "handlers": [ {"sink": sys.stdout, "format": "{time} - {message}"}, ],
30
- "extra": {"user": "someone"},
31
- }
35
+ class QubxLogConfig:
32
36
 
37
+ @staticmethod
38
+ def get_log_level():
39
+ return os.getenv("QUBX_LOG_LEVEL", "DEBUG")
40
+
41
+ @staticmethod
42
+ def set_log_level(level: str):
43
+ os.environ["QUBX_LOG_LEVEL"] = level
44
+ QubxLogConfig.setup_logger(level)
45
+
46
+ @staticmethod
47
+ def setup_logger(level: str | None = None):
48
+ global logger
49
+ config = {
50
+ "handlers": [
51
+ {"sink": sys.stdout, "format": "{time} - {message}"},
52
+ ],
53
+ "extra": {"user": "someone"},
54
+ }
55
+ logger.configure(**config)
56
+ logger.remove(None)
57
+ level = level or QubxLogConfig.get_log_level()
58
+ logger.add(sys.stdout, format=formatter, colorize=True, level=level)
59
+ logger = logger.opt(colors=True)
60
+
61
+
62
+ QubxLogConfig.setup_logger()
33
63
 
34
- logger.configure(**config)
35
- logger.remove(None)
36
- logger.add(sys.stdout, format=formatter, colorize=True)
37
- logger = logger.opt(colors=True)
38
64
 
39
65
  # - global lookup helper
40
66
  lookup = GlobalLookup(InstrumentsLookup(), FeesLookup())
41
67
 
42
68
 
43
69
  # registering magic for jupyter notebook
44
- if runtime_env() in ['notebook', 'shell']:
45
- from IPython.core.magic import (Magics, magics_class, line_magic, line_cell_magic)
70
+ if runtime_env() in ["notebook", "shell"]:
71
+ from IPython.core.magic import Magics, magics_class, line_magic, line_cell_magic
46
72
  from IPython import get_ipython
47
73
 
48
74
  @magics_class
@@ -52,11 +78,11 @@ if runtime_env() in ['notebook', 'shell']:
52
78
 
53
79
  @line_magic
54
80
  def qubxd(self, line: str):
55
- self.qubx_setup('dark' + ' ' + line)
81
+ self.qubx_setup("dark" + " " + line)
56
82
 
57
83
  @line_magic
58
84
  def qubxl(self, line: str):
59
- self.qubx_setup('light' + ' ' + line)
85
+ self.qubx_setup("light" + " " + line)
60
86
 
61
87
  @line_magic
62
88
  def qubx_setup(self, line: str):
@@ -64,25 +90,26 @@ if runtime_env() in ['notebook', 'shell']:
64
90
  QUBX framework initialization
65
91
  """
66
92
  import os
67
- args = [x.strip() for x in line.split(' ')]
68
-
93
+
94
+ args = [x.strip() for x in line.split(" ")]
95
+
69
96
  # setup cython dev hooks - only if 'dev' is passed as argument
70
- if line and 'dev' in args:
97
+ if line and "dev" in args:
71
98
  install_pyx_recompiler_for_dev()
72
99
 
73
100
  tpl_path = os.path.join(os.path.dirname(__file__), "_nb_magic.py")
74
- with open(tpl_path, 'r', encoding="utf8") as myfile:
101
+ with open(tpl_path, "r", encoding="utf8") as myfile:
75
102
  s = myfile.read()
76
103
 
77
104
  exec(s, self.shell.user_ns)
78
105
 
79
106
  # setup more funcy mpl theme instead of ugly default
80
107
  if line:
81
- if 'dark' in line.lower():
82
- set_mpl_theme('dark')
108
+ if "dark" in line.lower():
109
+ set_mpl_theme("dark")
83
110
 
84
- elif 'light' in line.lower():
85
- set_mpl_theme('light')
111
+ elif "light" in line.lower():
112
+ set_mpl_theme("light")
86
113
 
87
114
  # install additional plotly helpers
88
115
  # from qube.charting.plot_helpers import install_plotly_helpers
@@ -91,6 +118,7 @@ if runtime_env() in ['notebook', 'shell']:
91
118
  def _get_manager(self):
92
119
  if self.__manager is None:
93
120
  import multiprocessing as m
121
+
94
122
  self.__manager = m.Manager()
95
123
  return self.__manager
96
124
 
@@ -102,7 +130,7 @@ if runtime_env() in ['notebook', 'shell']:
102
130
  >>> %%proc x, y as MyProc1
103
131
  >>> x.set('Hello')
104
132
  >>> y.set([1,2,3,4])
105
-
133
+
106
134
  """
107
135
  import multiprocessing as m
108
136
  import time, re
@@ -111,8 +139,8 @@ if runtime_env() in ['notebook', 'shell']:
111
139
  name = None
112
140
  if line:
113
141
  # check if custom process name was provided
114
- if ' as ' in line:
115
- line, name = line.split('as')
142
+ if " as " in line:
143
+ line, name = line.split("as")
116
144
  if not name.isspace():
117
145
  name = name.strip()
118
146
  else:
@@ -120,11 +148,11 @@ if runtime_env() in ['notebook', 'shell']:
120
148
  return
121
149
 
122
150
  ipy = get_ipython()
123
- for a in [x for x in re.split('[\ ,;]', line.strip()) if x]:
151
+ for a in [x for x in re.split("[\ ,;]", line.strip()) if x]:
124
152
  ipy.push({a: self._get_manager().Value(None, None)})
125
153
 
126
154
  # code to run
127
- lines = '\n'.join([' %s' % x for x in cell.split('\n')])
155
+ lines = "\n".join([" %s" % x for x in cell.split("\n")])
128
156
 
129
157
  def fn():
130
158
  result = get_ipython().run_cell(lines)
@@ -136,17 +164,18 @@ if runtime_env() in ['notebook', 'shell']:
136
164
  if result.error_in_exec:
137
165
  raise result.error_in_exec
138
166
 
139
- t_start = str(time.time()).replace('.', '_')
140
- f_id = f'proc_{t_start}' if name is None else name
167
+ t_start = str(time.time()).replace(".", "_")
168
+ f_id = f"proc_{t_start}" if name is None else name
141
169
  if self._is_task_name_already_used(f_id):
142
170
  f_id = f"{f_id}_{t_start}"
143
171
 
144
172
  task = m.Process(target=fn, name=f_id)
145
173
  task.start()
146
- print(' -> Task %s is started' % f_id)
174
+ print(" -> Task %s is started" % f_id)
147
175
 
148
176
  def _is_task_name_already_used(self, name):
149
177
  import multiprocessing as m
178
+
150
179
  for p in m.active_children():
151
180
  if p.name == name:
152
181
  return True
@@ -155,16 +184,17 @@ if runtime_env() in ['notebook', 'shell']:
155
184
  @line_magic
156
185
  def list_proc(self, line):
157
186
  import multiprocessing as m
187
+
158
188
  for p in m.active_children():
159
189
  print(p.name)
160
190
 
161
191
  @line_magic
162
192
  def kill_proc(self, line):
163
193
  import multiprocessing as m
194
+
164
195
  for p in m.active_children():
165
196
  if line and p.name.startswith(line):
166
197
  p.terminate()
167
198
 
168
-
169
199
  # - registering magic here
170
200
  get_ipython().register_magics(QubxMagics)
qubx/_nb_magic.py CHANGED
@@ -1,7 +1,6 @@
1
1
  """"
2
2
  Here stuff we want to have in every Jupyter notebook after calling %qube magic
3
3
  """
4
- import importlib_metadata
5
4
 
6
5
  import qubx
7
6
  from qubx.utils import runtime_env
@@ -15,11 +14,19 @@ def np_fmt_short():
15
14
 
16
15
  def np_fmt_reset():
17
16
  # reset default np printing options
18
- np.set_printoptions(edgeitems=3, infstr='inf', linewidth=75, nanstr='nan', precision=8,
19
- suppress=False, threshold=1000, formatter=None)
17
+ np.set_printoptions(
18
+ edgeitems=3,
19
+ infstr="inf",
20
+ linewidth=75,
21
+ nanstr="nan",
22
+ precision=8,
23
+ suppress=False,
24
+ threshold=1000,
25
+ formatter=None,
26
+ )
20
27
 
21
28
 
22
- if runtime_env() in ['notebook', 'shell']:
29
+ if runtime_env() in ["notebook", "shell"]:
23
30
 
24
31
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
25
32
  # -- all imports below will appear in notebook after calling %%alphalab magic ---
@@ -39,19 +46,18 @@ if runtime_env() in ['notebook', 'shell']:
39
46
  # - - - - Learn stuff - - - -
40
47
  # - - - - Charting stuff - - - -
41
48
  from matplotlib import pyplot as plt
42
- from qubx.utils.charting.mpl_helpers import fig, subplot, sbp
49
+ from qubx.utils.charting.mpl_helpers import fig, subplot, sbp, plot_trends, ohlc_plot
43
50
 
44
51
  # - - - - Utils - - - -
45
52
  from qubx.pandaz.utils import scols, srows, ohlc_resample, continuous_periods, generate_equal_date_ranges
46
53
 
47
54
  # - setup short numpy output format
48
55
  np_fmt_short()
49
-
56
+
50
57
  # - add project home to system path
51
58
  add_project_to_system_path()
52
59
 
53
60
  # show logo first time
54
- if not hasattr(qubx.QubxMagics, '__already_initialized__'):
61
+ if not hasattr(qubx.QubxMagics, "__already_initialized__"):
55
62
  setattr(qubx.QubxMagics, "__already_initialized__", True)
56
63
  logo()
57
-
qubx/core/series.pxd CHANGED
@@ -15,7 +15,7 @@ cdef class TimeSeries:
15
15
  cdef public long long timeframe
16
16
  cdef public Indexed times
17
17
  cdef public Indexed values
18
- cdef float max_series_length
18
+ cdef public float max_series_length
19
19
  cdef unsigned short _is_new_item
20
20
  cdef public str name
21
21
  cdef dict indicators # it's used for indicators caching
@@ -28,8 +28,12 @@ cdef class TimeSeries:
28
28
 
29
29
 
30
30
  cdef class Indicator(TimeSeries):
31
- cdef TimeSeries series
32
- cdef TimeSeries parent
31
+ cdef public TimeSeries series
32
+ cdef public TimeSeries parent
33
+
34
+
35
+ cdef class IndicatorOHLC(Indicator):
36
+ pass
33
37
 
34
38
 
35
39
  cdef class RollingSum:
qubx/core/series.pyx CHANGED
@@ -300,6 +300,9 @@ def _wrap_indicator(series: TimeSeries, clz, *args, **kwargs):
300
300
 
301
301
 
302
302
  cdef class Indicator(TimeSeries):
303
+ """
304
+ Basic class for indicator that can be attached to TimeSeries
305
+ """
303
306
 
304
307
  def __init__(self, str name, TimeSeries series):
305
308
  if not name:
@@ -309,7 +312,7 @@ cdef class Indicator(TimeSeries):
309
312
  self.name = name
310
313
 
311
314
  # - we need to make a empty copy and fill it
312
- self.series = TimeSeries(series.name, series.timeframe, series.max_series_length)
315
+ self.series = self._instantiate_base_series(series.name, series.timeframe, series.max_series_length)
313
316
  self.parent = series
314
317
 
315
318
  # - notify the parent series that indicator has been attached
@@ -318,6 +321,9 @@ cdef class Indicator(TimeSeries):
318
321
  # - recalculate indicator on data as if it would being streamed
319
322
  self._initial_data_recalculate(series)
320
323
 
324
+ def _instantiate_base_series(self, str name, long long timeframe, float max_series_length):
325
+ return TimeSeries(name, timeframe, max_series_length)
326
+
321
327
  def _on_attach_indicator(self, indicator: Indicator, indicator_input: TimeSeries):
322
328
  self.parent._on_attach_indicator(indicator, indicator_input)
323
329
 
@@ -345,6 +351,17 @@ cdef class Indicator(TimeSeries):
345
351
  return _wrap_indicator(series, clz, *args, **kwargs)
346
352
 
347
353
 
354
+ cdef class IndicatorOHLC(Indicator):
355
+ """
356
+ Extension of indicator class to be used for OHLCV series
357
+ """
358
+ def _instantiate_base_series(self, str name, long long timeframe, float max_series_length):
359
+ return OHLCV(name, timeframe, max_series_length)
360
+
361
+ def calculate(self, long long time, Bar value, short new_item_started) -> object:
362
+ raise ValueError("Indicator must implement calculate() method")
363
+
364
+
348
365
  cdef class Lag(Indicator):
349
366
  cdef int period
350
367
 
qubx/core/utils.pyx CHANGED
@@ -50,5 +50,5 @@ cpdef recognize_timeframe(timeframe):
50
50
  tf = np.int64(timeframe.item().total_seconds() * NS)
51
51
 
52
52
  else:
53
- raise ValueError('Unknown timeframe type !')
53
+ raise ValueError(f'Unknown timeframe type: {timeframe} !')
54
54
  return tf
qubx/data/readers.py CHANGED
@@ -11,6 +11,7 @@ from functools import wraps
11
11
  from qubx import logger
12
12
  from qubx.core.series import TimeSeries, OHLCV, time_as_nsec, Quote, Trade
13
13
  from qubx.utils.time import infer_series_frequency, handle_start_stop
14
+ from psycopg.types.datetime import TimestampLoader
14
15
 
15
16
  _DT = lambda x: pd.Timedelta(x).to_numpy().item()
16
17
  D1, H1 = _DT("1D"), _DT("1h")
@@ -20,6 +21,12 @@ STOCK_DAILY_SESSION = (_DT("9:30:00.100"), _DT("15:59:59.900"))
20
21
  CME_FUTURES_DAILY_SESSION = (_DT("8:30:00.100"), _DT("15:14:59.900"))
21
22
 
22
23
 
24
+ class NpTimestampLoader(TimestampLoader):
25
+ def load(self, data) -> np.datetime64:
26
+ dt = super().load(data)
27
+ return np.datetime64(dt)
28
+
29
+
23
30
  def _recognize_t(t: Union[int, str], defaultvalue, timeunit) -> int:
24
31
  if isinstance(t, (str, pd.Timestamp)):
25
32
  try:
@@ -46,7 +53,7 @@ def _find_column_index_in_list(xs, *args):
46
53
 
47
54
 
48
55
  _FIND_TIME_COL_IDX = lambda column_names: _find_column_index_in_list(
49
- column_names, "time", "timestamp", "datetime", "date", "open_time"
56
+ column_names, "time", "timestamp", "datetime", "date", "open_time", "ts"
50
57
  )
51
58
 
52
59
 
@@ -56,7 +63,13 @@ class DataTransformer:
56
63
  self.buffer = []
57
64
  self._column_names = []
58
65
 
59
- def start_transform(self, name: str, column_names: List[str]):
66
+ def start_transform(
67
+ self,
68
+ name: str,
69
+ column_names: List[str],
70
+ start: str | None = None,
71
+ stop: str | None = None,
72
+ ):
60
73
  self._column_names = column_names
61
74
  self.buffer = []
62
75
 
@@ -181,7 +194,9 @@ class CsvStorageDataReader(DataReader):
181
194
 
182
195
  def _iter_chunks():
183
196
  for n in range(0, length // chunksize + 1):
184
- transform.start_transform(data_id, fieldnames)
197
+ transform.start_transform(
198
+ data_id, fieldnames, start=start, stop=stop
199
+ )
185
200
  raw_data = (
186
201
  selected_table[n * chunksize : min((n + 1) * chunksize, length)]
187
202
  .to_pandas()
@@ -192,7 +207,7 @@ class CsvStorageDataReader(DataReader):
192
207
 
193
208
  return _iter_chunks()
194
209
 
195
- transform.start_transform(data_id, fieldnames)
210
+ transform.start_transform(data_id, fieldnames, start=start, stop=stop)
196
211
  raw_data = selected_table.to_pandas().to_numpy()
197
212
  transform.process_data(raw_data)
198
213
  return transform.collect()
@@ -213,7 +228,7 @@ class AsPandasFrame(DataTransformer):
213
228
  def __init__(self, timestamp_units=None) -> None:
214
229
  self.timestamp_units = timestamp_units
215
230
 
216
- def start_transform(self, name: str, column_names: List[str]):
231
+ def start_transform(self, name: str, column_names: List[str], **kwargs):
217
232
  self._time_idx = _FIND_TIME_COL_IDX(column_names)
218
233
  self._column_names = column_names
219
234
  self._frame = pd.DataFrame()
@@ -256,7 +271,7 @@ class AsOhlcvSeries(DataTransformer):
256
271
  self._data_type = None
257
272
  self.timestamp_units = timestamp_units
258
273
 
259
- def start_transform(self, name: str, column_names: List[str]):
274
+ def start_transform(self, name: str, column_names: List[str], **kwargs):
260
275
  self._time_idx = _FIND_TIME_COL_IDX(column_names)
261
276
  self._volume_idx = None
262
277
  self._b_volume_idx = None
@@ -376,7 +391,7 @@ class AsQuotes(DataTransformer):
376
391
  Data must have appropriate structure: bid, ask, bidsize, asksize and time
377
392
  """
378
393
 
379
- def start_transform(self, name: str, column_names: List[str]):
394
+ def start_transform(self, name: str, column_names: List[str], **kwargs):
380
395
  self.buffer = list()
381
396
  self._time_idx = _FIND_TIME_COL_IDX(column_names)
382
397
  self._bid_idx = _find_column_index_in_list(column_names, "bid")
@@ -422,7 +437,7 @@ class AsTimestampedRecords(DataTransformer):
422
437
  def __init__(self, timestamp_units: str | None = None) -> None:
423
438
  self.timestamp_units = timestamp_units
424
439
 
425
- def start_transform(self, name: str, column_names: List[str]):
440
+ def start_transform(self, name: str, column_names: List[str], **kwargs):
426
441
  self.buffer = list()
427
442
  self._time_idx = _FIND_TIME_COL_IDX(column_names)
428
443
  self._column_names = column_names
@@ -465,7 +480,7 @@ class RestoreTicksFromOHLC(DataTransformer):
465
480
  self._d_session_start = daily_session_start_end[0]
466
481
  self._d_session_end = daily_session_start_end[1]
467
482
 
468
- def start_transform(self, name: str, column_names: List[str]):
483
+ def start_transform(self, name: str, column_names: List[str], **kwargs):
469
484
  self.buffer = []
470
485
  # - it will fail if receive data doesn't look as ohlcv
471
486
  self._time_idx = _FIND_TIME_COL_IDX(column_names)
@@ -606,10 +621,8 @@ def _retry(fn):
606
621
  # print(x, cls._reconnect_tries)
607
622
  try:
608
623
  return fn(*args, **kw)
609
- except (pg.InterfaceError, pg.OperationalError) as e:
610
- logger.warning(
611
- "Database Connection [InterfaceError or OperationalError]"
612
- )
624
+ except (pg.InterfaceError, pg.OperationalError, AttributeError) as e:
625
+ logger.debug("Database Connection [InterfaceError or OperationalError]")
613
626
  # print ("Idle for %s seconds" % (cls._reconnect_idle))
614
627
  # time.sleep(cls._reconnect_idle)
615
628
  cls._connect()
@@ -700,7 +713,7 @@ class QuestDBSqlCandlesBuilder(QuestDBSqlBuilder):
700
713
  resample
701
714
  )
702
715
  if resample
703
- else resample
716
+ else "1m" # if resample is empty let's use 1 minute timeframe
704
717
  )
705
718
  _rsmpl = f"SAMPLE by {resample}" if resample else ""
706
719
 
@@ -749,6 +762,16 @@ class QuestDBConnector(DataReader):
749
762
  self._builder = builder
750
763
  self._connect()
751
764
 
765
+ def __getstate__(self):
766
+ if self._connection:
767
+ self._connection.close()
768
+ self._connection = None
769
+ if self._cursor:
770
+ self._cursor.close()
771
+ self._cursor = None
772
+ state = self.__dict__.copy()
773
+ return state
774
+
752
775
  def _connect(self):
753
776
  self._connection = pg.connect(self.connection_url, autocommit=True)
754
777
  self._cursor = self._connection.cursor()
@@ -761,7 +784,7 @@ class QuestDBConnector(DataReader):
761
784
  stop: str | None = None,
762
785
  transform: DataTransformer = DataTransformer(),
763
786
  chunksize=0, # TODO: use self._cursor.fetchmany in this case !!!!
764
- timeframe: str = "1m",
787
+ timeframe: str | None = "1m",
765
788
  data_type="candles_1m",
766
789
  ) -> Any:
767
790
  return self._read(
@@ -786,7 +809,7 @@ class QuestDBConnector(DataReader):
786
809
  stop: str | None,
787
810
  transform: DataTransformer,
788
811
  chunksize: int, # TODO: use self._cursor.fetchmany in this case !!!!
789
- timeframe: str,
812
+ timeframe: str | None,
790
813
  data_type: str,
791
814
  builder: QuestDBSqlBuilder,
792
815
  ) -> Any:
@@ -795,9 +818,11 @@ class QuestDBConnector(DataReader):
795
818
 
796
819
  self._cursor.execute(_req) # type: ignore
797
820
  records = self._cursor.fetchall() # TODO: for chunksize > 0 use fetchmany etc
821
+ if not records:
822
+ return None
798
823
 
799
824
  names = [d.name for d in self._cursor.description] # type: ignore
800
- transform.start_transform(data_id, names)
825
+ transform.start_transform(data_id, names, start=start, stop=stop)
801
826
 
802
827
  transform.process_data(records)
803
828
  return transform.collect()
@@ -811,54 +836,20 @@ class QuestDBConnector(DataReader):
811
836
  def __del__(self):
812
837
  for c in (self._cursor, self._connection):
813
838
  try:
814
- logger.info("Closing connection")
839
+ logger.debug("Closing connection")
815
840
  c.close()
816
841
  except:
817
842
  pass
818
843
 
819
844
 
820
- class SnapshotsBuilder(DataTransformer):
821
- """
822
- Snapshots assembler from OB updates
823
- """
824
-
825
- def __init__(
826
- self,
827
- levels: int = -1, # how many levels restore, 1 - TOB, -1 - all
828
- as_frame=False, # result is dataframe
829
- ):
830
- self.buffer = []
831
- self.levels = levels
832
- self.as_frame = as_frame
833
-
834
- def start_transform(self, name: str, column_names: List[str]):
835
- # initialize buffer / series etc
836
- # let's keep restored snapshots into some buffer etc
837
- self.buffer = []
838
-
839
- # do additional init stuff here
840
-
841
- def process_data(self, rows_data: List[List]) -> Any:
842
- for r in rows_data:
843
- # restore snapshots and put into buffer or series
844
- pass
845
-
846
- def collect(self) -> Any:
847
- # - may be convert it to pandas DataFrame ?
848
- if self.as_frame:
849
- return pd.DataFrame.from_records(self.buffer) # or custom transform
850
-
851
- # - or just returns as plain list
852
- return self.buffer
853
-
854
-
855
- class QuestDBSqlOrderBookBilder(QuestDBSqlBuilder):
845
+ class QuestDBSqlOrderBookBuilder(QuestDBSqlCandlesBuilder):
856
846
  """
857
847
  Sql builder for snapshot data
858
848
  """
859
849
 
860
- def get_table_name(self, data_id: str, sfx: str = "") -> str:
861
- return ""
850
+ MAX_TIME_DELTA = pd.Timedelta("5h")
851
+ SNAPSHOT_DELTA = pd.Timedelta("1h")
852
+ MIN_DELTA = pd.Timedelta("1s")
862
853
 
863
854
  def prepare_data_sql(
864
855
  self,
@@ -868,7 +859,23 @@ class QuestDBSqlOrderBookBilder(QuestDBSqlBuilder):
868
859
  resample: str,
869
860
  data_type: str,
870
861
  ) -> str:
871
- return ""
862
+ if not start or not end:
863
+ raise ValueError("Start and end dates must be provided for orderbook data!")
864
+ start_dt, end_dt = pd.Timestamp(start), pd.Timestamp(end)
865
+ delta = end_dt - start_dt
866
+ if delta > self.MAX_TIME_DELTA:
867
+ raise ValueError(
868
+ f"Time range is too big for orderbook data: {delta}, max allowed: {self.MAX_TIME_DELTA}"
869
+ )
870
+
871
+ raw_start_dt = start_dt.floor(self.SNAPSHOT_DELTA) - self.MIN_DELTA
872
+
873
+ table_name = self.get_table_name(data_id, data_type)
874
+ query = f"""
875
+ SELECT * FROM {table_name}
876
+ WHERE timestamp BETWEEN '{raw_start_dt}' AND '{end_dt}'
877
+ """
878
+ return query
872
879
 
873
880
 
874
881
  class TradeSql(QuestDBSqlCandlesBuilder):
@@ -931,7 +938,8 @@ class MultiQdbConnector(QuestDBConnector):
931
938
  _TYPE_TO_BUILDER = {
932
939
  "candles_1m": QuestDBSqlCandlesBuilder(),
933
940
  "trade": TradeSql(),
934
- "orderbook": QuestDBSqlOrderBookBilder(),
941
+ "agg_trade": TradeSql(),
942
+ "orderbook": QuestDBSqlOrderBookBuilder(),
935
943
  }
936
944
 
937
945
  _TYPE_MAPPINGS = {
@@ -940,6 +948,9 @@ class MultiQdbConnector(QuestDBConnector):
940
948
  "ob": "orderbook",
941
949
  "trd": "trade",
942
950
  "td": "trade",
951
+ "aggTrade": "agg_trade",
952
+ "agg_trades": "agg_trade",
953
+ "aggTrades": "agg_trade",
943
954
  }
944
955
 
945
956
  def __init__(
@@ -974,9 +985,9 @@ class MultiQdbConnector(QuestDBConnector):
974
985
  start: str | None = None,
975
986
  stop: str | None = None,
976
987
  transform: DataTransformer = DataTransformer(),
977
- chunksize=0, # TODO: use self._cursor.fetchmany in this case !!!!
988
+ chunksize: int = 0, # TODO: use self._cursor.fetchmany in this case !!!!
978
989
  timeframe: str | None = None,
979
- data_type="candles",
990
+ data_type: str = "candles",
980
991
  ) -> Any:
981
992
  _mapped_data_type = self._TYPE_MAPPINGS.get(data_type, data_type)
982
993
  return self._read(
qubx/math/__init__.py CHANGED
@@ -1 +1 @@
1
- from .math import percentile_rank
1
+ from .stats import compare_to_norm, percentile_rank, kde
qubx/math/stats.py CHANGED
@@ -30,13 +30,30 @@ def compare_to_norm(xs, xranges=None):
30
30
  fit = stats.norm.pdf(sorted(xs), _m, _s)
31
31
 
32
32
  sbp(12, 1)
33
- plt.plot(sorted(xs), fit, 'r--', lw=2, label='N(%.2f, %.2f)' % (_m, _s))
34
- plt.legend(loc='upper right')
33
+ plt.plot(sorted(xs), fit, "r--", lw=2, label="N(%.2f, %.2f)" % (_m, _s))
34
+ plt.legend(loc="upper right")
35
35
 
36
- sns.kdeplot(xs, color='g', label='Data', shade=True)
36
+ sns.kdeplot(xs, color="g", label="Data", fill=True)
37
37
  if xranges is not None and len(xranges) > 1:
38
38
  plt.xlim(xranges)
39
- plt.legend(loc='upper right')
39
+ plt.legend(loc="upper right")
40
40
 
41
41
  sbp(12, 2)
42
42
  stats.probplot(xs, dist="norm", sparams=(_m, _s), plot=plt)
43
+
44
+
45
+ def kde(array, cut_down=True, bw_method="scott"):
46
+ """
47
+ Kernel dense estimation
48
+ """
49
+ from scipy.stats import gaussian_kde
50
+
51
+ if cut_down:
52
+ bins, counts = np.unique(array, return_counts=True)
53
+ f_mean = counts.mean()
54
+ f_above_mean = bins[counts > f_mean]
55
+ if len(f_above_mean) > 0:
56
+ bounds = [f_above_mean.min(), f_above_mean.max()]
57
+ array = array[np.bitwise_and(bounds[0] < array, array < bounds[1])]
58
+
59
+ return gaussian_kde(array, bw_method=bw_method)