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 +62 -32
- qubx/_nb_magic.py +14 -8
- qubx/core/series.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +7 -3
- qubx/core/series.pyx +18 -1
- qubx/core/utils.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.pyx +1 -1
- qubx/data/readers.py +71 -60
- qubx/math/__init__.py +1 -1
- qubx/math/stats.py +21 -4
- qubx/pandaz/ta.py +438 -376
- qubx/ta/indicators.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pyx +423 -1
- qubx/utils/charting/mpl_helpers.py +304 -243
- qubx/utils/misc.py +70 -60
- {qubx-0.1.83.dist-info → qubx-0.1.85.dist-info}/METADATA +7 -3
- {qubx-0.1.83.dist-info → qubx-0.1.85.dist-info}/RECORD +18 -18
- {qubx-0.1.83.dist-info → qubx-0.1.85.dist-info}/WHEEL +0 -0
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 =
|
|
13
|
+
fmt = (
|
|
14
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
|
|
15
|
+
)
|
|
14
16
|
|
|
15
|
-
prefix =
|
|
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(
|
|
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
|
-
|
|
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 [
|
|
45
|
-
from IPython.core.magic import
|
|
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(
|
|
81
|
+
self.qubx_setup("dark" + " " + line)
|
|
56
82
|
|
|
57
83
|
@line_magic
|
|
58
84
|
def qubxl(self, line: str):
|
|
59
|
-
self.qubx_setup(
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
82
|
-
set_mpl_theme(
|
|
108
|
+
if "dark" in line.lower():
|
|
109
|
+
set_mpl_theme("dark")
|
|
83
110
|
|
|
84
|
-
elif
|
|
85
|
-
set_mpl_theme(
|
|
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
|
|
115
|
-
line, name = line.split(
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
19
|
-
|
|
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 [
|
|
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,
|
|
61
|
+
if not hasattr(qubx.QubxMagics, "__already_initialized__"):
|
|
55
62
|
setattr(qubx.QubxMagics, "__already_initialized__", True)
|
|
56
63
|
logo()
|
|
57
|
-
|
|
Binary file
|
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 =
|
|
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
|
|
|
Binary file
|
qubx/core/utils.pyx
CHANGED
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(
|
|
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(
|
|
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.
|
|
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.
|
|
839
|
+
logger.debug("Closing connection")
|
|
815
840
|
c.close()
|
|
816
841
|
except:
|
|
817
842
|
pass
|
|
818
843
|
|
|
819
844
|
|
|
820
|
-
class
|
|
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
|
-
|
|
861
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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 .
|
|
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,
|
|
34
|
-
plt.legend(loc=
|
|
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=
|
|
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=
|
|
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)
|