Qubx 0.5.7__cp312-cp312-manylinux_2_39_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 +207 -0
- qubx/_nb_magic.py +100 -0
- qubx/backtester/__init__.py +5 -0
- qubx/backtester/account.py +145 -0
- qubx/backtester/broker.py +87 -0
- qubx/backtester/data.py +296 -0
- qubx/backtester/management.py +378 -0
- qubx/backtester/ome.py +296 -0
- qubx/backtester/optimization.py +201 -0
- qubx/backtester/simulated_data.py +558 -0
- qubx/backtester/simulator.py +362 -0
- qubx/backtester/utils.py +780 -0
- qubx/cli/__init__.py +0 -0
- qubx/cli/commands.py +67 -0
- qubx/connectors/ccxt/__init__.py +0 -0
- qubx/connectors/ccxt/account.py +495 -0
- qubx/connectors/ccxt/broker.py +132 -0
- qubx/connectors/ccxt/customizations.py +193 -0
- qubx/connectors/ccxt/data.py +612 -0
- qubx/connectors/ccxt/exceptions.py +17 -0
- qubx/connectors/ccxt/factory.py +93 -0
- qubx/connectors/ccxt/utils.py +307 -0
- qubx/core/__init__.py +0 -0
- qubx/core/account.py +251 -0
- qubx/core/basics.py +850 -0
- qubx/core/context.py +420 -0
- qubx/core/exceptions.py +38 -0
- qubx/core/helpers.py +480 -0
- qubx/core/interfaces.py +1150 -0
- qubx/core/loggers.py +514 -0
- qubx/core/lookups.py +475 -0
- qubx/core/metrics.py +1512 -0
- qubx/core/mixins/__init__.py +13 -0
- qubx/core/mixins/market.py +94 -0
- qubx/core/mixins/processing.py +428 -0
- qubx/core/mixins/subscription.py +203 -0
- qubx/core/mixins/trading.py +88 -0
- qubx/core/mixins/universe.py +270 -0
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +125 -0
- qubx/core/series.pyi +118 -0
- qubx/core/series.pyx +988 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.pyi +6 -0
- qubx/core/utils.pyx +62 -0
- qubx/data/__init__.py +25 -0
- qubx/data/helpers.py +416 -0
- qubx/data/readers.py +1562 -0
- qubx/data/tardis.py +100 -0
- qubx/gathering/simplest.py +88 -0
- qubx/math/__init__.py +3 -0
- qubx/math/stats.py +129 -0
- qubx/pandaz/__init__.py +23 -0
- qubx/pandaz/ta.py +2757 -0
- qubx/pandaz/utils.py +638 -0
- qubx/resources/instruments/symbols-binance.cm.json +1 -0
- qubx/resources/instruments/symbols-binance.json +1 -0
- qubx/resources/instruments/symbols-binance.um.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.json +1 -0
- qubx/resources/instruments/symbols-kraken.f.json +1 -0
- qubx/resources/instruments/symbols-kraken.json +1 -0
- qubx/ta/__init__.py +0 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pxd +149 -0
- qubx/ta/indicators.pyi +41 -0
- qubx/ta/indicators.pyx +787 -0
- qubx/trackers/__init__.py +3 -0
- qubx/trackers/abvanced.py +236 -0
- qubx/trackers/composite.py +146 -0
- qubx/trackers/rebalancers.py +129 -0
- qubx/trackers/riskctrl.py +641 -0
- qubx/trackers/sizers.py +235 -0
- qubx/utils/__init__.py +5 -0
- qubx/utils/_pyxreloader.py +281 -0
- qubx/utils/charting/lookinglass.py +1057 -0
- qubx/utils/charting/mpl_helpers.py +1183 -0
- qubx/utils/marketdata/binance.py +284 -0
- qubx/utils/marketdata/ccxt.py +90 -0
- qubx/utils/marketdata/dukas.py +130 -0
- qubx/utils/misc.py +541 -0
- qubx/utils/ntp.py +63 -0
- qubx/utils/numbers_utils.py +7 -0
- qubx/utils/orderbook.py +491 -0
- qubx/utils/plotting/__init__.py +0 -0
- qubx/utils/plotting/dashboard.py +150 -0
- qubx/utils/plotting/data.py +137 -0
- qubx/utils/plotting/interfaces.py +25 -0
- qubx/utils/plotting/renderers/__init__.py +0 -0
- qubx/utils/plotting/renderers/plotly.py +0 -0
- qubx/utils/runner/__init__.py +1 -0
- qubx/utils/runner/_jupyter_runner.pyt +60 -0
- qubx/utils/runner/accounts.py +88 -0
- qubx/utils/runner/configs.py +65 -0
- qubx/utils/runner/runner.py +470 -0
- qubx/utils/time.py +312 -0
- qubx-0.5.7.dist-info/METADATA +105 -0
- qubx-0.5.7.dist-info/RECORD +100 -0
- qubx-0.5.7.dist-info/WHEEL +4 -0
- qubx-0.5.7.dist-info/entry_points.txt +3 -0
|
Binary file
|
qubx/core/utils.pyi
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
def recognize_time(time): ...
|
|
2
|
+
def time_to_str(t: int, units: str = "ns") -> str: ...
|
|
3
|
+
def time_delta_to_str(d: int) -> str: ...
|
|
4
|
+
def recognize_timeframe(timeframe): ...
|
|
5
|
+
def prec_ceil(a: float, precision: int) -> float: ...
|
|
6
|
+
def prec_floor(a: float, precision: int) -> float: ...
|
qubx/core/utils.pyx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from qubx.utils import convert_tf_str_td64
|
|
2
|
+
import numpy as np
|
|
3
|
+
cimport numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
NS = 1_000_000_000
|
|
7
|
+
|
|
8
|
+
cpdef recognize_time(time):
|
|
9
|
+
return np.datetime64(time, 'ns') if isinstance(time, str) else np.datetime64(time, 'ms')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
cpdef str time_to_str(long long t, str units = 'ns'):
|
|
13
|
+
return str(np.datetime64(t, units)) #.isoformat()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
cpdef str time_delta_to_str(long long d):
|
|
17
|
+
"""
|
|
18
|
+
Convert timedelta object to pretty print format
|
|
19
|
+
|
|
20
|
+
:param d:
|
|
21
|
+
:return:
|
|
22
|
+
"""
|
|
23
|
+
days, seconds = divmod(d, 86400*NS)
|
|
24
|
+
hours, seconds = divmod(seconds, 3600*NS)
|
|
25
|
+
minutes, seconds = divmod(seconds, 60*NS)
|
|
26
|
+
seconds, rem = divmod(seconds, NS)
|
|
27
|
+
r = ''
|
|
28
|
+
if days > 0:
|
|
29
|
+
r += '%dD' % days
|
|
30
|
+
if hours > 0:
|
|
31
|
+
r += '%dh' % hours
|
|
32
|
+
if minutes > 0:
|
|
33
|
+
r += '%dMin' % minutes
|
|
34
|
+
if seconds > 0:
|
|
35
|
+
r += '%dS' % seconds
|
|
36
|
+
if rem > 0:
|
|
37
|
+
r += '%dmS' % (rem // 1000000)
|
|
38
|
+
return r
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
cpdef recognize_timeframe(timeframe):
|
|
42
|
+
tf = timeframe
|
|
43
|
+
if isinstance(timeframe, str):
|
|
44
|
+
tf = np.int64(convert_tf_str_td64(timeframe).item().total_seconds() * NS)
|
|
45
|
+
|
|
46
|
+
elif isinstance(timeframe, (int, float)) and timeframe >= 0:
|
|
47
|
+
tf = timeframe
|
|
48
|
+
|
|
49
|
+
elif isinstance(timeframe, np.timedelta64):
|
|
50
|
+
tf = np.int64(timeframe.item().total_seconds() * NS)
|
|
51
|
+
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f'Unknown timeframe type: {timeframe} !')
|
|
54
|
+
return tf
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
cpdef double prec_ceil(double a, int precision):
|
|
58
|
+
return np.sign(a) * np.true_divide(np.ceil(round(abs(a) * 10**precision, precision)), 10**precision)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
cpdef double prec_floor(double a, int precision):
|
|
62
|
+
return np.sign(a) * np.true_divide(np.floor(round(abs(a) * 10**precision, precision)), 10**precision)
|
qubx/data/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"DataReader",
|
|
3
|
+
"CsvStorageDataReader",
|
|
4
|
+
"MultiQdbConnector",
|
|
5
|
+
"QuestDBConnector",
|
|
6
|
+
"AsOhlcvSeries",
|
|
7
|
+
"AsPandasFrame",
|
|
8
|
+
"AsQuotes",
|
|
9
|
+
"AsTimestampedRecords",
|
|
10
|
+
"RestoreTicksFromOHLC",
|
|
11
|
+
"loader",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
from .helpers import loader
|
|
15
|
+
from .readers import (
|
|
16
|
+
AsOhlcvSeries,
|
|
17
|
+
AsPandasFrame,
|
|
18
|
+
AsQuotes,
|
|
19
|
+
AsTimestampedRecords,
|
|
20
|
+
CsvStorageDataReader,
|
|
21
|
+
DataReader,
|
|
22
|
+
MultiQdbConnector,
|
|
23
|
+
QuestDBConnector,
|
|
24
|
+
RestoreTicksFromOHLC,
|
|
25
|
+
)
|
qubx/data/helpers.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import Any, Dict, Iterable, List, Set, Type
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from joblib import delayed
|
|
7
|
+
|
|
8
|
+
from qubx import logger
|
|
9
|
+
from qubx.core.basics import DataType, ITimeProvider
|
|
10
|
+
from qubx.core.series import TimeSeries
|
|
11
|
+
from qubx.data.readers import (
|
|
12
|
+
CsvStorageDataReader,
|
|
13
|
+
DataReader,
|
|
14
|
+
DataTransformer,
|
|
15
|
+
InMemoryDataFrameReader,
|
|
16
|
+
MultiQdbConnector,
|
|
17
|
+
QuestDBConnector,
|
|
18
|
+
_list_to_chunked_iterator,
|
|
19
|
+
)
|
|
20
|
+
from qubx.pandaz.utils import OhlcDict, generate_equal_date_ranges, ohlc_resample, srows
|
|
21
|
+
from qubx.utils.misc import ProgressParallel
|
|
22
|
+
from qubx.utils.time import handle_start_stop
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InMemoryCachedReader(InMemoryDataFrameReader):
|
|
26
|
+
"""
|
|
27
|
+
A class for caching and reading financial data from memory.
|
|
28
|
+
|
|
29
|
+
This class extends InMemoryDataFrameReader to provide efficient data caching and retrieval
|
|
30
|
+
for financial data from a specific exchange and timeframe.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
exchange: str
|
|
34
|
+
_data_timeframe: str
|
|
35
|
+
_reader: DataReader
|
|
36
|
+
_n_jobs: int
|
|
37
|
+
_start: pd.Timestamp | None = None
|
|
38
|
+
_stop: pd.Timestamp | None = None
|
|
39
|
+
_symbols: list[str]
|
|
40
|
+
|
|
41
|
+
# - external data
|
|
42
|
+
_external: dict[str, pd.DataFrame | pd.Series]
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
exchange: str,
|
|
47
|
+
reader: DataReader,
|
|
48
|
+
base_timeframe: str,
|
|
49
|
+
n_jobs: int = -1,
|
|
50
|
+
**kwargs,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._reader = reader
|
|
53
|
+
self._n_jobs = n_jobs
|
|
54
|
+
self._data_timeframe = base_timeframe
|
|
55
|
+
self.exchange = exchange
|
|
56
|
+
self._external = {}
|
|
57
|
+
self._symbols = []
|
|
58
|
+
|
|
59
|
+
# - copy external data
|
|
60
|
+
for k, v in kwargs.items():
|
|
61
|
+
if isinstance(v, (pd.DataFrame, pd.Series)):
|
|
62
|
+
self._external[k] = v
|
|
63
|
+
|
|
64
|
+
super().__init__({}, exchange)
|
|
65
|
+
|
|
66
|
+
def read(
|
|
67
|
+
self,
|
|
68
|
+
data_id: str,
|
|
69
|
+
start: str | None = None,
|
|
70
|
+
stop: str | None = None,
|
|
71
|
+
transform: DataTransformer = DataTransformer(),
|
|
72
|
+
chunksize=0,
|
|
73
|
+
# timeframe: str | None = None,
|
|
74
|
+
**kwargs,
|
|
75
|
+
) -> Iterable | List:
|
|
76
|
+
_s_path = data_id
|
|
77
|
+
if not data_id.startswith(self.exchange):
|
|
78
|
+
_s_path = f"{self.exchange}:{data_id}"
|
|
79
|
+
_, symb = _s_path.split(":")
|
|
80
|
+
|
|
81
|
+
_start = str(self._start) if start is None and self._start is not None else start
|
|
82
|
+
_stop = str(self._stop) if stop is None and self._stop is not None else stop
|
|
83
|
+
if _start is None or _stop is None:
|
|
84
|
+
raise ValueError("Start and stop date must be provided")
|
|
85
|
+
|
|
86
|
+
# - refresh symbol's data
|
|
87
|
+
self._handle_symbols_data_from_to([symb], _start, _stop)
|
|
88
|
+
|
|
89
|
+
# - super InMemoryDataFrameReader supports chunked reading now
|
|
90
|
+
return super().read(_s_path, start, stop, transform, chunksize=chunksize, **kwargs)
|
|
91
|
+
|
|
92
|
+
def __getitem__(self, keys) -> dict[str, pd.DataFrame | pd.Series] | pd.DataFrame | pd.Series:
|
|
93
|
+
"""
|
|
94
|
+
This helper mostly for using in research notebooks
|
|
95
|
+
"""
|
|
96
|
+
_start: str | None = None
|
|
97
|
+
_stop: str | None = None
|
|
98
|
+
_instruments: List[str] = []
|
|
99
|
+
_as_dict = False
|
|
100
|
+
|
|
101
|
+
if isinstance(keys, (tuple)):
|
|
102
|
+
for k in keys:
|
|
103
|
+
if isinstance(k, slice):
|
|
104
|
+
_start, _stop = k.start, k.stop
|
|
105
|
+
if isinstance(k, (list, tuple, set)):
|
|
106
|
+
_instruments = list(k)
|
|
107
|
+
_as_dict = True
|
|
108
|
+
if isinstance(k, str):
|
|
109
|
+
_instruments.append(k)
|
|
110
|
+
else:
|
|
111
|
+
if isinstance(keys, (list, tuple)):
|
|
112
|
+
_instruments.extend(keys)
|
|
113
|
+
_as_dict = True
|
|
114
|
+
elif isinstance(keys, slice):
|
|
115
|
+
_start, _stop = keys.start, keys.stop
|
|
116
|
+
else:
|
|
117
|
+
_instruments.append(keys)
|
|
118
|
+
_as_dict |= len(_instruments) > 1
|
|
119
|
+
|
|
120
|
+
if not _instruments:
|
|
121
|
+
_instruments = list(self._data.keys())
|
|
122
|
+
|
|
123
|
+
if not _instruments:
|
|
124
|
+
raise ValueError("No symbols provided")
|
|
125
|
+
|
|
126
|
+
if (_start is None and self._start is None) or (_stop is None and self._stop is None):
|
|
127
|
+
raise ValueError("Start and stop date must be provided")
|
|
128
|
+
|
|
129
|
+
_start = str(self._start) if _start is None else _start
|
|
130
|
+
_stop = str(self._stop) if _stop is None else _stop
|
|
131
|
+
|
|
132
|
+
_r = self._handle_symbols_data_from_to(_instruments, _start, _stop)
|
|
133
|
+
if not _as_dict and len(_instruments) == 1:
|
|
134
|
+
return _r.get(_instruments[0], pd.DataFrame())
|
|
135
|
+
return _r
|
|
136
|
+
|
|
137
|
+
def _load_candle_data(
|
|
138
|
+
self, symbols: List[str], start: str | pd.Timestamp, stop: str | pd.Timestamp, timeframe: str
|
|
139
|
+
) -> Dict[str, pd.DataFrame | pd.Series]:
|
|
140
|
+
_ohlcs = defaultdict(list)
|
|
141
|
+
_chunk_size_id_days = 30 * (4 if pd.Timedelta(timeframe) >= pd.Timedelta("1h") else 1)
|
|
142
|
+
_ranges = list(generate_equal_date_ranges(str(start), str(stop), _chunk_size_id_days, "D"))
|
|
143
|
+
|
|
144
|
+
# - for timeframes less than 1d generate_equal_date_ranges may skip days
|
|
145
|
+
# so we need to fix intervals
|
|
146
|
+
_es = list(zip(_ranges[:], _ranges[1:]))
|
|
147
|
+
_es = [(start, end[0]) for (start, _), end in _es]
|
|
148
|
+
_es.append((_ranges[-1][0], str(stop)))
|
|
149
|
+
|
|
150
|
+
_results = ProgressParallel(n_jobs=self._n_jobs, silent=True, total=len(_ranges))(
|
|
151
|
+
delayed(self._reader.get_aux_data)(
|
|
152
|
+
"candles", exchange=self.exchange, symbols=symbols, start=s, stop=e, timeframe=timeframe
|
|
153
|
+
)
|
|
154
|
+
for s, e in _es
|
|
155
|
+
)
|
|
156
|
+
for (s, e), data in zip(_ranges, _results):
|
|
157
|
+
assert isinstance(data, pd.DataFrame)
|
|
158
|
+
try:
|
|
159
|
+
# - some periods of data may be empty so just skipping it to avoid error log
|
|
160
|
+
if not data.empty:
|
|
161
|
+
data_symbols = data.index.get_level_values(1).unique()
|
|
162
|
+
for smb in data_symbols:
|
|
163
|
+
_ohlcs[smb].append(data.loc[pd.IndexSlice[:, smb], :].droplevel(1))
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
logger.warning(f"(InMemoryCachedReader) Failed to load data for {s} - {e} : {str(exc)}")
|
|
166
|
+
|
|
167
|
+
ohlc = {smb.upper(): srows(*vs, keep="first") for smb, vs in _ohlcs.items() if len(vs) > 0}
|
|
168
|
+
return ohlc
|
|
169
|
+
|
|
170
|
+
def _handle_symbols_data_from_to(
|
|
171
|
+
self, symbols: List[str], start: str, stop: str
|
|
172
|
+
) -> Dict[str, pd.DataFrame | pd.Series]:
|
|
173
|
+
# _dtf = pd.Timedelta(self._data_timeframe)
|
|
174
|
+
# T = lambda x: pd.Timestamp(x).floor(self._data_timeframe)
|
|
175
|
+
def convert_to_timestamp(x):
|
|
176
|
+
return pd.Timestamp(x)
|
|
177
|
+
|
|
178
|
+
_start, _stop = map(convert_to_timestamp, handle_start_stop(start, stop))
|
|
179
|
+
|
|
180
|
+
# - full interval
|
|
181
|
+
_new_symbols = list(set([s for s in symbols if s not in self._data]))
|
|
182
|
+
if _new_symbols:
|
|
183
|
+
_s_req = min(_start, self._start if self._start else _start)
|
|
184
|
+
_e_req = max(_stop, self._stop if self._stop else _stop)
|
|
185
|
+
logger.debug(f"(InMemoryCachedReader) Loading all data {_s_req} - {_e_req} for {','.join(_new_symbols)} ")
|
|
186
|
+
# _new_data = self._load_candle_data(_new_symbols, _s_req, _e_req + _dtf, self._data_timeframe)
|
|
187
|
+
_new_data = self._load_candle_data(_new_symbols, _s_req, _e_req, self._data_timeframe)
|
|
188
|
+
self._data |= _new_data
|
|
189
|
+
|
|
190
|
+
# - pre intervals
|
|
191
|
+
if self._start and _start < self._start:
|
|
192
|
+
_smbs = list(self._data.keys())
|
|
193
|
+
logger.debug(f"(InMemoryCachedReader) Updating {len(_smbs)} symbols pre interval {_start} : {self._start}")
|
|
194
|
+
# _before = self._load_candle_data(_smbs, _start, self._start + _dtf, self._data_timeframe)
|
|
195
|
+
_before = self._load_candle_data(_smbs, _start, self._start, self._data_timeframe)
|
|
196
|
+
for k, c in _before.items():
|
|
197
|
+
# self._data[k] = srows(c, self._data[k], keep="first")
|
|
198
|
+
self._data[k] = srows(c, self._data[k], keep="last")
|
|
199
|
+
|
|
200
|
+
# - post intervals
|
|
201
|
+
if self._stop and _stop > self._stop:
|
|
202
|
+
_smbs = list(self._data.keys())
|
|
203
|
+
logger.debug(f"(InMemoryCachedReader) Updating {len(_smbs)} symbols post interval {self._stop} : {_stop}")
|
|
204
|
+
# _after = self._load_candle_data(_smbs, self._stop - _dtf, _stop, self._data_timeframe)
|
|
205
|
+
_after = self._load_candle_data(_smbs, self._stop, _stop, self._data_timeframe)
|
|
206
|
+
for k, c in _after.items():
|
|
207
|
+
self._data[k] = srows(self._data[k], c, keep="last")
|
|
208
|
+
|
|
209
|
+
self._start = min(_start, self._start if self._start else _start)
|
|
210
|
+
self._stop = max(_stop, self._stop if self._stop else _stop)
|
|
211
|
+
return OhlcDict({s: self._data[s].loc[_start:_stop] for s in symbols if s in self._data})
|
|
212
|
+
|
|
213
|
+
def get_aux_data_ids(self) -> set[str]:
|
|
214
|
+
return self._reader.get_aux_data_ids() | set(self._external.keys())
|
|
215
|
+
|
|
216
|
+
def get_aux_data(self, data_id: str, **kwargs) -> Any:
|
|
217
|
+
_exch = kwargs.pop("exchange") if "exchange" in kwargs else None
|
|
218
|
+
if _exch and _exch != self.exchange:
|
|
219
|
+
raise ValueError(f"Exchange mismatch: expected {self.exchange}, got {_exch}")
|
|
220
|
+
|
|
221
|
+
match data_id:
|
|
222
|
+
# - special case for candles - it builds them from loaded ohlc data
|
|
223
|
+
case "candles":
|
|
224
|
+
return self._get_candles(**kwargs)
|
|
225
|
+
|
|
226
|
+
# - only symbols in cache
|
|
227
|
+
case "symbols":
|
|
228
|
+
return list(self._data.keys())
|
|
229
|
+
|
|
230
|
+
if data_id not in self._external:
|
|
231
|
+
self._external[data_id] = self._reader.get_aux_data(data_id, exchange=self.exchange)
|
|
232
|
+
|
|
233
|
+
_ext_data = self._external.get(data_id)
|
|
234
|
+
if _ext_data is not None:
|
|
235
|
+
_s = kwargs.pop("start") if "start" in kwargs else None
|
|
236
|
+
_e = kwargs.pop("stop") if "stop" in kwargs else None
|
|
237
|
+
_ext_data = _ext_data[:_e] if _e else _ext_data
|
|
238
|
+
_ext_data = _ext_data[_s:] if _s else _ext_data
|
|
239
|
+
return _ext_data
|
|
240
|
+
|
|
241
|
+
def _get_candles(
|
|
242
|
+
self,
|
|
243
|
+
symbols: List[str],
|
|
244
|
+
start: str | pd.Timestamp,
|
|
245
|
+
stop: str | pd.Timestamp,
|
|
246
|
+
timeframe: str = "1d",
|
|
247
|
+
) -> pd.DataFrame:
|
|
248
|
+
_xd: Dict[str, pd.DataFrame] = self[symbols, start:stop]
|
|
249
|
+
_xd = ohlc_resample(_xd, timeframe) if timeframe else _xd
|
|
250
|
+
_r = [x.assign(symbol=s.upper(), timestamp=x.index) for s, x in _xd.items()]
|
|
251
|
+
return srows(*_r).set_index(["timestamp", "symbol"])
|
|
252
|
+
|
|
253
|
+
def get_names(self, **kwargs) -> list[str]:
|
|
254
|
+
return self._reader.get_names(**kwargs)
|
|
255
|
+
|
|
256
|
+
def get_symbols(self, exchange: str, dtype: str) -> list[str]:
|
|
257
|
+
if not self._symbols:
|
|
258
|
+
self._symbols = self._reader.get_symbols(self.exchange, DataType.OHLC)
|
|
259
|
+
return self._symbols
|
|
260
|
+
|
|
261
|
+
def get_time_ranges(self, symbol: str, dtype: DataType) -> tuple[Any, Any]:
|
|
262
|
+
_id = f"{self.exchange}:{symbol}" if not symbol.startswith(self.exchange) else symbol
|
|
263
|
+
return self._reader.get_time_ranges(_id, dtype)
|
|
264
|
+
|
|
265
|
+
def __str__(self) -> str:
|
|
266
|
+
return f"{self.__class__.__name__}(exchange={self.exchange},timeframe={self._data_timeframe})"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TimeGuardedWrapper(DataReader):
|
|
270
|
+
# - currently 'known' time, can be used for limiting data
|
|
271
|
+
_time_guard_provider: ITimeProvider
|
|
272
|
+
_reader: InMemoryCachedReader
|
|
273
|
+
|
|
274
|
+
def __init__(
|
|
275
|
+
self,
|
|
276
|
+
reader: InMemoryCachedReader,
|
|
277
|
+
time_guard: ITimeProvider | None = None,
|
|
278
|
+
) -> None:
|
|
279
|
+
# - if no time provider is provided, use stub
|
|
280
|
+
class _NoTimeGuard(ITimeProvider):
|
|
281
|
+
def time(self) -> np.datetime64 | None:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
self._time_guard_provider = time_guard if time_guard is not None else _NoTimeGuard()
|
|
285
|
+
self._reader = reader
|
|
286
|
+
|
|
287
|
+
def read(
|
|
288
|
+
self,
|
|
289
|
+
data_id: str,
|
|
290
|
+
start: str | None = None,
|
|
291
|
+
stop: str | None = None,
|
|
292
|
+
transform: DataTransformer = DataTransformer(),
|
|
293
|
+
chunksize=0,
|
|
294
|
+
# timeframe: str | None = None,
|
|
295
|
+
**kwargs,
|
|
296
|
+
) -> Iterable | list:
|
|
297
|
+
xs = self._time_guarded_data(
|
|
298
|
+
self._reader.read(data_id, start=start, stop=stop, transform=transform, chunksize=0, **kwargs), # type: ignore
|
|
299
|
+
prev_bar=True,
|
|
300
|
+
)
|
|
301
|
+
return _list_to_chunked_iterator(xs, chunksize) if chunksize > 0 else xs
|
|
302
|
+
|
|
303
|
+
def get_aux_data(self, data_id: str, **kwargs) -> Any:
|
|
304
|
+
return self._time_guarded_data(self._reader.get_aux_data(data_id, **kwargs))
|
|
305
|
+
|
|
306
|
+
def __getitem__(self, keys):
|
|
307
|
+
return self._time_guarded_data(self._reader.__getitem__(keys), prev_bar=True)
|
|
308
|
+
|
|
309
|
+
def _time_guarded_data(
|
|
310
|
+
self, data: pd.DataFrame | pd.Series | dict[str, pd.DataFrame | pd.Series] | list, prev_bar: bool = False
|
|
311
|
+
) -> pd.DataFrame | pd.Series | Dict[str, pd.DataFrame | pd.Series] | list:
|
|
312
|
+
"""
|
|
313
|
+
This function is responsible for limiting the data based on a given time guard.
|
|
314
|
+
|
|
315
|
+
Parameters:
|
|
316
|
+
- data (pd.DataFrame | pd.Series | Dict[str, pd.DataFrame | pd.Series] | List): The data to be limited.
|
|
317
|
+
- prev_bar (bool, optional): If True, the time guard is applied to the previous bar. Defaults to False.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
- pd.DataFrame | pd.Series | Dict[str, pd.DataFrame | pd.Series] | List: The limited data.
|
|
321
|
+
"""
|
|
322
|
+
# - when no any limits - just returns it as is
|
|
323
|
+
if (_c_time := self._time_guard_provider.time()) is None:
|
|
324
|
+
return data
|
|
325
|
+
|
|
326
|
+
def cut_dict(xs, t):
|
|
327
|
+
return OhlcDict({s: v.loc[:t] for s, v in xs.items()})
|
|
328
|
+
|
|
329
|
+
def cut_list_of_timestamped(xs, t):
|
|
330
|
+
return list(filter(lambda x: x.time <= t, xs))
|
|
331
|
+
|
|
332
|
+
def cut_list_raw(xs, t):
|
|
333
|
+
return list(filter(lambda x: x[0] <= t, xs))
|
|
334
|
+
|
|
335
|
+
def cut_time_series(ts, t):
|
|
336
|
+
return ts.loc[: str(t)]
|
|
337
|
+
|
|
338
|
+
if prev_bar:
|
|
339
|
+
_c_time = _c_time - pd.Timedelta(self._reader._data_timeframe)
|
|
340
|
+
|
|
341
|
+
# - input is Dict[str, pd.DataFrame]
|
|
342
|
+
if isinstance(data, dict):
|
|
343
|
+
return cut_dict(data, _c_time)
|
|
344
|
+
|
|
345
|
+
# - input is List[(time, *data)] or List[Quote | Trade | Bar]
|
|
346
|
+
if isinstance(data, list):
|
|
347
|
+
if isinstance(data[0], (list, tuple, np.ndarray)):
|
|
348
|
+
return cut_list_raw(data, _c_time)
|
|
349
|
+
else:
|
|
350
|
+
return cut_list_of_timestamped(data, _c_time.asm8.item())
|
|
351
|
+
|
|
352
|
+
# - input is TimeSeries
|
|
353
|
+
if isinstance(data, TimeSeries):
|
|
354
|
+
return cut_time_series(data, _c_time)
|
|
355
|
+
|
|
356
|
+
return data.loc[:_c_time]
|
|
357
|
+
|
|
358
|
+
def __str__(self) -> str:
|
|
359
|
+
return f"TimeGuarded @ {str(self._reader)}"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
__KNOWN_READERS = {
|
|
363
|
+
"mqdb": MultiQdbConnector, # mqdb::xlydian-data
|
|
364
|
+
"multi": MultiQdbConnector,
|
|
365
|
+
"qdb": QuestDBConnector, # questdb::localhost
|
|
366
|
+
"questdb": MultiQdbConnector, # questdb::localhost
|
|
367
|
+
"csv": CsvStorageDataReader, # csv::path_to_storage, csv::c:/ssss/
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def loader(
|
|
372
|
+
exchange: str, timeframe: str, *symbols: List[str], source: str = "mqdb::localhost", no_cache=False, **kwargs
|
|
373
|
+
) -> DataReader:
|
|
374
|
+
"""
|
|
375
|
+
Create and initialize an InMemoryCachedReader for a specific exchange and timeframe.
|
|
376
|
+
|
|
377
|
+
This function sets up a cached reader for financial data, optionally pre-loading
|
|
378
|
+
data for specified symbols from the beginning of time until now.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
exchange (str): The name of the exchange to load data from.
|
|
382
|
+
timeframe (str): The time interval for the data (e.g., '1d' for daily, '1h' for hourly).
|
|
383
|
+
*symbols (List[str]): Variable number of symbol names to pre-load data for.
|
|
384
|
+
source (str): The data reader spec and it's parameter to use. Defaults to mqdb::localhost.
|
|
385
|
+
no_cache (bool): If True, data will not be cached. Defaults to False.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
InMemoryCachedReader: An initialized InMemoryCachedReader object, potentially pre-loaded with data.
|
|
389
|
+
|
|
390
|
+
Examples:
|
|
391
|
+
--------
|
|
392
|
+
>>> ld = loader("BINANCE.UM", '1h', source="mqdb::xlydian-data")
|
|
393
|
+
d = ld["BTCUSDT", "ETHUSDT", "SOLUSDT" , "2020-01-01":"2024-12-01"]
|
|
394
|
+
d('1d').close.plot()
|
|
395
|
+
print(d('1d'))
|
|
396
|
+
d("4h").close.pct_change(fill_method=None).cov()
|
|
397
|
+
"""
|
|
398
|
+
if not source:
|
|
399
|
+
raise ValueError("Source parameter must be provided")
|
|
400
|
+
|
|
401
|
+
_rcls_par = source.split("::")
|
|
402
|
+
_c: Type[DataReader] | None = __KNOWN_READERS.get(_rcls_par[0])
|
|
403
|
+
if _c is None:
|
|
404
|
+
raise ValueError(
|
|
405
|
+
f"Unsupported data reader type: {_rcls_par[0]}. Supported names: {', '.join(__KNOWN_READERS.keys())}."
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
reader_object: DataReader = _c(_rcls_par[1]) if len(_rcls_par) else _c()
|
|
409
|
+
inmcr = reader_object
|
|
410
|
+
# - if not need to cache data
|
|
411
|
+
if not no_cache:
|
|
412
|
+
inmcr = InMemoryCachedReader(exchange, reader_object, timeframe, **kwargs)
|
|
413
|
+
if symbols:
|
|
414
|
+
# by default slicing from 1970-01-01 until now
|
|
415
|
+
inmcr[list(symbols), slice("1970-01-01", str(pd.Timestamp("now")))]
|
|
416
|
+
return inmcr
|