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
qubx/utils/time.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
UNIX_T0 = np.datetime64("1970-01-01T00:00:00")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
time_to_str = lambda t, u="us": np.datetime_as_string( # noqa: E731
|
|
12
|
+
t if isinstance(t, np.datetime64) else np.datetime64(t, u), unit=u
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def convert_tf_str_td64(c_tf: str) -> np.timedelta64:
|
|
17
|
+
"""
|
|
18
|
+
Convert string timeframe to timedelta64
|
|
19
|
+
|
|
20
|
+
'15Min' -> timedelta64(15, 'm') etc
|
|
21
|
+
"""
|
|
22
|
+
_t = re.findall(r"(\d+)([A-Za-z]+)", c_tf)
|
|
23
|
+
_dt = 0
|
|
24
|
+
for g in _t:
|
|
25
|
+
unit = g[1].lower()
|
|
26
|
+
n = int(g[0])
|
|
27
|
+
u1 = unit[0]
|
|
28
|
+
u2 = unit[:2]
|
|
29
|
+
unit = u1
|
|
30
|
+
|
|
31
|
+
if u1 in ["d", "w"]:
|
|
32
|
+
unit = u1.upper()
|
|
33
|
+
|
|
34
|
+
if u1 in ["y"]:
|
|
35
|
+
n = 356 * n
|
|
36
|
+
unit = "D"
|
|
37
|
+
|
|
38
|
+
if u2 in ["ms", "ns", "us", "ps"]:
|
|
39
|
+
unit = u2
|
|
40
|
+
|
|
41
|
+
_dt += np.timedelta64(n, unit)
|
|
42
|
+
|
|
43
|
+
return _dt
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def convert_seconds_to_str(seconds: int, convert_months=False) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Convert seconds to string representation: 310 -> '5Min10S' etc
|
|
49
|
+
"""
|
|
50
|
+
r = ""
|
|
51
|
+
|
|
52
|
+
if convert_months:
|
|
53
|
+
months, seconds = divmod(seconds, 4 * 7 * 86400)
|
|
54
|
+
if months > 0:
|
|
55
|
+
r += "%dmonth" % months
|
|
56
|
+
|
|
57
|
+
weeks, seconds = divmod(seconds, 7 * 86400)
|
|
58
|
+
if weeks > 0:
|
|
59
|
+
r += "%dw" % weeks
|
|
60
|
+
|
|
61
|
+
days, seconds = divmod(seconds, 86400)
|
|
62
|
+
if days > 0:
|
|
63
|
+
r += "%dd" % days
|
|
64
|
+
|
|
65
|
+
hours, seconds = divmod(seconds, 3600)
|
|
66
|
+
if hours > 0:
|
|
67
|
+
r += "%dh" % hours
|
|
68
|
+
|
|
69
|
+
minutes, seconds = divmod(seconds, 60)
|
|
70
|
+
if minutes > 0:
|
|
71
|
+
r += "%dmin" % minutes
|
|
72
|
+
|
|
73
|
+
if seconds > 0:
|
|
74
|
+
r += "%ds" % seconds
|
|
75
|
+
return r
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def floor_t64(time: np.datetime64 | datetime, dt: np.timedelta64 | int | str):
|
|
79
|
+
"""
|
|
80
|
+
Floor timestamp by dt
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(dt, int):
|
|
83
|
+
dt = np.timedelta64(dt, "s")
|
|
84
|
+
|
|
85
|
+
if isinstance(dt, str):
|
|
86
|
+
dt = convert_tf_str_td64(dt)
|
|
87
|
+
|
|
88
|
+
if isinstance(time, datetime):
|
|
89
|
+
time = np.datetime64(time)
|
|
90
|
+
|
|
91
|
+
return time - (time - UNIX_T0) % dt
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def infer_series_frequency(series: list | pd.DataFrame | pd.Series | pd.DatetimeIndex) -> np.timedelta64:
|
|
95
|
+
"""
|
|
96
|
+
Infer frequency of given timeseries
|
|
97
|
+
|
|
98
|
+
:param series: Series, DataFrame, DatetimeIndex or list of timestamps object
|
|
99
|
+
:return: timedelta for found frequency
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(series, (pd.DataFrame, pd.Series, pd.DatetimeIndex)):
|
|
102
|
+
times_index = (series if isinstance(series, pd.DatetimeIndex) else series.index).to_pydatetime()
|
|
103
|
+
elif isinstance(series, (set, list, tuple)):
|
|
104
|
+
times_index = np.array(series)
|
|
105
|
+
elif isinstance(series, np.ndarray):
|
|
106
|
+
times_index = series
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError("Can't recognize input data")
|
|
109
|
+
|
|
110
|
+
if times_index.shape[0] < 2:
|
|
111
|
+
raise ValueError("Series must have at least 2 points to determ frequency")
|
|
112
|
+
|
|
113
|
+
values = np.array(
|
|
114
|
+
sorted(
|
|
115
|
+
[
|
|
116
|
+
(
|
|
117
|
+
x
|
|
118
|
+
if isinstance(x, (np.timedelta64, int, np.int64))
|
|
119
|
+
else int(x)
|
|
120
|
+
if isinstance(x, float)
|
|
121
|
+
else int(1e9 * x.total_seconds())
|
|
122
|
+
)
|
|
123
|
+
for x in np.abs(np.diff(times_index))
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
diff = np.concatenate(([1], np.diff(values)))
|
|
128
|
+
idx = np.concatenate((np.where(diff)[0], [len(values)]))
|
|
129
|
+
freqs = dict(zip(values[idx[:-1]], np.diff(idx)))
|
|
130
|
+
return np.timedelta64(max(freqs, key=freqs.get))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def handle_start_stop(
|
|
134
|
+
s: str | pd.Timestamp | None, e: str | pd.Timestamp | None, convert: Callable = str
|
|
135
|
+
) -> tuple[str | pd.Timestamp | None, str | pd.Timestamp | None]:
|
|
136
|
+
"""
|
|
137
|
+
Process start/stop times
|
|
138
|
+
|
|
139
|
+
>>> handle_start_stop('2020-01-01', '2020-02-01') # 2020-01-01, 2020-02-01
|
|
140
|
+
>>> handle_start_stop('2020-02-01', '2020-01-01') # 2020-01-01, 2020-02-01
|
|
141
|
+
>>> handle_start_stop('2020-01-01', '1w') # 2020-01-01, 2020-01-01 + 1week
|
|
142
|
+
>>> handle_start_stop('1w', '2020-01-01') # 2020-01-01 - 1week, '2020-01-01'
|
|
143
|
+
>>> handle_start_stop('2020-01-01', '-1w') # 2020-01-01 - 1week, 2020-01-01,
|
|
144
|
+
>>> handle_start_stop(None, '2020-01-01') # None, '2020-01-01'
|
|
145
|
+
>>> handle_start_stop('2020-01-01', None) # '2020-01-01', None
|
|
146
|
+
>>> handle_start_stop(None, None) # None, None
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def _h_time_like(x):
|
|
151
|
+
try:
|
|
152
|
+
return pd.Timestamp(x), False
|
|
153
|
+
except:
|
|
154
|
+
try:
|
|
155
|
+
return pd.Timedelta(x), True
|
|
156
|
+
except:
|
|
157
|
+
pass
|
|
158
|
+
return None, None
|
|
159
|
+
|
|
160
|
+
t0, d0 = _h_time_like(s) if s else (None, False)
|
|
161
|
+
t1, d1 = _h_time_like(e) if e else (None, False)
|
|
162
|
+
|
|
163
|
+
def _converts(xs):
|
|
164
|
+
return (convert(xs[0]) if xs[0] else None, convert(xs[1]) if xs[1] else None)
|
|
165
|
+
|
|
166
|
+
if not t1 and not t0:
|
|
167
|
+
return None, None
|
|
168
|
+
|
|
169
|
+
if d0 and d1:
|
|
170
|
+
raise ValueError("Start and stop can't both be deltas !")
|
|
171
|
+
|
|
172
|
+
if d0:
|
|
173
|
+
if not t1:
|
|
174
|
+
raise ValueError("First argument is delta but stop time is not defined !")
|
|
175
|
+
return _converts(sorted([t1 - abs(t0), t1]))
|
|
176
|
+
if d1:
|
|
177
|
+
if not t0:
|
|
178
|
+
raise ValueError("Second argument is delta but start time is not defined !")
|
|
179
|
+
return _converts(sorted([t0, t0 + t1]))
|
|
180
|
+
|
|
181
|
+
if t0 and t1:
|
|
182
|
+
return _converts(sorted([t0, t1]))
|
|
183
|
+
|
|
184
|
+
return _converts([t0, t1])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def timedelta_to_crontab(td: pd.Timedelta) -> str:
|
|
188
|
+
"""
|
|
189
|
+
Convert a pandas Timedelta to a crontab specification string.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
td (pd.Timedelta): Timedelta to convert to crontab spec
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
str: Crontab specification string
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
>>> timedelta_to_crontab(pd.Timedelta('4h'))
|
|
199
|
+
'0 */4 * * *'
|
|
200
|
+
>>> timedelta_to_crontab(pd.Timedelta('2d'))
|
|
201
|
+
'59 23 */2 * *'
|
|
202
|
+
>>> timedelta_to_crontab(pd.Timedelta('1d23h50Min10Sec'))
|
|
203
|
+
'50 23 */2 * * 10'
|
|
204
|
+
"""
|
|
205
|
+
days = td.days
|
|
206
|
+
hours = td.components.hours
|
|
207
|
+
minutes = td.components.minutes
|
|
208
|
+
seconds = td.components.seconds
|
|
209
|
+
|
|
210
|
+
if days > 0:
|
|
211
|
+
if hours == 0 and minutes == 0 and seconds == 0:
|
|
212
|
+
hours, minutes, seconds = 23, 59, 59
|
|
213
|
+
_sched = f"{minutes} {hours} */{days} * *"
|
|
214
|
+
return _sched + f" {seconds}" if seconds > 0 else _sched
|
|
215
|
+
|
|
216
|
+
if hours > 0:
|
|
217
|
+
_sched = f"{minutes} */{hours} * * *"
|
|
218
|
+
return _sched + f" {seconds}" if seconds > 0 else _sched
|
|
219
|
+
|
|
220
|
+
if minutes > 0:
|
|
221
|
+
_sched = f"*/{minutes} * * * *"
|
|
222
|
+
return _sched + f" {seconds}" if seconds > 0 else _sched
|
|
223
|
+
|
|
224
|
+
if seconds > 0:
|
|
225
|
+
return f"* * * * * */{seconds}"
|
|
226
|
+
|
|
227
|
+
raise ValueError("Timedelta must specify a non-zero period of days, hours, minutes or seconds")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def interval_to_cron(inv: str) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Convert a custom schedule format to a cron expression.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
inv (str): Custom schedule format string. Can be either:
|
|
236
|
+
- A pandas Timedelta string (e.g. "4h", "2d", "1d12h")
|
|
237
|
+
- A custom schedule format "<interval>@<time>" where:
|
|
238
|
+
interval: Optional number + unit (Q=quarter, M=month, Y=year, D=day, SUN=Sunday, MON=Monday, etc.)
|
|
239
|
+
time: HH:MM or HH:MM:SS
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
str: Cron expression
|
|
243
|
+
|
|
244
|
+
Examples:
|
|
245
|
+
>>> interval_to_cron("4h") # Pandas Timedelta
|
|
246
|
+
'0 */4 * * *'
|
|
247
|
+
>>> interval_to_cron("2d") # Pandas Timedelta
|
|
248
|
+
'59 23 */2 * *'
|
|
249
|
+
>>> interval_to_cron("@10:30") # Daily at 10:30
|
|
250
|
+
'30 10 * * *'
|
|
251
|
+
>>> interval_to_cron("1M@15:00") # Monthly at 15:00
|
|
252
|
+
'0 15 1 */1 * *'
|
|
253
|
+
>>> interval_to_cron("2Q@09:30:15") # Every 2 quarters at 9:30:15
|
|
254
|
+
'30 9 1 */6 * 15'
|
|
255
|
+
>>> interval_to_cron("Y@00:00") # Annually at midnight
|
|
256
|
+
'0 0 1 1 * *'
|
|
257
|
+
>>> interval_to_cron("TUE @ 23:59")
|
|
258
|
+
'59 23 * * 2'
|
|
259
|
+
"""
|
|
260
|
+
# - first try parsing as pandas Timedelta
|
|
261
|
+
try:
|
|
262
|
+
_td_inv = pd.Timedelta(inv)
|
|
263
|
+
return timedelta_to_crontab(_td_inv)
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
# - parse custom schedule format
|
|
268
|
+
try:
|
|
269
|
+
# - split into interval and time parts
|
|
270
|
+
interval, time = inv.split("@")
|
|
271
|
+
interval = interval.strip()
|
|
272
|
+
time = time.strip()
|
|
273
|
+
|
|
274
|
+
# - parse time
|
|
275
|
+
time_parts = time.split(":")
|
|
276
|
+
if len(time_parts) == 2:
|
|
277
|
+
hour, minute = time_parts
|
|
278
|
+
second = "0"
|
|
279
|
+
elif len(time_parts) == 3:
|
|
280
|
+
hour, minute, second = time_parts
|
|
281
|
+
else:
|
|
282
|
+
raise ValueError("Invalid time format")
|
|
283
|
+
|
|
284
|
+
# - parse interval
|
|
285
|
+
if not interval: # Default to 1 day if no interval specified
|
|
286
|
+
return f"{minute} {hour} * * * {second}"
|
|
287
|
+
|
|
288
|
+
match = re.match(r"^(\d+)?([A-Za-z]+)$", interval)
|
|
289
|
+
if not match:
|
|
290
|
+
raise ValueError(f"Invalid interval format: {interval}")
|
|
291
|
+
number = match.group(1) or "1"
|
|
292
|
+
unit = match.group(2).upper()
|
|
293
|
+
|
|
294
|
+
dow = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]
|
|
295
|
+
|
|
296
|
+
# - convert to cron expression
|
|
297
|
+
match unit:
|
|
298
|
+
case "Q": # Quarter
|
|
299
|
+
return f"{minute} {hour} 1 */{3 * int(number)} * {second}"
|
|
300
|
+
case "M": # Month
|
|
301
|
+
return f"{minute} {hour} 1 */{number} * {second}"
|
|
302
|
+
case "Y": # Year
|
|
303
|
+
return f"{minute} {hour} 1 1 * {second}"
|
|
304
|
+
case "SUN" | "MON" | "TUE" | "WED" | "THU" | "FRI" | "SAT": # Day of Week
|
|
305
|
+
return f"{minute} {hour} * * {dow.index(unit)} {second}"
|
|
306
|
+
case "D": # Day
|
|
307
|
+
return f"{minute} {hour} */{number} * * {second}"
|
|
308
|
+
case _:
|
|
309
|
+
raise ValueError(f"Invalid interval unit: {unit}")
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
raise ValueError(f"Invalid schedule format: {inv}") from e
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: Qubx
|
|
3
|
+
Version: 0.5.7
|
|
4
|
+
Summary: Qubx - Quantitative Trading Framework
|
|
5
|
+
Author: Dmitry Marienko
|
|
6
|
+
Author-email: dmitry.marienko@xlydian.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: ccxt (>=4.2.68,<5.0.0)
|
|
14
|
+
Requires-Dist: croniter (>=2.0.5,<3.0.0)
|
|
15
|
+
Requires-Dist: cython (==3.0.8)
|
|
16
|
+
Requires-Dist: dash (>=2.18.2,<3.0.0)
|
|
17
|
+
Requires-Dist: dash-bootstrap-components (>=1.6.0,<2.0.0)
|
|
18
|
+
Requires-Dist: importlib-metadata
|
|
19
|
+
Requires-Dist: jupyter-console (>=6.6.3,<7.0.0)
|
|
20
|
+
Requires-Dist: loguru (>=0.7.2,<0.8.0)
|
|
21
|
+
Requires-Dist: matplotlib (>=3.8.4,<4.0.0)
|
|
22
|
+
Requires-Dist: msgspec (>=0.18.6,<0.19.0)
|
|
23
|
+
Requires-Dist: ntplib (>=0.4.0,<0.5.0)
|
|
24
|
+
Requires-Dist: numba (>=0.59.1,<0.60.0)
|
|
25
|
+
Requires-Dist: numpy (>=1.26.3,<2.0.0)
|
|
26
|
+
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
27
|
+
Requires-Dist: plotly (>=5.22.0,<6.0.0)
|
|
28
|
+
Requires-Dist: psycopg (>=3.1.18,<4.0.0)
|
|
29
|
+
Requires-Dist: psycopg-binary (>=3.1.19,<4.0.0)
|
|
30
|
+
Requires-Dist: psycopg-pool (>=3.2.2,<4.0.0)
|
|
31
|
+
Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
|
|
32
|
+
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
|
33
|
+
Requires-Dist: pymongo (>=4.6.1,<5.0.0)
|
|
34
|
+
Requires-Dist: python-binance (>=1.0.19,<2.0.0)
|
|
35
|
+
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
36
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
|
37
|
+
Requires-Dist: scikit-learn (>=1.4.2,<2.0.0)
|
|
38
|
+
Requires-Dist: scipy (>=1.12.0,<2.0.0)
|
|
39
|
+
Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
|
|
40
|
+
Requires-Dist: stackprinter (>=0.2.10,<0.3.0)
|
|
41
|
+
Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
|
|
42
|
+
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
43
|
+
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
44
|
+
Requires-Dist: tqdm
|
|
45
|
+
Project-URL: Repository, https://github.com/xLydianSoftware/Qubx
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# Qubx
|
|
49
|
+
|
|
50
|
+
[](https://github.com/xLydianSoftware/Qubx/actions/workflows/ci.yml)
|
|
51
|
+
|
|
52
|
+
## Next generation of Qube quantitative backtesting framework (QUBX)
|
|
53
|
+
```
|
|
54
|
+
⠀⠀⡰⡖⠒⠒⢒⢦⠀⠀
|
|
55
|
+
⠀⢠⠃⠈⢆⣀⣎⣀⣱⡀ QUBX | Quantitative Backtesting Environment
|
|
56
|
+
⠀⢳⠒⠒⡞⠚⡄⠀⡰⠁ (c) 2024, by Dmytro Mariienko
|
|
57
|
+
⠀⠀⠱⣜⣀⣀⣈⣦⠃⠀⠀⠀
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
See [Qubx Documentation](https://xlydiansoftware.github.io/Qubx/en/latest/)
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
> pip install qubx
|
|
66
|
+
|
|
67
|
+
## How to run live trading (Only Binance spot tested)
|
|
68
|
+
1. cd experiments/
|
|
69
|
+
2. Edit strategy config file (zero_test.yaml). Testing strategy is just doing flip / flop trading once per minute (trading_allowed should be set for trading)
|
|
70
|
+
3. Modify accounts config file under ./configs/.env and provide your API binance credentials (see example in example-accounts.cfg):
|
|
71
|
+
```
|
|
72
|
+
[binance-mde]
|
|
73
|
+
apiKey = ...
|
|
74
|
+
secret = ...
|
|
75
|
+
base_currency = USDT
|
|
76
|
+
```
|
|
77
|
+
4. Run in console (-j key if want to run under jupyter console)
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
> python ..\src\qubx\utils\runner.py configs\zero_test.yaml -a configs\.env -j
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Running tests
|
|
84
|
+
We use `pytest` for running tests. For running unit tests execute
|
|
85
|
+
```
|
|
86
|
+
just test
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
We also have several integration tests (marked with `@pytest.mark.integration`), which mainly make sure that the exchange connectors function properly. We test them on the corresponding testnets, so you will need to generate api credentials for the exchange testnets that you want to verify.
|
|
90
|
+
|
|
91
|
+
Once you have the testnet credentials store them in an `.env.integration` file in the root of the Qubx directory
|
|
92
|
+
```
|
|
93
|
+
# BINANCE SPOT test credentials
|
|
94
|
+
BINANCE_SPOT_API_KEY=...
|
|
95
|
+
BINANCE_SPOT_SECRET=...
|
|
96
|
+
|
|
97
|
+
# BINANCE FUTURES test credentials
|
|
98
|
+
BINANCE_FUTURES_API_KEY=...
|
|
99
|
+
BINANCE_FUTURES_SECRET=...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To run the tests simply call
|
|
103
|
+
```
|
|
104
|
+
just test-integration
|
|
105
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
qubx/__init__.py,sha256=pfxwsCedCQJRMyLurWZGccVlUytAPNonVRa_cyBSbmw,7145
|
|
2
|
+
qubx/_nb_magic.py,sha256=r1Qd2khQhwIqgbuf2oALobEGNw0pGh_sBU3XSpK76UA,3025
|
|
3
|
+
qubx/backtester/__init__.py,sha256=OhXhLmj2x6sp6k16wm5IPATvv-E2qRZVIcvttxqPgcg,176
|
|
4
|
+
qubx/backtester/account.py,sha256=VBFiUMS3So1wVJCmQ3NtZ6Px1zMyi9hVjiq5Cn7sfm8,5838
|
|
5
|
+
qubx/backtester/broker.py,sha256=9Xm85OyLf-1hc2G1CcIPnatTMvFcdUTZSClJWc4quKU,2759
|
|
6
|
+
qubx/backtester/data.py,sha256=ArY8EDbgws_34Or1eHkgZgzU_QRNJrH3PaGXH7x3CfM,11967
|
|
7
|
+
qubx/backtester/management.py,sha256=HuyzFsBPgR7j-ei78Ngcx34CeSn65c9atmaii1aTsYg,14900
|
|
8
|
+
qubx/backtester/ome.py,sha256=BPb8iLJ-YpBhS4XJrrPzcj2RN8hmTNjkOjdAzDkNfXY,11055
|
|
9
|
+
qubx/backtester/optimization.py,sha256=HHUIYA6Y66rcOXoePWFOuOVX9iaHGKV0bGt_4d5e6FM,7619
|
|
10
|
+
qubx/backtester/simulated_data.py,sha256=xy4zDIF8bwum5F3mEachdIaQu9DOIfsBcR1AogF3CH0,22176
|
|
11
|
+
qubx/backtester/simulator.py,sha256=nkoCD3JQ6eUPxyo1e1lB7lLpd5_IwtB4oR7VpKaE0sI,14380
|
|
12
|
+
qubx/backtester/utils.py,sha256=V8ethHIUhs3o85q1ssWwJj1s6NplwVpdzOFIjluUd3A,30483
|
|
13
|
+
qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
qubx/cli/commands.py,sha256=SMl7Zax3OiWjB5M4fFh5w0bIVI4OLJrYUDsamjCe7_w,2276
|
|
15
|
+
qubx/connectors/ccxt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
qubx/connectors/ccxt/account.py,sha256=0-NyqDVEu6vim9rBgO4L9Ek3EKHmq2A82eTKpoOSyFM,21630
|
|
17
|
+
qubx/connectors/ccxt/broker.py,sha256=I91BRQBbVrZm9YGp6AkW_qHSQv-6Qf0H2Rt53Pmh4rk,4114
|
|
18
|
+
qubx/connectors/ccxt/customizations.py,sha256=K5MoNuatjmyOcDhQ_54vdp8D1cb_QgfkoeHdBGJxiOQ,7709
|
|
19
|
+
qubx/connectors/ccxt/data.py,sha256=RekxCqjRBOTY1IEkNM2aNvdXwm-rHoRs7hT2vk4lrI0,25291
|
|
20
|
+
qubx/connectors/ccxt/exceptions.py,sha256=OfZc7iMdEG8uLorcZta2NuEuJrSIqi0FG7IICmwF54M,262
|
|
21
|
+
qubx/connectors/ccxt/factory.py,sha256=KROJ9-7zfoKZuess99UZMzgsZakISbN3XDOkEB-Ev4s,2818
|
|
22
|
+
qubx/connectors/ccxt/utils.py,sha256=Hn1gK3BCmwbivVjAmwaBy4zQPqwZl0jWbNwdV5_E0po,10612
|
|
23
|
+
qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
qubx/core/account.py,sha256=MOrllpuZLyaJ1EwsEJst0GaxcC7Z7XMT-VF7OR-1NMQ,10213
|
|
25
|
+
qubx/core/basics.py,sha256=7CaER2uM73840CUNe9ej9I6BzSkwgydM-RQwVsL5LZ0,27780
|
|
26
|
+
qubx/core/context.py,sha256=nOzB_FFzGwjtYo7o8lwBEnJASr1fkd9B_Cc3SCcy7Kw,15616
|
|
27
|
+
qubx/core/exceptions.py,sha256=Jidp6v8rF6bCGB4SDNPt5CMHltkd9tbVkHzOvM29KdU,477
|
|
28
|
+
qubx/core/helpers.py,sha256=7nhO-CgleU6RTXpSwCdMwb0ZwLCYi5hJWnag8kfFDXo,17701
|
|
29
|
+
qubx/core/interfaces.py,sha256=giiZAZpNsj5cUCEJMRv9IY1LKqTkbKS41xYksAI24uE,34369
|
|
30
|
+
qubx/core/loggers.py,sha256=STXvRvDsi9NTHdEzQFsedbvg71aB0ixjNfNZ2QvIT9w,17595
|
|
31
|
+
qubx/core/lookups.py,sha256=UN136wkw-51MBm61WAMaEvFfhjM63oCRkEsoELd8BIU,15686
|
|
32
|
+
qubx/core/metrics.py,sha256=7b9kza1Fu3aGSf4llUiNTIGT1wsRDOaProK5lGk_a5M,57877
|
|
33
|
+
qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
|
|
34
|
+
qubx/core/mixins/market.py,sha256=s1NQDUjex7LR_ShnbSA3VnPMZpP7NmCgax5cmHdTmh4,3251
|
|
35
|
+
qubx/core/mixins/processing.py,sha256=dOERoe2TkNPihuXpb1lhawU78gCIdUukZNWThc-JeJY,18097
|
|
36
|
+
qubx/core/mixins/subscription.py,sha256=J_SX0CNw2bPy4bhxe0vswvDXY4LCkwXSaj_1PepKRLY,8540
|
|
37
|
+
qubx/core/mixins/trading.py,sha256=CQQIp1t1LJiFph5CiHQR4k4vxTymjFqrkA0awKYn4Dw,3224
|
|
38
|
+
qubx/core/mixins/universe.py,sha256=1ya2P3QZrsAVXmMXqq0t6CHGAC5iMGVD2ARUAtSfv04,10062
|
|
39
|
+
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=mpLWRTkmp3hepypAn87T4_b21k86StgccuOHRbxm-P0,882248
|
|
40
|
+
qubx/core/series.pxd,sha256=EqgYT41FrpVB274mDG3jpLCSqK_ykkL-d-1IH8DE1ik,3301
|
|
41
|
+
qubx/core/series.pyi,sha256=zBt8DQCiIdTU3MLJz_9MlrONo7UCVYh2xYUltAcAj6c,3247
|
|
42
|
+
qubx/core/series.pyx,sha256=4XCRdH3otXsU8EJ-g4_zLQfhqR8TVjtEq_e4oDz5mZ4,33836
|
|
43
|
+
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=zoDD-UQ8Je50_INgdV0PK7CzKTQQ9nSPgs3nZo60obg,86568
|
|
44
|
+
qubx/core/utils.pyi,sha256=DAjyRVPJSxK4Em-9wui2F0yYHfP5tI5DjKavXNOnHa8,276
|
|
45
|
+
qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
|
|
46
|
+
qubx/data/__init__.py,sha256=CTbEWfMC3eVfD4v6OdhEH4AXGNybrnJJ-mxOM-n2e_M,482
|
|
47
|
+
qubx/data/helpers.py,sha256=GZIQJk5m1rbCX-_heZmJrMZeTpElPT88vGosUIWDuKI,16660
|
|
48
|
+
qubx/data/readers.py,sha256=RedH9MyOzrYjS3EXNwM_y4nRKbYJtU0Gs6---RFdrBA,55528
|
|
49
|
+
qubx/data/tardis.py,sha256=LzKSjCEhAupMYlB46SWUo71zSKhSwh26GnGHHxhb9MQ,3769
|
|
50
|
+
qubx/gathering/simplest.py,sha256=2BXyzj0wHohVYT1E4Rqwdf9K_gZPoZ_eW9MSe0uliBo,3959
|
|
51
|
+
qubx/math/__init__.py,sha256=ltHSQj40sCBm3owcvtoZp34h6ws7pZCFcSZgUkTsUCY,114
|
|
52
|
+
qubx/math/stats.py,sha256=dLfomw5uBYolurxNPKxcGoL27pTEqiTnjI5cZ_-9-gU,4021
|
|
53
|
+
qubx/pandaz/__init__.py,sha256=vSUWqD3XhPq96Kc3Q_msjdRFEjFB-moSnFWxlCf5gkw,460
|
|
54
|
+
qubx/pandaz/ta.py,sha256=ONqPOeqtAkjKWolO9gWbbfrS2bDDB_kzV8aBu_yDVEE,91608
|
|
55
|
+
qubx/pandaz/utils.py,sha256=zAHUIAApSRrlQa5AjpIbiQ9ftSGIBOu_ppDg0c3gXaE,23380
|
|
56
|
+
qubx/resources/instruments/symbols-binance.cm.json,sha256=rNI3phNeeRY95_IE7_0Um9d5U4jUtEijZQ_PaYg5cdw,25127
|
|
57
|
+
qubx/resources/instruments/symbols-binance.json,sha256=Qx_XckgsWNhmmV8_t5DpG0AeGkuTyt1uiif2EeeBDIg,939678
|
|
58
|
+
qubx/resources/instruments/symbols-binance.um.json,sha256=eroAZmpc_Iez0uDxsk1rcnSmznuQ9eEoCPQHlNyTfI8,227785
|
|
59
|
+
qubx/resources/instruments/symbols-bitfinex.f.json,sha256=URpMkOM4hB4-6-RY1yJU-fSPADwE7F-EOGmxWEjqspo,34758
|
|
60
|
+
qubx/resources/instruments/symbols-bitfinex.json,sha256=CpzoVgWzGZRN6RpUNhtJVxa3SeSvLVx5Q8GYHfYwd8s,263231
|
|
61
|
+
qubx/resources/instruments/symbols-kraken.f.json,sha256=lwNqml3H7lNUl1h3siySSyE1MRcGfqfhb6BcxLsiKr0,212258
|
|
62
|
+
qubx/resources/instruments/symbols-kraken.json,sha256=RjUTvkQuuu7V1HfSQREvnA4qqkdkB3-rzykDaQds2rQ,456544
|
|
63
|
+
qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
+
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=EIsiwKN8_U6jxWISbKLKfmrrXMBY5ZUswPuLM-slv-g,654440
|
|
65
|
+
qubx/ta/indicators.pxd,sha256=eCJ9paOxtxbDFx4U5CUhcgB1jjCQAfVqMF2FnbJ03Lo,4222
|
|
66
|
+
qubx/ta/indicators.pyi,sha256=NJlvN_774UV1U3_lvaYYbCEikLR8sOUo0TdcUGR5GBM,1940
|
|
67
|
+
qubx/ta/indicators.pyx,sha256=FVkv5ld04TpZMT3a_kR1MU3IUuWfijzjJnh_lG78JxM,26029
|
|
68
|
+
qubx/trackers/__init__.py,sha256=Df2YGR6SM9ANfMQDV21Q6Nh0MO-tLIt4Yt42v6HR3EI,175
|
|
69
|
+
qubx/trackers/abvanced.py,sha256=vo4DuX6sYzsXLcp5z1UYuGowlJEE47vzmSoKsMLBPu4,10307
|
|
70
|
+
qubx/trackers/composite.py,sha256=W-n1vd4l-RZEoojj6lICqvJ8EgTV2kE6JUUmZUkZ1cI,6339
|
|
71
|
+
qubx/trackers/rebalancers.py,sha256=5Dx39QZ67iZVx-cfpYx4IoMgDd7-fCHvGkwtezL7ofY,5269
|
|
72
|
+
qubx/trackers/riskctrl.py,sha256=QyYbXZ7NGJYpNK1i92z9PlCvWbiKMb-TTuj6xs1PzbA,26799
|
|
73
|
+
qubx/trackers/sizers.py,sha256=r2w8HOS2_LRZEi_WjU0KRXqxmfhi2JNhbtOpfqmywRk,9608
|
|
74
|
+
qubx/utils/__init__.py,sha256=pIS1ulI6Hj8btZlPd5P9To7DlyEY20bEVvFREAZkR0A,384
|
|
75
|
+
qubx/utils/_pyxreloader.py,sha256=FyqGzfSpZGYziB8JYS5AP3cLRAvJSIPAKgwQn0E4YQ0,12017
|
|
76
|
+
qubx/utils/charting/lookinglass.py,sha256=m7lWU8c0E8tXzGbkN0GB8CL-kd92MnH_wD8cATX067k,39232
|
|
77
|
+
qubx/utils/charting/mpl_helpers.py,sha256=e9XNnEvUpFJtB3zpgJCyMSeUkKbBfumJhnWLCXmU1k0,36085
|
|
78
|
+
qubx/utils/marketdata/binance.py,sha256=_Hm2KtMFrUOguHlT3ZhRZFmoszG8ivRh72vCUZWhssU,11224
|
|
79
|
+
qubx/utils/marketdata/ccxt.py,sha256=IhTT8prs42x_pIhFKv2AweFGIdEh2kGZYacbjXSmxV0,3466
|
|
80
|
+
qubx/utils/marketdata/dukas.py,sha256=yz4dfOM4UDuz2BYeM8vlTzaZX83VPx_OW-mGwGr-L4E,3237
|
|
81
|
+
qubx/utils/misc.py,sha256=xwAPcU9H50jc3NEOJizOfrZD9unrtVFR16QApS66xGM,16218
|
|
82
|
+
qubx/utils/ntp.py,sha256=yNurgbdiqKhq_dVrJ5PRnho9SzT3ijQG-Bi2sDnFSLs,1904
|
|
83
|
+
qubx/utils/numbers_utils.py,sha256=pAXZsurOOXwoFdPJPm0muSY1RkFhFHYjuB-Dzx6Ed48,240
|
|
84
|
+
qubx/utils/orderbook.py,sha256=Lz9TqYRiHmfUA1HcmKgtmiZKKvOtn-ugX8d81s4xyMU,18258
|
|
85
|
+
qubx/utils/plotting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
86
|
+
qubx/utils/plotting/dashboard.py,sha256=NB236RONucjcPr3NWAMzUGs9sIY6eKsPdBXi8PkXODc,5547
|
|
87
|
+
qubx/utils/plotting/data.py,sha256=ZOg8rHAq4NVmfsyhvzFHtey4HaXywAHufxhv1IExRqg,4773
|
|
88
|
+
qubx/utils/plotting/interfaces.py,sha256=mtRcoWIMt2xkv-Tc2ZgXZQpr5HRiWhPcqkIslzZTeig,493
|
|
89
|
+
qubx/utils/plotting/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
+
qubx/utils/plotting/renderers/plotly.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
91
|
+
qubx/utils/runner/__init__.py,sha256=axs9MF78BYk30jhHBu0gSXIr-IN5ZOzoprlJ_N85yN8,77
|
|
92
|
+
qubx/utils/runner/_jupyter_runner.pyt,sha256=0SSc9F6caok_uRy9Qzy3L7hEuebZykH6U5QEM9YnhZU,2321
|
|
93
|
+
qubx/utils/runner/accounts.py,sha256=3D9bqqG4MWVRw2YJ5iT1RgmyGRdTEBr7BDk1UephIUo,3237
|
|
94
|
+
qubx/utils/runner/configs.py,sha256=nQXU1oqtSSGpGHw4cqk1dVpcojibj7bzjWZbDAHRxNc,1741
|
|
95
|
+
qubx/utils/runner/runner.py,sha256=As5tMRilyK9Eh2_NlQdEibXBxhEF3QR1fOidjvrwrPg,17894
|
|
96
|
+
qubx/utils/time.py,sha256=1Cvh077Uqf-XjcE5nWp_T9JzFVT6i39kU7Qz-ssHKIo,9630
|
|
97
|
+
qubx-0.5.7.dist-info/METADATA,sha256=YO2DNAvYGdPWm-2r8SJOr-8S0Goskz1tKIpPiNzhYy0,3853
|
|
98
|
+
qubx-0.5.7.dist-info/WHEEL,sha256=h1DdjcD2ZFnKGsDLjEycQhNNPJ5l-R8qdFdDSXHrAGY,110
|
|
99
|
+
qubx-0.5.7.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
|
|
100
|
+
qubx-0.5.7.dist-info/RECORD,,
|