Qubx 0.6.35__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.37__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/connectors/ccxt/broker.py +68 -44
- qubx/connectors/ccxt/exchanges/__init__.py +1 -1
- qubx/connectors/ccxt/exchanges/binance/exchange.py +7 -2
- qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +23 -8
- qubx/connectors/ccxt/utils.py +2 -2
- qubx/connectors/tardis/data.py +1 -1
- qubx/core/context.py +13 -2
- qubx/core/errors.py +19 -0
- qubx/core/interfaces.py +11 -2
- qubx/core/metrics.py +26 -4
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/data/hft.py +37 -9
- qubx/data/readers.py +22 -22
- qubx/features/core.py +8 -7
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/runner/_jupyter_runner.pyt +8 -1
- qubx/utils/runner/runner.py +1 -1
- {qubx-0.6.35.dist-info → qubx-0.6.37.dist-info}/METADATA +1 -1
- {qubx-0.6.35.dist-info → qubx-0.6.37.dist-info}/RECORD +23 -23
- {qubx-0.6.35.dist-info → qubx-0.6.37.dist-info}/LICENSE +0 -0
- {qubx-0.6.35.dist-info → qubx-0.6.37.dist-info}/WHEEL +0 -0
- {qubx-0.6.35.dist-info → qubx-0.6.37.dist-info}/entry_points.txt +0 -0
qubx/connectors/ccxt/broker.py
CHANGED
|
@@ -14,7 +14,7 @@ from qubx.core.basics import (
|
|
|
14
14
|
Order,
|
|
15
15
|
OrderSide,
|
|
16
16
|
)
|
|
17
|
-
from qubx.core.errors import OrderCancellationError, OrderCreationError, create_error_event
|
|
17
|
+
from qubx.core.errors import ErrorLevel, OrderCancellationError, OrderCreationError, create_error_event
|
|
18
18
|
from qubx.core.exceptions import BadRequest, InvalidOrderParameters
|
|
19
19
|
from qubx.core.interfaces import (
|
|
20
20
|
IAccountProcessor,
|
|
@@ -61,6 +61,57 @@ class CcxtBroker(IBroker):
|
|
|
61
61
|
def is_simulated_trading(self) -> bool:
|
|
62
62
|
return False
|
|
63
63
|
|
|
64
|
+
def _post_order_error_to_databus(
|
|
65
|
+
self,
|
|
66
|
+
error: Exception,
|
|
67
|
+
instrument: Instrument,
|
|
68
|
+
order_side: OrderSide,
|
|
69
|
+
order_type: str,
|
|
70
|
+
amount: float,
|
|
71
|
+
price: float | None,
|
|
72
|
+
client_id: str | None,
|
|
73
|
+
time_in_force: str,
|
|
74
|
+
**options,
|
|
75
|
+
):
|
|
76
|
+
level = ErrorLevel.LOW
|
|
77
|
+
match error:
|
|
78
|
+
case ccxt.InsufficientFunds():
|
|
79
|
+
level = ErrorLevel.HIGH
|
|
80
|
+
logger.error(
|
|
81
|
+
f"(::create_order) INSUFFICIENT FUNDS for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
82
|
+
)
|
|
83
|
+
case ccxt.OrderNotFillable():
|
|
84
|
+
level = ErrorLevel.LOW
|
|
85
|
+
logger.error(
|
|
86
|
+
f"(::create_order) ORDER NOT FILLEABLE for {order_side} {amount} {order_type} for [{instrument.symbol}] : {error}"
|
|
87
|
+
)
|
|
88
|
+
case ccxt.InvalidOrder():
|
|
89
|
+
level = ErrorLevel.LOW
|
|
90
|
+
logger.error(
|
|
91
|
+
f"(::create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
92
|
+
)
|
|
93
|
+
case ccxt.BadRequest():
|
|
94
|
+
level = ErrorLevel.LOW
|
|
95
|
+
logger.error(
|
|
96
|
+
f"(::create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
97
|
+
)
|
|
98
|
+
case _:
|
|
99
|
+
level = ErrorLevel.MEDIUM
|
|
100
|
+
logger.error(f"(::create_order) Unexpected error: {error}")
|
|
101
|
+
|
|
102
|
+
error_event = OrderCreationError(
|
|
103
|
+
timestamp=self.time_provider.time(),
|
|
104
|
+
message=f"Error message: {str(error)}",
|
|
105
|
+
level=level,
|
|
106
|
+
instrument=instrument,
|
|
107
|
+
amount=amount,
|
|
108
|
+
price=price,
|
|
109
|
+
order_type=order_type,
|
|
110
|
+
side=order_side,
|
|
111
|
+
error=error,
|
|
112
|
+
)
|
|
113
|
+
self.channel.send(create_error_event(error_event))
|
|
114
|
+
|
|
64
115
|
def send_order_async(
|
|
65
116
|
self,
|
|
66
117
|
instrument: Instrument,
|
|
@@ -93,33 +144,20 @@ class CcxtBroker(IBroker):
|
|
|
93
144
|
)
|
|
94
145
|
|
|
95
146
|
if error:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
timestamp=self.time_provider.time(),
|
|
99
|
-
message=str(error),
|
|
100
|
-
instrument=instrument,
|
|
101
|
-
amount=amount,
|
|
102
|
-
price=price,
|
|
103
|
-
order_type=order_type,
|
|
104
|
-
side=order_side,
|
|
147
|
+
self._post_order_error_to_databus(
|
|
148
|
+
error, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
105
149
|
)
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
order = None
|
|
151
|
+
|
|
108
152
|
return order
|
|
153
|
+
|
|
109
154
|
except Exception as err:
|
|
110
155
|
# Catch any unexpected errors and send them through the channel as well
|
|
111
|
-
logger.error(f"Unexpected error in async order creation: {err}")
|
|
156
|
+
logger.error(f"{self.__class__.__name__} :: Unexpected error in async order creation: {err}")
|
|
112
157
|
logger.error(traceback.format_exc())
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
message=f"Unexpected error: {str(err)}",
|
|
116
|
-
instrument=instrument,
|
|
117
|
-
amount=amount,
|
|
118
|
-
price=price,
|
|
119
|
-
order_type=order_type,
|
|
120
|
-
side=order_side,
|
|
158
|
+
self._post_order_error_to_databus(
|
|
159
|
+
err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
121
160
|
)
|
|
122
|
-
self.channel.send(create_error_event(error_event))
|
|
123
161
|
return None
|
|
124
162
|
|
|
125
163
|
# Submit the task to the async loop
|
|
@@ -135,7 +173,7 @@ class CcxtBroker(IBroker):
|
|
|
135
173
|
client_id: str | None = None,
|
|
136
174
|
time_in_force: str = "gtc",
|
|
137
175
|
**options,
|
|
138
|
-
) -> Order:
|
|
176
|
+
) -> Order | None:
|
|
139
177
|
"""
|
|
140
178
|
Submit an order and wait for the result. Exceptions will be raised on errors.
|
|
141
179
|
|
|
@@ -169,14 +207,16 @@ class CcxtBroker(IBroker):
|
|
|
169
207
|
|
|
170
208
|
# If there was no error but also no order, something went wrong
|
|
171
209
|
if not order and not self.enable_create_order_ws:
|
|
172
|
-
raise ExchangeError("Order creation failed with no specific error")
|
|
210
|
+
raise ExchangeError(f"{self.__class__.__name__} :: Order creation failed with no specific error")
|
|
173
211
|
|
|
174
212
|
return order
|
|
175
213
|
|
|
176
214
|
except Exception as err:
|
|
177
215
|
# This will catch any errors from future.result() or if we explicitly raise an error
|
|
178
|
-
|
|
179
|
-
|
|
216
|
+
self._post_order_error_to_databus(
|
|
217
|
+
err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
218
|
+
)
|
|
219
|
+
return None
|
|
180
220
|
|
|
181
221
|
def cancel_order(self, order_id: str) -> Order | None:
|
|
182
222
|
orders = self.account.get_orders()
|
|
@@ -231,25 +271,7 @@ class CcxtBroker(IBroker):
|
|
|
231
271
|
logger.info(f"New order {order}")
|
|
232
272
|
return order, None
|
|
233
273
|
|
|
234
|
-
except ccxt.OrderNotFillable as exc:
|
|
235
|
-
logger.error(
|
|
236
|
-
f"(::_create_order) [{instrument.symbol}] ORDER NOT FILLEABLE for {order_side} {amount} {order_type} : {exc}"
|
|
237
|
-
)
|
|
238
|
-
return None, exc
|
|
239
|
-
except ccxt.InvalidOrder as exc:
|
|
240
|
-
logger.error(
|
|
241
|
-
f"(::_create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {exc}"
|
|
242
|
-
)
|
|
243
|
-
return None, exc
|
|
244
|
-
except ccxt.BadRequest as exc:
|
|
245
|
-
logger.error(
|
|
246
|
-
f"(::_create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {exc}"
|
|
247
|
-
)
|
|
248
|
-
return None, exc
|
|
249
274
|
except Exception as err:
|
|
250
|
-
logger.error(
|
|
251
|
-
f"(::_create_order) {order_side} {amount} {order_type} for {instrument.symbol} exception : {err}"
|
|
252
|
-
)
|
|
253
275
|
return None, err
|
|
254
276
|
|
|
255
277
|
def _prepare_order_payload(
|
|
@@ -371,6 +393,8 @@ class CcxtBroker(IBroker):
|
|
|
371
393
|
order_id=order_id,
|
|
372
394
|
message=f"Timeout reached for canceling order {order_id}",
|
|
373
395
|
instrument=instrument,
|
|
396
|
+
level=ErrorLevel.LOW,
|
|
397
|
+
error=None,
|
|
374
398
|
)
|
|
375
399
|
)
|
|
376
400
|
)
|
|
@@ -25,7 +25,7 @@ EXCHANGE_ALIASES = {
|
|
|
25
25
|
|
|
26
26
|
CUSTOM_BROKERS = {
|
|
27
27
|
"binance": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
28
|
-
"binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=
|
|
28
|
+
"binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
|
|
29
29
|
"binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
30
30
|
"binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
|
|
31
31
|
"bitfinex.f": partial(CcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
|
|
@@ -3,7 +3,7 @@ from typing import Dict, List
|
|
|
3
3
|
import ccxt.pro as cxp
|
|
4
4
|
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp
|
|
5
5
|
from ccxt.async_support.base.ws.client import Client
|
|
6
|
-
from ccxt.base.errors import ArgumentsRequired, BadRequest, NotSupported
|
|
6
|
+
from ccxt.base.errors import ArgumentsRequired, BadRequest, InsufficientFunds, NotSupported
|
|
7
7
|
from ccxt.base.precise import Precise
|
|
8
8
|
from ccxt.base.types import (
|
|
9
9
|
Any,
|
|
@@ -34,7 +34,12 @@ class BinanceQV(cxp.binance):
|
|
|
34
34
|
"name": "aggTrade",
|
|
35
35
|
},
|
|
36
36
|
"localOrderBookLimit": 10_000, # set a large limit to avoid cutting off the orderbook
|
|
37
|
-
}
|
|
37
|
+
},
|
|
38
|
+
"exceptions": {
|
|
39
|
+
"exact": {
|
|
40
|
+
"-2019": InsufficientFunds, # ccxt doesn't have this code for some weird reason !!
|
|
41
|
+
},
|
|
42
|
+
},
|
|
38
43
|
},
|
|
39
44
|
)
|
|
40
45
|
|
|
@@ -78,6 +78,10 @@ class BitfinexF(cxp.bitfinex):
|
|
|
78
78
|
# GTX is not supported by bitfinex, so we need to convert it to PO
|
|
79
79
|
params["timeInForce"] = "PO"
|
|
80
80
|
params["postOnly"] = True
|
|
81
|
+
|
|
82
|
+
if "lev" not in params:
|
|
83
|
+
params["lev"] = 2
|
|
84
|
+
|
|
81
85
|
response = await super().create_order(symbol, type, side, amount, price, params)
|
|
82
86
|
return response
|
|
83
87
|
|
|
@@ -90,6 +94,9 @@ class BitfinexF(cxp.bitfinex):
|
|
|
90
94
|
params["timeInForce"] = "PO"
|
|
91
95
|
params["postOnly"] = True
|
|
92
96
|
|
|
97
|
+
if "lev" not in params:
|
|
98
|
+
params["lev"] = 2
|
|
99
|
+
|
|
93
100
|
await self.load_markets()
|
|
94
101
|
market = self.market(symbol)
|
|
95
102
|
request = self.create_order_request(symbol, type, side, amount, price, params)
|
|
@@ -98,14 +105,22 @@ class BitfinexF(cxp.bitfinex):
|
|
|
98
105
|
# request["cid"] = request["newClientOrderId"]
|
|
99
106
|
# del request["newClientOrderId"]
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
type
|
|
103
|
-
symbol
|
|
104
|
-
amount
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
_params = {
|
|
109
|
+
"type": request["type"],
|
|
110
|
+
"symbol": request["symbol"],
|
|
111
|
+
"amount": float(request["amount"]),
|
|
112
|
+
"lev": request["lev"],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if "price" in request:
|
|
116
|
+
_params["price"] = float(request["price"])
|
|
117
|
+
else:
|
|
118
|
+
_params["price"] = None
|
|
119
|
+
|
|
120
|
+
if "flags" in request:
|
|
121
|
+
_params["flags"] = request["flags"]
|
|
122
|
+
|
|
123
|
+
await self.bfx.wss.inputs.submit_order(**_params)
|
|
109
124
|
return self.safe_order({"info": {}}, market) # type: ignore
|
|
110
125
|
|
|
111
126
|
async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order | None:
|
qubx/connectors/ccxt/utils.py
CHANGED
|
@@ -54,7 +54,7 @@ def ccxt_convert_order_info(instrument: Instrument, raw: dict[str, Any]) -> Orde
|
|
|
54
54
|
type=_type,
|
|
55
55
|
instrument=instrument,
|
|
56
56
|
time=pd.Timestamp(raw["timestamp"], unit="ms"), # type: ignore
|
|
57
|
-
quantity=amnt,
|
|
57
|
+
quantity=abs(amnt) * (-1 if side == "SELL" else 1),
|
|
58
58
|
price=float(price) if price is not None else 0.0,
|
|
59
59
|
side=side,
|
|
60
60
|
status=status.upper(),
|
|
@@ -157,7 +157,7 @@ def ccxt_convert_positions(
|
|
|
157
157
|
)
|
|
158
158
|
pos = Position(
|
|
159
159
|
instrument=instr,
|
|
160
|
-
quantity=info["contracts"] * (-1 if info["side"] == "short" else 1),
|
|
160
|
+
quantity=abs(info["contracts"]) * (-1 if info["side"] == "short" else 1),
|
|
161
161
|
pos_average_price=info["entryPrice"],
|
|
162
162
|
)
|
|
163
163
|
if info.get("markPrice", None) is not None:
|
qubx/connectors/tardis/data.py
CHANGED
|
@@ -454,7 +454,7 @@ class TardisDataProvider(IDataProvider):
|
|
|
454
454
|
|
|
455
455
|
# Record data arrival for health monitoring
|
|
456
456
|
tardis_type = data["type"]
|
|
457
|
-
tardis_name = data["name"]
|
|
457
|
+
tardis_name = data["name"] if "name" in data else ""
|
|
458
458
|
qubx_type = self._map_tardis_type_to_data_type(tardis_type)
|
|
459
459
|
if qubx_type:
|
|
460
460
|
self._health_monitor.record_data_arrival(qubx_type, dt_64(msg_time, "ns"))
|
qubx/core/context.py
CHANGED
|
@@ -16,6 +16,7 @@ from qubx.core.basics import (
|
|
|
16
16
|
Timestamped,
|
|
17
17
|
dt_64,
|
|
18
18
|
)
|
|
19
|
+
from qubx.core.errors import BaseErrorEvent, ErrorLevel
|
|
19
20
|
from qubx.core.exceptions import StrategyExceededMaxNumberOfRuntimeFailuresError
|
|
20
21
|
from qubx.core.helpers import (
|
|
21
22
|
BasicScheduler,
|
|
@@ -286,7 +287,7 @@ class StrategyContext(IStrategyContext):
|
|
|
286
287
|
|
|
287
288
|
# - invoke strategy's stop code
|
|
288
289
|
try:
|
|
289
|
-
if not self.
|
|
290
|
+
if not self.is_warmup_in_progress:
|
|
290
291
|
self.strategy.on_stop(self)
|
|
291
292
|
except Exception as strat_error:
|
|
292
293
|
logger.error(
|
|
@@ -327,7 +328,7 @@ class StrategyContext(IStrategyContext):
|
|
|
327
328
|
return self._data_providers[0].is_simulation
|
|
328
329
|
|
|
329
330
|
@property
|
|
330
|
-
def
|
|
331
|
+
def is_paper_trading(self) -> bool:
|
|
331
332
|
return self._brokers[0].is_simulated_trading
|
|
332
333
|
|
|
333
334
|
# IAccountViewer delegation
|
|
@@ -536,6 +537,16 @@ class StrategyContext(IStrategyContext):
|
|
|
536
537
|
if _should_record:
|
|
537
538
|
self._health_monitor.record_start_processing(d_type, dt_64(data.time, "ns"))
|
|
538
539
|
|
|
540
|
+
# - notify error if error level is medium or higher
|
|
541
|
+
if (
|
|
542
|
+
self._lifecycle_notifier
|
|
543
|
+
and isinstance(data, BaseErrorEvent)
|
|
544
|
+
and data.level.value >= ErrorLevel.MEDIUM.value
|
|
545
|
+
):
|
|
546
|
+
self._lifecycle_notifier.notify_error(
|
|
547
|
+
self._strategy_name, data.error or Exception("Unknown error"), {"message": str(data)}
|
|
548
|
+
)
|
|
549
|
+
|
|
539
550
|
if self.process_data(instrument, d_type, data, hist):
|
|
540
551
|
channel.stop()
|
|
541
552
|
break
|
qubx/core/errors.py
CHANGED
|
@@ -3,14 +3,27 @@ Error types that are sent through the event channel.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
6
7
|
|
|
7
8
|
from qubx.core.basics import Instrument, dt_64
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
class ErrorLevel(Enum):
|
|
12
|
+
LOW = 1 # continue trading
|
|
13
|
+
MEDIUM = 2 # send notifications and continue trading
|
|
14
|
+
HIGH = 3 # send notification and cancel orders and close positions
|
|
15
|
+
CRITICAL = 4 # send notification and shutdown strategy
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
@dataclass
|
|
11
19
|
class BaseErrorEvent:
|
|
12
20
|
timestamp: dt_64
|
|
13
21
|
message: str
|
|
22
|
+
level: ErrorLevel
|
|
23
|
+
error: Exception | None
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error}"
|
|
14
27
|
|
|
15
28
|
|
|
16
29
|
def create_error_event(error: BaseErrorEvent) -> tuple[None, str, BaseErrorEvent, bool]:
|
|
@@ -25,8 +38,14 @@ class OrderCreationError(BaseErrorEvent):
|
|
|
25
38
|
order_type: str
|
|
26
39
|
side: str
|
|
27
40
|
|
|
41
|
+
def __str__(self):
|
|
42
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error} ||| Order creation error for {self.order_type} {self.side} {self.instrument} {self.amount}"
|
|
43
|
+
|
|
28
44
|
|
|
29
45
|
@dataclass
|
|
30
46
|
class OrderCancellationError(BaseErrorEvent):
|
|
31
47
|
instrument: Instrument
|
|
32
48
|
order_id: str
|
|
49
|
+
|
|
50
|
+
def __str__(self):
|
|
51
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error} ||| Order cancellation error for {self.order_id} {self.instrument}"
|
qubx/core/interfaces.py
CHANGED
|
@@ -1068,7 +1068,6 @@ class IStrategyContext(
|
|
|
1068
1068
|
IProcessingManager,
|
|
1069
1069
|
IAccountViewer,
|
|
1070
1070
|
IWarmupStateSaver,
|
|
1071
|
-
StrategyState,
|
|
1072
1071
|
):
|
|
1073
1072
|
strategy: "IStrategy"
|
|
1074
1073
|
initializer: "IStrategyInitializer"
|
|
@@ -1086,17 +1085,27 @@ class IStrategyContext(
|
|
|
1086
1085
|
"""Stop the strategy context."""
|
|
1087
1086
|
pass
|
|
1088
1087
|
|
|
1088
|
+
@property
|
|
1089
|
+
def state(self) -> StrategyState:
|
|
1090
|
+
"""Get the strategy state."""
|
|
1091
|
+
return StrategyState(**self._strategy_state.__dict__)
|
|
1092
|
+
|
|
1089
1093
|
def is_running(self) -> bool:
|
|
1090
1094
|
"""Check if the strategy context is running."""
|
|
1091
1095
|
return False
|
|
1092
1096
|
|
|
1097
|
+
@property
|
|
1098
|
+
def is_warmup_in_progress(self) -> bool:
|
|
1099
|
+
"""Check if the warmup is in progress."""
|
|
1100
|
+
return self._strategy_state.is_warmup_in_progress
|
|
1101
|
+
|
|
1093
1102
|
@property
|
|
1094
1103
|
def is_simulation(self) -> bool:
|
|
1095
1104
|
"""Check if the strategy context is running in simulation mode."""
|
|
1096
1105
|
return False
|
|
1097
1106
|
|
|
1098
1107
|
@property
|
|
1099
|
-
def
|
|
1108
|
+
def is_paper_trading(self) -> bool:
|
|
1100
1109
|
"""Check if the strategy context is running in simulated trading mode."""
|
|
1101
1110
|
return False
|
|
1102
1111
|
|
qubx/core/metrics.py
CHANGED
|
@@ -717,7 +717,7 @@ class TradingSessionResult:
|
|
|
717
717
|
"name": self.name,
|
|
718
718
|
"start": pd.Timestamp(self.start).isoformat(),
|
|
719
719
|
"stop": pd.Timestamp(self.stop).isoformat(),
|
|
720
|
-
"
|
|
720
|
+
"exchanges": self.exchanges,
|
|
721
721
|
"capital": self.capital,
|
|
722
722
|
"base_currency": self.base_currency,
|
|
723
723
|
"commissions": self.commissions,
|
|
@@ -824,6 +824,12 @@ class TradingSessionResult:
|
|
|
824
824
|
info = self.info()
|
|
825
825
|
if description:
|
|
826
826
|
info["description"] = description
|
|
827
|
+
# - set name if not specified
|
|
828
|
+
if info.get("name") is None:
|
|
829
|
+
info["name"] = name
|
|
830
|
+
|
|
831
|
+
# - add numpy array representer
|
|
832
|
+
yaml.SafeDumper.add_representer(np.ndarray, lambda dumper, data: dumper.represent_list(data.tolist()))
|
|
827
833
|
yaml.safe_dump(info, f, sort_keys=False, indent=4)
|
|
828
834
|
|
|
829
835
|
# - save logs
|
|
@@ -855,15 +861,31 @@ class TradingSessionResult:
|
|
|
855
861
|
|
|
856
862
|
with zipfile.ZipFile(path, "r") as zip_ref:
|
|
857
863
|
info = yaml.safe_load(zip_ref.read("info.yml"))
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
864
|
+
try:
|
|
865
|
+
portfolio = pd.read_csv(
|
|
866
|
+
zip_ref.open("portfolio.csv"), index_col=["timestamp"], parse_dates=["timestamp"]
|
|
867
|
+
)
|
|
868
|
+
except:
|
|
869
|
+
portfolio = pd.DataFrame()
|
|
870
|
+
try:
|
|
871
|
+
executions = pd.read_csv(
|
|
872
|
+
zip_ref.open("executions.csv"), index_col=["timestamp"], parse_dates=["timestamp"]
|
|
873
|
+
)
|
|
874
|
+
except:
|
|
875
|
+
executions = pd.DataFrame()
|
|
876
|
+
try:
|
|
877
|
+
signals = pd.read_csv(zip_ref.open("signals.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
|
|
878
|
+
except:
|
|
879
|
+
signals = pd.DataFrame()
|
|
861
880
|
|
|
862
881
|
# load result
|
|
863
882
|
_qbx_version = info.pop("qubx_version")
|
|
864
883
|
_decr = info.pop("description", None)
|
|
865
884
|
_perf = info.pop("performance", None)
|
|
866
885
|
info["instruments"] = info.pop("symbols")
|
|
886
|
+
# - fix for old versions
|
|
887
|
+
_exch = info.pop("exchange")
|
|
888
|
+
info["exchanges"] = _exch if isinstance(_exch, list) else [_exch]
|
|
867
889
|
tsr = TradingSessionResult(**info, portfolio_log=portfolio, executions_log=executions, signals_log=signals)
|
|
868
890
|
tsr.qubx_version = _qbx_version
|
|
869
891
|
tsr._metrics = _perf
|
|
Binary file
|
|
Binary file
|
qubx/data/hft.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import queue
|
|
2
|
-
from collections import defaultdict
|
|
2
|
+
from collections import defaultdict
|
|
3
3
|
from multiprocessing import Event, Process, Queue
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from threading import Thread
|
|
6
|
-
from typing import Any, Iterable, Optional, TypeAlias, TypeVar
|
|
6
|
+
from typing import Any, Iterable, Optional, TypeAlias, TypeVar
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pandas as pd
|
|
@@ -148,7 +148,7 @@ class HftChunkPrefetcher:
|
|
|
148
148
|
enable_trade="trade" in queues,
|
|
149
149
|
enable_orderbook="orderbook" in queues,
|
|
150
150
|
)
|
|
151
|
-
ctx = reader._get_or_create_context(data_id, start, stop)
|
|
151
|
+
ctx = reader._get_or_create_context(data_id, start.floor("d"), stop)
|
|
152
152
|
instrument = reader._data_id_to_instrument[data_id]
|
|
153
153
|
|
|
154
154
|
# Initialize buffers only for enabled data types
|
|
@@ -205,9 +205,11 @@ class HftChunkPrefetcher:
|
|
|
205
205
|
]
|
|
206
206
|
)
|
|
207
207
|
reader._create_buffer_if_needed("quotes", instrument, (quote_chunksize,), quote_dtype)
|
|
208
|
+
start_time_ns = start.value
|
|
209
|
+
stop_time_ns = stop.value
|
|
208
210
|
|
|
209
211
|
while not stop_event.is_set():
|
|
210
|
-
reader._next_batch(
|
|
212
|
+
stop_reached = reader._next_batch(
|
|
211
213
|
ctx=ctx,
|
|
212
214
|
instrument=instrument,
|
|
213
215
|
chunksize=chunk_args["chunksize"],
|
|
@@ -216,6 +218,8 @@ class HftChunkPrefetcher:
|
|
|
216
218
|
orderbook_period=orderbook_period,
|
|
217
219
|
tick_size_pct=chunk_args["tick_size_pct"],
|
|
218
220
|
depth=chunk_args["depth"],
|
|
221
|
+
start_time=start_time_ns,
|
|
222
|
+
stop_time=stop_time_ns,
|
|
219
223
|
)
|
|
220
224
|
|
|
221
225
|
# Get records for enabled data types
|
|
@@ -270,6 +274,9 @@ class HftChunkPrefetcher:
|
|
|
270
274
|
for data_type in queues:
|
|
271
275
|
reader._mark_processed(data_type, instrument)
|
|
272
276
|
|
|
277
|
+
if stop_reached:
|
|
278
|
+
break
|
|
279
|
+
|
|
273
280
|
except Exception as e:
|
|
274
281
|
error_queue.put(e)
|
|
275
282
|
for queue in queues.values():
|
|
@@ -450,7 +457,9 @@ class HftDataReader(DataReader):
|
|
|
450
457
|
):
|
|
451
458
|
raise ValueError(f"Data type {data_type} is not enabled")
|
|
452
459
|
|
|
453
|
-
|
|
460
|
+
# - handle start and stop
|
|
461
|
+
_start_raw, _stop = handle_start_stop(start, stop, lambda x: pd.Timestamp(x))
|
|
462
|
+
_start = _start_raw.floor("d") # we must to start from day's start
|
|
454
463
|
assert isinstance(_start, pd.Timestamp) and isinstance(_stop, pd.Timestamp)
|
|
455
464
|
|
|
456
465
|
# Check if we need to recreate the prefetcher
|
|
@@ -489,7 +498,7 @@ class HftDataReader(DataReader):
|
|
|
489
498
|
"orderbook_interval": self.orderbook_interval,
|
|
490
499
|
"trade_capacity": self.trade_capacity,
|
|
491
500
|
}
|
|
492
|
-
prefetcher.start(self.path, data_id,
|
|
501
|
+
prefetcher.start(self.path, data_id, _start_raw, _stop, chunk_args)
|
|
493
502
|
logger.debug(f"Started prefetcher for {data_id}")
|
|
494
503
|
self._prefetchers[data_id] = prefetcher
|
|
495
504
|
self._prefetcher_ranges[data_id] = (_start, _stop)
|
|
@@ -620,7 +629,9 @@ class HftDataReader(DataReader):
|
|
|
620
629
|
orderbook_period: int,
|
|
621
630
|
tick_size_pct: float,
|
|
622
631
|
depth: int,
|
|
623
|
-
|
|
632
|
+
start_time: int,
|
|
633
|
+
stop_time: int,
|
|
634
|
+
) -> bool:
|
|
624
635
|
match data_type:
|
|
625
636
|
case "quote":
|
|
626
637
|
if self._instrument_to_quote_index[instrument] > 0 or not self.enable_quote:
|
|
@@ -636,8 +647,11 @@ class HftDataReader(DataReader):
|
|
|
636
647
|
quote_index,
|
|
637
648
|
trade_index,
|
|
638
649
|
orderbook_index,
|
|
650
|
+
stop_reached,
|
|
639
651
|
) = _simulate_hft(
|
|
640
652
|
ctx=ctx,
|
|
653
|
+
start_time=start_time,
|
|
654
|
+
stop_time=stop_time,
|
|
641
655
|
ob_timestamp=self._instrument_to_name_to_buffer["ob_timestamp"][instrument],
|
|
642
656
|
bid_price_buffer=self._instrument_to_name_to_buffer["bid_price"][instrument],
|
|
643
657
|
ask_price_buffer=self._instrument_to_name_to_buffer["ask_price"][instrument],
|
|
@@ -660,6 +674,8 @@ class HftDataReader(DataReader):
|
|
|
660
674
|
self._instrument_to_trade_index[instrument] = trade_index if self.enable_trade else 0
|
|
661
675
|
self._instrument_to_orderbook_index[instrument] = orderbook_index if self.enable_orderbook else 0
|
|
662
676
|
|
|
677
|
+
return stop_reached
|
|
678
|
+
|
|
663
679
|
def _create_backtest_assets(self, files: list[str], instrument: Instrument) -> list[BacktestAsset]:
|
|
664
680
|
mid_price = _get_initial_mid_price(files)
|
|
665
681
|
roi_lb, roi_ub = mid_price / 4, mid_price * 4
|
|
@@ -760,6 +776,8 @@ def _simulate_hft(
|
|
|
760
776
|
trade_buffer: np.ndarray,
|
|
761
777
|
quote_buffer: np.ndarray,
|
|
762
778
|
batch_size: int,
|
|
779
|
+
start_time: int,
|
|
780
|
+
stop_time: int,
|
|
763
781
|
interval: int = 1_000_000_000,
|
|
764
782
|
orderbook_period: int = 1,
|
|
765
783
|
tick_size_pct: float = 0.0,
|
|
@@ -767,12 +785,17 @@ def _simulate_hft(
|
|
|
767
785
|
enable_quote: bool = True,
|
|
768
786
|
enable_trade: bool = True,
|
|
769
787
|
enable_orderbook: bool = True,
|
|
770
|
-
) -> tuple[int, int, int]:
|
|
788
|
+
) -> tuple[int, int, int, bool]:
|
|
771
789
|
orderbook_index = 0
|
|
772
790
|
quote_index = 0
|
|
773
791
|
trade_index = 0
|
|
792
|
+
stop_reached = False
|
|
774
793
|
|
|
775
794
|
while ctx.elapse(interval) == 0 and orderbook_index < batch_size:
|
|
795
|
+
# - skip if we are before the start time
|
|
796
|
+
if ctx.current_timestamp < start_time:
|
|
797
|
+
continue
|
|
798
|
+
|
|
776
799
|
depth = ctx.depth(0)
|
|
777
800
|
|
|
778
801
|
# record quote
|
|
@@ -824,4 +847,9 @@ def _simulate_hft(
|
|
|
824
847
|
ctx.clear_last_trades(0)
|
|
825
848
|
quote_index += 1
|
|
826
849
|
|
|
827
|
-
|
|
850
|
+
# - stop if we reached the stop time
|
|
851
|
+
if ctx.current_timestamp >= stop_time:
|
|
852
|
+
stop_reached = True
|
|
853
|
+
break
|
|
854
|
+
|
|
855
|
+
return quote_index, trade_index, orderbook_index, stop_reached
|
qubx/data/readers.py
CHANGED
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
import re
|
|
4
4
|
from functools import wraps
|
|
5
5
|
from os.path import exists, join
|
|
6
|
-
from typing import Any, Iterable, Iterator
|
|
6
|
+
from typing import Any, Iterable, Iterator
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pandas as pd
|
|
@@ -33,7 +33,7 @@ STOCK_DAILY_SESSION = (convert_timedelta_to_numpy("9:30:00.100"), convert_timede
|
|
|
33
33
|
CME_FUTURES_DAILY_SESSION = (convert_timedelta_to_numpy("8:30:00.100"), convert_timedelta_to_numpy("15:14:59.900"))
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def _recognize_t(t:
|
|
36
|
+
def _recognize_t(t: int | str, defaultvalue, timeunit) -> int:
|
|
37
37
|
if isinstance(t, (str, pd.Timestamp)):
|
|
38
38
|
try:
|
|
39
39
|
return np.datetime64(t, timeunit)
|
|
@@ -78,7 +78,7 @@ class DataTransformer:
|
|
|
78
78
|
def start_transform(
|
|
79
79
|
self,
|
|
80
80
|
name: str,
|
|
81
|
-
column_names:
|
|
81
|
+
column_names: list[str],
|
|
82
82
|
start: str | None = None,
|
|
83
83
|
stop: str | None = None,
|
|
84
84
|
):
|
|
@@ -94,7 +94,7 @@ class DataTransformer:
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
class DataReader:
|
|
97
|
-
def get_names(self, **kwargs) ->
|
|
97
|
+
def get_names(self, **kwargs) -> list[str]:
|
|
98
98
|
"""
|
|
99
99
|
TODO: not sure we really need this !
|
|
100
100
|
"""
|
|
@@ -108,10 +108,10 @@ class DataReader:
|
|
|
108
108
|
transform: DataTransformer = DataTransformer(),
|
|
109
109
|
chunksize=0,
|
|
110
110
|
**kwargs,
|
|
111
|
-
) -> Iterator |
|
|
111
|
+
) -> Iterator | list:
|
|
112
112
|
raise NotImplementedError("read() method is not implemented")
|
|
113
113
|
|
|
114
|
-
def get_aux_data_ids(self) ->
|
|
114
|
+
def get_aux_data_ids(self) -> set[str]:
|
|
115
115
|
"""
|
|
116
116
|
Returns list of all auxiliary data IDs available for this data reader
|
|
117
117
|
"""
|
|
@@ -290,7 +290,7 @@ class CsvStorageDataReader(DataReader):
|
|
|
290
290
|
_r.append(x.assign(symbol=symbol.upper(), timestamp=x.index)) # type: ignore
|
|
291
291
|
return srows(*_r).set_index(["timestamp", "symbol"]) if _r else pd.DataFrame()
|
|
292
292
|
|
|
293
|
-
def get_names(self, **kwargs) ->
|
|
293
|
+
def get_names(self, **kwargs) -> list[str]:
|
|
294
294
|
_n = []
|
|
295
295
|
for root, _, files in os.walk(self.path):
|
|
296
296
|
path = root.split(os.sep)
|
|
@@ -383,7 +383,7 @@ class InMemoryDataFrameReader(DataReader):
|
|
|
383
383
|
|
|
384
384
|
Returns:
|
|
385
385
|
--------
|
|
386
|
-
Iterable |
|
|
386
|
+
Iterable | list
|
|
387
387
|
The processed and transformed data, either as an iterable (if chunksize > 0) or as a list.
|
|
388
388
|
|
|
389
389
|
Raises:
|
|
@@ -437,7 +437,7 @@ class AsPandasFrame(DataTransformer):
|
|
|
437
437
|
def __init__(self, timestamp_units=None) -> None:
|
|
438
438
|
self.timestamp_units = timestamp_units
|
|
439
439
|
|
|
440
|
-
def start_transform(self, name: str, column_names:
|
|
440
|
+
def start_transform(self, name: str, column_names: list[str], **kwargs):
|
|
441
441
|
self._time_idx = _find_time_col_idx(column_names)
|
|
442
442
|
self._column_names = column_names
|
|
443
443
|
self._frame = pd.DataFrame()
|
|
@@ -480,7 +480,7 @@ class AsOhlcvSeries(DataTransformer):
|
|
|
480
480
|
self._data_type = None
|
|
481
481
|
self.timestamp_units = timestamp_units
|
|
482
482
|
|
|
483
|
-
def start_transform(self, name: str, column_names:
|
|
483
|
+
def start_transform(self, name: str, column_names: list[str], **kwargs):
|
|
484
484
|
self._time_idx = _find_time_col_idx(column_names)
|
|
485
485
|
self._volume_idx = None
|
|
486
486
|
self._b_volume_idx = None
|
|
@@ -539,7 +539,7 @@ class AsOhlcvSeries(DataTransformer):
|
|
|
539
539
|
if self.timeframe:
|
|
540
540
|
self._series = OHLCV(self._name, self.timeframe)
|
|
541
541
|
|
|
542
|
-
def _proc_ohlc(self, rows_data:
|
|
542
|
+
def _proc_ohlc(self, rows_data: list[list]):
|
|
543
543
|
for d in rows_data:
|
|
544
544
|
self._series.update_by_bar(
|
|
545
545
|
_time(d[self._time_idx], self.timestamp_units),
|
|
@@ -551,21 +551,21 @@ class AsOhlcvSeries(DataTransformer):
|
|
|
551
551
|
d[self._b_volume_idx] if self._b_volume_idx else 0,
|
|
552
552
|
)
|
|
553
553
|
|
|
554
|
-
def _proc_quotes(self, rows_data:
|
|
554
|
+
def _proc_quotes(self, rows_data: list[list]):
|
|
555
555
|
for d in rows_data:
|
|
556
556
|
self._series.update(
|
|
557
557
|
_time(d[self._time_idx], self.timestamp_units),
|
|
558
558
|
(d[self._ask_idx] + d[self._bid_idx]) / 2,
|
|
559
559
|
)
|
|
560
560
|
|
|
561
|
-
def _proc_trades(self, rows_data:
|
|
561
|
+
def _proc_trades(self, rows_data: list[list]):
|
|
562
562
|
for d in rows_data:
|
|
563
563
|
a = d[self._taker_idx] if self._taker_idx else 0
|
|
564
564
|
s = d[self._size_idx]
|
|
565
565
|
b = s if a else 0
|
|
566
566
|
self._series.update(_time(d[self._time_idx], self.timestamp_units), d[self._price_idx], s, b)
|
|
567
567
|
|
|
568
|
-
def process_data(self, rows_data:
|
|
568
|
+
def process_data(self, rows_data: list[list]) -> Any:
|
|
569
569
|
if self._series is None:
|
|
570
570
|
ts = [t[self._time_idx] for t in rows_data[:100]]
|
|
571
571
|
self.timeframe = pd.Timedelta(infer_series_frequency(ts)).asm8.item()
|
|
@@ -610,7 +610,7 @@ class AsQuotes(DataTransformer):
|
|
|
610
610
|
Data must have appropriate structure: bid, ask, bidsize, asksize and time
|
|
611
611
|
"""
|
|
612
612
|
|
|
613
|
-
def start_transform(self, name: str, column_names:
|
|
613
|
+
def start_transform(self, name: str, column_names: list[str], **kwargs):
|
|
614
614
|
self.buffer = list()
|
|
615
615
|
self._time_idx = _find_time_col_idx(column_names)
|
|
616
616
|
self._bid_idx = _find_column_index_in_list(column_names, "bid")
|
|
@@ -639,7 +639,7 @@ class AsOrderBook(DataTransformer):
|
|
|
639
639
|
super().__init__()
|
|
640
640
|
self.timestamp_units = timestamp_units
|
|
641
641
|
|
|
642
|
-
def start_transform(self, name: str, column_names:
|
|
642
|
+
def start_transform(self, name: str, column_names: list[str], **kwargs):
|
|
643
643
|
self.buffer = list()
|
|
644
644
|
self._time_idx = _find_time_col_idx(column_names)
|
|
645
645
|
self._top_bid_idx = _find_column_index_in_list(column_names, "top_bid")
|
|
@@ -668,7 +668,7 @@ class AsTrades(DataTransformer):
|
|
|
668
668
|
Market maker column specifies if buyer is a maker or taker.
|
|
669
669
|
"""
|
|
670
670
|
|
|
671
|
-
def start_transform(self, name: str, column_names:
|
|
671
|
+
def start_transform(self, name: str, column_names: list[str], **kwargs):
|
|
672
672
|
self.buffer: list[Trade | TradeArray] = list()
|
|
673
673
|
self._time_idx = _find_time_col_idx(column_names)
|
|
674
674
|
self._price_idx = _find_column_index_in_list(column_names, "price")
|
|
@@ -727,7 +727,7 @@ class AsTimestampedRecords(DataTransformer):
|
|
|
727
727
|
def __init__(self, timestamp_units: str | None = None) -> None:
|
|
728
728
|
self.timestamp_units = timestamp_units
|
|
729
729
|
|
|
730
|
-
def start_transform(self, name: str, column_names:
|
|
730
|
+
def start_transform(self, name: str, column_names: list[str], **kwargs):
|
|
731
731
|
self.buffer = list()
|
|
732
732
|
self._time_idx = _find_time_col_idx(column_names)
|
|
733
733
|
self._column_names = column_names
|
|
@@ -792,7 +792,7 @@ class RestoredEmulatorHelper(DataTransformer):
|
|
|
792
792
|
self._t_mid2 = dt // 2 + H1
|
|
793
793
|
self._t_end = self._d_session_end - self._open_close_time_shift_secs * S1
|
|
794
794
|
|
|
795
|
-
def start_transform(self, name: str, column_names:
|
|
795
|
+
def start_transform(self, name: str, column_names: list[str], **kwargs):
|
|
796
796
|
self.buffer = []
|
|
797
797
|
# - it will fail if receive data doesn't look as ohlcv
|
|
798
798
|
self._time_idx = _find_time_col_idx(column_names)
|
|
@@ -989,7 +989,7 @@ class RestoredBarsFromOHLC(RestoredEmulatorHelper):
|
|
|
989
989
|
):
|
|
990
990
|
super().__init__(daily_session_start_end, timestamp_units, open_close_time_shift_secs)
|
|
991
991
|
|
|
992
|
-
def process_data(self, rows_data:
|
|
992
|
+
def process_data(self, rows_data: list[list]) -> Any:
|
|
993
993
|
if rows_data is None:
|
|
994
994
|
return
|
|
995
995
|
|
|
@@ -1033,7 +1033,7 @@ class AsDict(DataTransformer):
|
|
|
1033
1033
|
Tries to keep incoming data as list of dictionaries with preprocessed time
|
|
1034
1034
|
"""
|
|
1035
1035
|
|
|
1036
|
-
def start_transform(self, name: str, column_names:
|
|
1036
|
+
def start_transform(self, name: str, column_names: list[str], **kwargs):
|
|
1037
1037
|
self.buffer = list()
|
|
1038
1038
|
self._time_idx = _find_time_col_idx(column_names)
|
|
1039
1039
|
self._column_names = column_names
|
|
@@ -1360,7 +1360,7 @@ class QuestDBConnector(DataReader):
|
|
|
1360
1360
|
return pd.DataFrame()
|
|
1361
1361
|
return df.set_index(["timestamp", "symbol", "metric"]).value.unstack("metric")
|
|
1362
1362
|
|
|
1363
|
-
def get_names(self) ->
|
|
1363
|
+
def get_names(self) -> list[str]:
|
|
1364
1364
|
return self._get_names(self._builder)
|
|
1365
1365
|
|
|
1366
1366
|
@_retry
|
qubx/features/core.py
CHANGED
|
@@ -35,13 +35,14 @@ class FeatureManager:
|
|
|
35
35
|
"""
|
|
36
36
|
Subscribes the given feature provider to the manager.
|
|
37
37
|
"""
|
|
38
|
-
# - add the required subscriptions to the manager and update routing table
|
|
39
|
-
self.feature_providers
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
# - add the required subscriptions to the manager and update routing table if not already added
|
|
39
|
+
if feature_provider not in self.feature_providers:
|
|
40
|
+
self.feature_providers.append(feature_provider)
|
|
41
|
+
for input_name in feature_provider.inputs():
|
|
42
|
+
self.subscription_to_providers[input_name].append(feature_provider)
|
|
43
|
+
|
|
44
|
+
# - notify the feature provider that it has been subscribed
|
|
45
|
+
feature_provider.on_subscribe(self)
|
|
45
46
|
return self
|
|
46
47
|
|
|
47
48
|
def __str__(self) -> str:
|
|
Binary file
|
|
@@ -68,7 +68,7 @@ def __interceptor_on_universe_change(func):
|
|
|
68
68
|
|
|
69
69
|
# print new portfolio
|
|
70
70
|
print(" - New Universe - ")
|
|
71
|
-
portfolio()
|
|
71
|
+
portfolio(True)
|
|
72
72
|
|
|
73
73
|
return result
|
|
74
74
|
return _intercepted
|
|
@@ -113,6 +113,9 @@ class ActiveInstrument:
|
|
|
113
113
|
def trade(self, qty: float, price=None, tif='gtc', **options):
|
|
114
114
|
return ctx.trade(self._instrument, qty, price, tif, **options)
|
|
115
115
|
|
|
116
|
+
def trade_a(self, qty: float, price=None, tif='gtc', **options):
|
|
117
|
+
return ctx.trade_async(self._instrument, qty, price, tif, **options)
|
|
118
|
+
|
|
116
119
|
def signal(self, s: float, price: float | None = None,
|
|
117
120
|
stop: float | None = None,
|
|
118
121
|
take: float | None = None,
|
|
@@ -174,6 +177,10 @@ def orders(instrument: Instrument | ActiveInstrument | None=None):
|
|
|
174
177
|
def trade(instrument: Instrument | ActiveInstrument, qty: float, price=None, tif='gtc'):
|
|
175
178
|
return ctx.trade(instrument if isinstance(instrument, Instrument) else instrument._instrument, qty, price, tif)
|
|
176
179
|
|
|
180
|
+
|
|
181
|
+
def trade_a(instrument: Instrument | ActiveInstrument, qty: float, price=None, tif='gtc'):
|
|
182
|
+
return ctx.trade_async(instrument if isinstance(instrument, Instrument) else instrument._instrument, qty, price, tif)
|
|
183
|
+
|
|
177
184
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
178
185
|
def portfolio(all=True):
|
|
179
186
|
from tabulate import tabulate
|
qubx/utils/runner/runner.py
CHANGED
|
@@ -536,7 +536,7 @@ def _create_broker(
|
|
|
536
536
|
enable_mm=_enable_mm,
|
|
537
537
|
)
|
|
538
538
|
return get_ccxt_broker(
|
|
539
|
-
exchange_name, exchange, channel, time_provider, account, data_provider, **
|
|
539
|
+
exchange_name, exchange, channel, time_provider, account, data_provider, **params
|
|
540
540
|
)
|
|
541
541
|
case "paper":
|
|
542
542
|
assert isinstance(account, SimulatedAccountProcessor)
|
|
@@ -19,51 +19,51 @@ qubx/cli/misc.py,sha256=tP28QxLEzuP8R2xnt8g3JTs9Z7aYy4iVWY4g3VzKTsQ,14777
|
|
|
19
19
|
qubx/cli/release.py,sha256=JYdNt_ZM9jarmYiRDtKqbRqqllzm2Qwi6VggokB2j8A,28167
|
|
20
20
|
qubx/connectors/ccxt/__init__.py,sha256=HEQ7lM9HS8sED_zfsAHrhFT7F9E7NFGAecwZwNr-TDE,65
|
|
21
21
|
qubx/connectors/ccxt/account.py,sha256=HILqsSPfor58NrlP0qYwO5lkNZzUBG-SR5Hy1OSa7_M,24308
|
|
22
|
-
qubx/connectors/ccxt/broker.py,sha256=
|
|
22
|
+
qubx/connectors/ccxt/broker.py,sha256=Hg2tC3qKPYAURGro9ONzzR7QaiI41oytyKqQlpcM5Zw,16175
|
|
23
23
|
qubx/connectors/ccxt/data.py,sha256=COVUh37ZdCUjiDB0a38Cj9SNSV8P95mqG2B3Gc_fQ2U,30172
|
|
24
24
|
qubx/connectors/ccxt/exceptions.py,sha256=OfZc7iMdEG8uLorcZta2NuEuJrSIqi0FG7IICmwF54M,262
|
|
25
|
-
qubx/connectors/ccxt/exchanges/__init__.py,sha256=
|
|
25
|
+
qubx/connectors/ccxt/exchanges/__init__.py,sha256=dEBkyeiGEQgfyuGVhhx4ZTRIlU9e_H1m6K2ROXRpIi8,1884
|
|
26
26
|
qubx/connectors/ccxt/exchanges/binance/broker.py,sha256=BB2V82zaOm1EjP3GrsOqQQMeGpml6-w23iv7goKrjyU,2111
|
|
27
|
-
qubx/connectors/ccxt/exchanges/binance/exchange.py,sha256=
|
|
28
|
-
qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py,sha256=
|
|
27
|
+
qubx/connectors/ccxt/exchanges/binance/exchange.py,sha256=qdyqiNTIVolpIwQ2g4Vrc5ibYUdS9moN7qt_UYNnezs,24794
|
|
28
|
+
qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py,sha256=Q6inz-43fDrl--UsnTaDMGHWWkUlcmzbuMLMkcQDVyk,10682
|
|
29
29
|
qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py,sha256=zrnA6GJiNddoM5JF-SlFFO4FpITDO5rGaU9ipshUvAY,1603
|
|
30
30
|
qubx/connectors/ccxt/exchanges/kraken/kraken.py,sha256=RFrnvr1L1NZYoKYWR5_L8vVkpMXtY7UDkWRnHeoasDU,351
|
|
31
31
|
qubx/connectors/ccxt/factory.py,sha256=T0cMSH5m6-T2LXrbZHM9uCSOYOfZf-bh1fAOXAoFhF4,3226
|
|
32
32
|
qubx/connectors/ccxt/reader.py,sha256=qaZIaOZkRf3Rz31ZrEqqAv4kATk5zDlSq-LK1jziBs8,8314
|
|
33
|
-
qubx/connectors/ccxt/utils.py,sha256=
|
|
34
|
-
qubx/connectors/tardis/data.py,sha256=
|
|
33
|
+
qubx/connectors/ccxt/utils.py,sha256=7BRmVT3hbGFbhRpJXTJ3lYX7R6VXi5aPxNYj1EjxObY,11775
|
|
34
|
+
qubx/connectors/tardis/data.py,sha256=JYkU9PTxIL4m0L2bLROzKtiiJSRbk48GDR4wnrL9ENM,30945
|
|
35
35
|
qubx/connectors/tardis/utils.py,sha256=epThu9DwqbDb7BgScH6fHa_FVpKUaItOqp3JwtKGc5g,9092
|
|
36
36
|
qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
37
|
qubx/core/account.py,sha256=4_XskMLR9Uh-rpJBDYMrceYiGMvAZw56k1ve-unIW8w,19417
|
|
38
38
|
qubx/core/basics.py,sha256=PaprFa6sscrYPZfxImWOPLfVoVlu3PtcXTWbz0ZMtEk,28864
|
|
39
|
-
qubx/core/context.py,sha256=
|
|
39
|
+
qubx/core/context.py,sha256=ZetAOnQ9pfUiMAFln02zJr4FePTjKaY5OSIVwaMPhnE,22822
|
|
40
40
|
qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
|
|
41
|
-
qubx/core/errors.py,sha256=
|
|
41
|
+
qubx/core/errors.py,sha256=LENtlgmVzxxUFNCsuy4PwyHYhkZkxuZQ2BPif8jaGmw,1411
|
|
42
42
|
qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
|
|
43
43
|
qubx/core/helpers.py,sha256=qzRsttt4sMYMarDWMzWvc3b2W-Qp9qAQwFiQBljAsA0,19722
|
|
44
44
|
qubx/core/initializer.py,sha256=PUiD_cIjvGpuPjYyRpUjpwm3xNQ2Kipa8bAhbtxCQRo,3935
|
|
45
|
-
qubx/core/interfaces.py,sha256=
|
|
45
|
+
qubx/core/interfaces.py,sha256=CzIl8tB6ImQkDcZEmhpstwHPOCY8NhZxXmBHLQUAieI,58253
|
|
46
46
|
qubx/core/loggers.py,sha256=eYhJANHYwz1heeFMa5V7jYCL196wkTSvj6c-8lkPj1Y,19567
|
|
47
47
|
qubx/core/lookups.py,sha256=n5ZjjEhhRvmidCB-Cubr1b0Opm6lf_QVZNEWa_BOQG0,19376
|
|
48
|
-
qubx/core/metrics.py,sha256=
|
|
48
|
+
qubx/core/metrics.py,sha256=Gq3Ultwn5meICfyauBUJrBS4nffSxFVH3OF6N1Y0xgo,58664
|
|
49
49
|
qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
|
|
50
50
|
qubx/core/mixins/market.py,sha256=lBappEimPhIuI0vmUvwVlIztkYjlEjJBpP-AdpfudII,3948
|
|
51
51
|
qubx/core/mixins/processing.py,sha256=dqehukrfqcLy5BeILKnkpHCvva4SbLKj1ZbQdnByu1k,24552
|
|
52
52
|
qubx/core/mixins/subscription.py,sha256=V_g9wCPQ8S5SHkU-qOZ84cV5nReAUrV7DoSNAGG0LPY,10372
|
|
53
53
|
qubx/core/mixins/trading.py,sha256=idfRPaqrvkfMxzu9mXr9i_xfqLee-ZAOrERxkxv6Ruo,7256
|
|
54
54
|
qubx/core/mixins/universe.py,sha256=L3s2Jw46_J1iDh4622Gk_LvCjol4W7mflBwEHrLfZEw,9899
|
|
55
|
-
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
55
|
+
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=xDkkXCrX_5IEVqcTRyEEW_6yHElmh3wbdMOnWGrPqb0,978280
|
|
56
56
|
qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
|
|
57
57
|
qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
|
|
58
58
|
qubx/core/series.pyx,sha256=7cM3zZThW59waHiYcZmMxvYj-HYD7Ej_l7nKA4emPjE,46477
|
|
59
|
-
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
59
|
+
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=dbfYcmWGKzRq-kIx0d7CPbIMobWlyFuRzl_oHxEtYgM,86568
|
|
60
60
|
qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
|
|
61
61
|
qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
|
|
62
62
|
qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
|
|
63
63
|
qubx/data/composite.py,sha256=bcFJzIzR2-IfVW8Ot3cUibKS8smnmRbHipd8ztIuScs,18015
|
|
64
64
|
qubx/data/helpers.py,sha256=VcXBl1kfWzAOqrjadKrP9WemGjJIB0q3xascbesErh4,16268
|
|
65
|
-
qubx/data/hft.py,sha256=
|
|
66
|
-
qubx/data/readers.py,sha256=
|
|
65
|
+
qubx/data/hft.py,sha256=be7AwzTOjqqCENn0ClrZoHDyKv3SFG66IyTp8QadHlM,33687
|
|
66
|
+
qubx/data/readers.py,sha256=H68n38VLMjjk8R5FW7URGLcJCh0MREKFGdMGgWCWzhU,62503
|
|
67
67
|
qubx/data/registry.py,sha256=45mjy5maBSO6cf-0zfIRRDs8b0VDW7wHSPn43aRjv-o,3883
|
|
68
68
|
qubx/data/tardis.py,sha256=VMw13tIIlrGZwermKvdFRSNtLUiJDGOKW4l6WuAMQSA,33747
|
|
69
69
|
qubx/emitters/__init__.py,sha256=tpJ9OoW-gycTBXGJ0647tT8-dVBmq23T2wMX_kmk3nM,565
|
|
@@ -81,7 +81,7 @@ qubx/exporters/formatters/slack.py,sha256=MPjbEFh7PQufPdkg_Fwiu2tVw5zYJa977tCemo
|
|
|
81
81
|
qubx/exporters/redis_streams.py,sha256=8Cd39kAXUYSOS6-dQMSm1PpeQ4urOGVq0oe3dAXwUEI,8924
|
|
82
82
|
qubx/exporters/slack.py,sha256=wnVZRwWOKq9lMQyW0MWh_6gkW1id1TUanfOKy-_clwI,7723
|
|
83
83
|
qubx/features/__init__.py,sha256=ZFCX7K5bDAH7yTsG-mf8zibW8UW8GCneEagL1_p8kDQ,385
|
|
84
|
-
qubx/features/core.py,sha256=
|
|
84
|
+
qubx/features/core.py,sha256=eXa1qIu-LXo40td1X4EUBFQ5jJcSTuaQIi-562bPCoM,10587
|
|
85
85
|
qubx/features/orderbook.py,sha256=idmBEYDMMNBkQopHKQs_oEmQMPP9gNJpgHVmUBlXeek,1288
|
|
86
86
|
qubx/features/price.py,sha256=fyjrHwHR0ftjGNXlpEx8Hjyfi3d1lBNFjsbe6qLFMqo,699
|
|
87
87
|
qubx/features/trades.py,sha256=d0dhmbemOOOJcXSet6n9sBI4-LVl9-oFh6Of7cRoY-4,3582
|
|
@@ -117,7 +117,7 @@ qubx/restorers/signal.py,sha256=DBLqA7vDhoMTAzUC4N9UerrO0GbjeHdTeMoCz7U7iI8,6621
|
|
|
117
117
|
qubx/restorers/state.py,sha256=duXyEHQhS1zdNdo3VKscMhieZ5sYNlfE4S_pPXQ1Tuw,4109
|
|
118
118
|
qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
|
|
119
119
|
qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
120
|
-
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
120
|
+
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=aQuDwEXqPC-9q3Ue_oHiyIZ-ecql-PYm_FG1yFpWfbg,654440
|
|
121
121
|
qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
|
|
122
122
|
qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
|
|
123
123
|
qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
|
|
@@ -147,15 +147,15 @@ qubx/utils/plotting/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
|
147
147
|
qubx/utils/plotting/renderers/plotly.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
148
148
|
qubx/utils/questdb.py,sha256=TdjmlGPoZXdjidZ_evcBIkFtoL4nGQXPR4IQSUc6IvA,2509
|
|
149
149
|
qubx/utils/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
150
|
-
qubx/utils/runner/_jupyter_runner.pyt,sha256=
|
|
150
|
+
qubx/utils/runner/_jupyter_runner.pyt,sha256=fDj4AUs25jsdGmY9DDeSFufH1JkVhLFwy0BOmVO7nIU,9609
|
|
151
151
|
qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
|
|
152
152
|
qubx/utils/runner/configs.py,sha256=4lonQgksh4wDygsN67lIidVRIUksskWuhL25A2IZwho,3694
|
|
153
153
|
qubx/utils/runner/factory.py,sha256=vQ2dBTbrQE9YH__-TvuFzGF-E1li-vt_qQum9GHa11g,11666
|
|
154
|
-
qubx/utils/runner/runner.py,sha256=
|
|
154
|
+
qubx/utils/runner/runner.py,sha256=TF-nD-EH8k1U2Wkjk70eEVketU6HtWpTFmFH7vjE9Z8,29298
|
|
155
155
|
qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
|
|
156
156
|
qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
|
|
157
|
-
qubx-0.6.
|
|
158
|
-
qubx-0.6.
|
|
159
|
-
qubx-0.6.
|
|
160
|
-
qubx-0.6.
|
|
161
|
-
qubx-0.6.
|
|
157
|
+
qubx-0.6.37.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
|
|
158
|
+
qubx-0.6.37.dist-info/METADATA,sha256=wxX0lvx6Mb2XSM4_ZHwHs9X_uq5qI3SG0t1IOyW-8TM,4492
|
|
159
|
+
qubx-0.6.37.dist-info/WHEEL,sha256=XjdW4AGUgFDhpG9b3b2KPhtR_JLZvHyfemLgJJwcqOI,110
|
|
160
|
+
qubx-0.6.37.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
|
|
161
|
+
qubx-0.6.37.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|