Qubx 0.6.19__tar.gz → 0.6.20__tar.gz
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-0.6.19 → qubx-0.6.20}/PKG-INFO +1 -1
- {qubx-0.6.19 → qubx-0.6.20}/pyproject.toml +1 -1
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/account.py +4 -2
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/data.py +23 -6
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/context.py +36 -15
- qubx-0.6.20/src/qubx/core/deque.py +182 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/interfaces.py +213 -4
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/mixins/processing.py +82 -75
- qubx-0.6.20/src/qubx/health/__init__.py +3 -0
- qubx-0.6.20/src/qubx/health/base.py +668 -0
- qubx-0.6.20/src/qubx/utils/collections.py +220 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/runner/configs.py +8 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/runner/runner.py +18 -2
- {qubx-0.6.19 → qubx-0.6.20}/LICENSE +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/README.md +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/build.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/account.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/basics.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/helpers.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/metrics.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/data/readers.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/features/core.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/features/price.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/resources/instruments/symbols-binance.json +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.19 → qubx-0.6.20}/src/qubx/utils/version.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "Qubx"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.20"
|
|
8
8
|
description = "Qubx - Quantitative Trading Framework"
|
|
9
9
|
authors = [ "Dmitry Marienko <dmitry.marienko@xlydian.com>", "Yuriy Arabskyy <yuriy.arabskyy@xlydian.com>",]
|
|
10
10
|
readme = "README.md"
|
|
@@ -81,6 +81,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
81
81
|
open_order_backoff: str = "1Min",
|
|
82
82
|
max_position_restore_days: int = 30,
|
|
83
83
|
max_retries: int = 10,
|
|
84
|
+
read_only: bool = False,
|
|
84
85
|
):
|
|
85
86
|
super().__init__(
|
|
86
87
|
account_id=account_id,
|
|
@@ -106,6 +107,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
106
107
|
self._required_instruments = set()
|
|
107
108
|
self._latest_instruments = set()
|
|
108
109
|
self._subscription_manager = None
|
|
110
|
+
self._read_only = read_only
|
|
109
111
|
|
|
110
112
|
def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
|
|
111
113
|
self._subscription_manager = manager
|
|
@@ -439,9 +441,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
439
441
|
for order in orders:
|
|
440
442
|
logger.debug(f" :: [SYNC] {order.side} {order.quantity} @ {order.price} ({order.status})")
|
|
441
443
|
else:
|
|
442
|
-
# TODO: think if this should actually be here
|
|
443
444
|
# - we need to cancel the unexpected orders
|
|
444
|
-
|
|
445
|
+
if not self._read_only:
|
|
446
|
+
await self._cancel_unexpected_orders(_open_orders)
|
|
445
447
|
|
|
446
448
|
async def _cancel_unexpected_orders(self, open_orders: dict[str, Order]) -> None:
|
|
447
449
|
_expected_orders = set(self._active_orders.keys())
|
|
@@ -20,8 +20,9 @@ from ccxt.pro import Exchange
|
|
|
20
20
|
from qubx import logger
|
|
21
21
|
from qubx.core.basics import CtrlChannel, DataType, Instrument, ITimeProvider, dt_64
|
|
22
22
|
from qubx.core.helpers import BasicScheduler
|
|
23
|
-
from qubx.core.interfaces import IDataProvider
|
|
23
|
+
from qubx.core.interfaces import IDataProvider, IHealthMonitor
|
|
24
24
|
from qubx.core.series import Bar, Quote
|
|
25
|
+
from qubx.health import DummyHealthMonitor
|
|
25
26
|
from qubx.utils.misc import AsyncThreadLoop
|
|
26
27
|
|
|
27
28
|
from .exceptions import CcxtLiquidationParsingError, CcxtSymbolNotRecognized
|
|
@@ -64,12 +65,14 @@ class CcxtDataProvider(IDataProvider):
|
|
|
64
65
|
channel: CtrlChannel,
|
|
65
66
|
max_ws_retries: int = 10,
|
|
66
67
|
warmup_timeout: int = 120,
|
|
68
|
+
health_monitor: IHealthMonitor | None = None,
|
|
67
69
|
):
|
|
68
70
|
self._exchange_id = str(exchange.name)
|
|
69
71
|
self.time_provider = time_provider
|
|
70
72
|
self.channel = channel
|
|
71
73
|
self.max_ws_retries = max_ws_retries
|
|
72
74
|
self._warmup_timeout = warmup_timeout
|
|
75
|
+
self._health_monitor = health_monitor or DummyHealthMonitor()
|
|
73
76
|
|
|
74
77
|
# - create new even loop
|
|
75
78
|
self._exchange = exchange
|
|
@@ -416,11 +419,13 @@ class CcxtDataProvider(IDataProvider):
|
|
|
416
419
|
instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
|
|
417
420
|
for _, ohlcvs in _data.items():
|
|
418
421
|
for oh in ohlcvs:
|
|
422
|
+
timestamp_ns = oh[0] * 1_000_000
|
|
423
|
+
self._health_monitor.record_data_arrival(sub_type, dt_64(timestamp_ns, "ns"))
|
|
419
424
|
channel.send(
|
|
420
425
|
(
|
|
421
426
|
instrument,
|
|
422
427
|
sub_type,
|
|
423
|
-
Bar(
|
|
428
|
+
Bar(timestamp_ns, oh[1], oh[2], oh[3], oh[4], oh[6], oh[7]),
|
|
424
429
|
False, # not historical bar
|
|
425
430
|
)
|
|
426
431
|
)
|
|
@@ -463,7 +468,9 @@ class CcxtDataProvider(IDataProvider):
|
|
|
463
468
|
exch_symbol = trades[0]["symbol"]
|
|
464
469
|
instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
|
|
465
470
|
for trade in trades:
|
|
466
|
-
|
|
471
|
+
converted_trade = ccxt_convert_trade(trade)
|
|
472
|
+
self._health_monitor.record_data_arrival(sub_type, dt_64(converted_trade.time, "ns"))
|
|
473
|
+
channel.send((instrument, sub_type, converted_trade, False))
|
|
467
474
|
|
|
468
475
|
async def un_watch_trades(instruments: list[Instrument]):
|
|
469
476
|
symbols = [_instr_to_ccxt_symbol[i] for i in instruments]
|
|
@@ -497,6 +504,7 @@ class CcxtDataProvider(IDataProvider):
|
|
|
497
504
|
ob = ccxt_convert_orderbook(ccxt_ob, instrument, levels=depth, tick_size_pct=tick_size_pct)
|
|
498
505
|
if ob is None:
|
|
499
506
|
return
|
|
507
|
+
self._health_monitor.record_data_arrival(sub_type, dt_64(ob.time, "ns"))
|
|
500
508
|
quote = ob.to_quote()
|
|
501
509
|
self._last_quotes[instrument] = quote
|
|
502
510
|
channel.send((instrument, sub_type, ob, False))
|
|
@@ -529,6 +537,7 @@ class CcxtDataProvider(IDataProvider):
|
|
|
529
537
|
for exch_symbol, ccxt_ticker in ccxt_tickers.items(): # type: ignore
|
|
530
538
|
instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
|
|
531
539
|
quote = ccxt_convert_ticker(ccxt_ticker)
|
|
540
|
+
self._health_monitor.record_data_arrival(sub_type, dt_64(quote.time, "ns"))
|
|
532
541
|
self._last_quotes[instrument] = quote
|
|
533
542
|
channel.send((instrument, sub_type, quote, False))
|
|
534
543
|
|
|
@@ -562,8 +571,11 @@ class CcxtDataProvider(IDataProvider):
|
|
|
562
571
|
liquidations = await self._exchange.watch_liquidations_for_symbols(symbols)
|
|
563
572
|
for liquidation in liquidations:
|
|
564
573
|
try:
|
|
565
|
-
|
|
566
|
-
|
|
574
|
+
exch_symbol = liquidation["symbol"]
|
|
575
|
+
instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
|
|
576
|
+
liquidation_event = ccxt_convert_liquidation(liquidation)
|
|
577
|
+
self._health_monitor.record_data_arrival(sub_type, dt_64(liquidation_event.time, "ns"))
|
|
578
|
+
channel.send((instrument, sub_type, liquidation_event, False))
|
|
567
579
|
except CcxtLiquidationParsingError:
|
|
568
580
|
logger.debug(f"Could not parse liquidation {liquidation}")
|
|
569
581
|
continue
|
|
@@ -593,12 +605,17 @@ class CcxtDataProvider(IDataProvider):
|
|
|
593
605
|
async def watch_funding_rates():
|
|
594
606
|
funding_rates = await self._exchange.watch_funding_rates() # type: ignore
|
|
595
607
|
instrument_to_funding_rate = {}
|
|
608
|
+
current_time = self.time_provider.time()
|
|
609
|
+
|
|
596
610
|
for symbol, info in funding_rates.items():
|
|
597
611
|
try:
|
|
598
612
|
instrument = ccxt_find_instrument(symbol, self._exchange)
|
|
599
|
-
|
|
613
|
+
funding_rate = ccxt_convert_funding_rate(info)
|
|
614
|
+
instrument_to_funding_rate[instrument] = funding_rate
|
|
615
|
+
self._health_monitor.record_data_arrival(sub_type, dt_64(current_time, "s"))
|
|
600
616
|
except CcxtSymbolNotRecognized:
|
|
601
617
|
continue
|
|
618
|
+
|
|
602
619
|
channel.send((None, sub_type, instrument_to_funding_rate, False))
|
|
603
620
|
|
|
604
621
|
async def un_watch_funding_rates():
|
|
@@ -4,7 +4,6 @@ from typing import Any, Callable
|
|
|
4
4
|
|
|
5
5
|
from qubx import logger
|
|
6
6
|
from qubx.core.basics import (
|
|
7
|
-
SW,
|
|
8
7
|
AssetBalance,
|
|
9
8
|
CtrlChannel,
|
|
10
9
|
DataType,
|
|
@@ -14,6 +13,7 @@ from qubx.core.basics import (
|
|
|
14
13
|
Order,
|
|
15
14
|
OrderRequest,
|
|
16
15
|
Position,
|
|
16
|
+
Timestamped,
|
|
17
17
|
dt_64,
|
|
18
18
|
)
|
|
19
19
|
from qubx.core.exceptions import StrategyExceededMaxNumberOfRuntimeFailuresError
|
|
@@ -27,6 +27,7 @@ from qubx.core.interfaces import (
|
|
|
27
27
|
IAccountProcessor,
|
|
28
28
|
IBroker,
|
|
29
29
|
IDataProvider,
|
|
30
|
+
IHealthMonitor,
|
|
30
31
|
IMarketManager,
|
|
31
32
|
IMetricEmitter,
|
|
32
33
|
IPositionGathering,
|
|
@@ -45,6 +46,7 @@ from qubx.core.interfaces import (
|
|
|
45
46
|
from qubx.core.loggers import StrategyLogging
|
|
46
47
|
from qubx.data.readers import DataReader
|
|
47
48
|
from qubx.gathering.simplest import SimplePositionGatherer
|
|
49
|
+
from qubx.health import DummyHealthMonitor
|
|
48
50
|
from qubx.trackers.sizers import FixedSizer
|
|
49
51
|
|
|
50
52
|
from .mixins import (
|
|
@@ -102,6 +104,7 @@ class StrategyContext(IStrategyContext):
|
|
|
102
104
|
initializer: BasicStrategyInitializer | None = None,
|
|
103
105
|
strategy_name: str | None = None,
|
|
104
106
|
strategy_state: StrategyState | None = None,
|
|
107
|
+
health_monitor: IHealthMonitor | None = None,
|
|
105
108
|
) -> None:
|
|
106
109
|
self.account = account
|
|
107
110
|
self.strategy = self.__instantiate_strategy(strategy, config)
|
|
@@ -127,6 +130,9 @@ class StrategyContext(IStrategyContext):
|
|
|
127
130
|
self._strategy_state = strategy_state if strategy_state is not None else StrategyState()
|
|
128
131
|
self._strategy_name = strategy_name if strategy_name is not None else strategy.__class__.__name__
|
|
129
132
|
|
|
133
|
+
self._health_monitor = health_monitor or DummyHealthMonitor()
|
|
134
|
+
self.health = self._health_monitor
|
|
135
|
+
|
|
130
136
|
__position_tracker = self.strategy.tracker(self)
|
|
131
137
|
if __position_tracker is None:
|
|
132
138
|
__position_tracker = StrategyContext.DEFAULT_POSITION_TRACKER()
|
|
@@ -180,6 +186,7 @@ class StrategyContext(IStrategyContext):
|
|
|
180
186
|
scheduler=self._scheduler,
|
|
181
187
|
is_simulation=self._data_provider.is_simulation,
|
|
182
188
|
exporter=self._exporter,
|
|
189
|
+
health_monitor=self._health_monitor,
|
|
183
190
|
)
|
|
184
191
|
self.__post_init__()
|
|
185
192
|
|
|
@@ -245,6 +252,9 @@ class StrategyContext(IStrategyContext):
|
|
|
245
252
|
# - start account processing
|
|
246
253
|
self.account.start()
|
|
247
254
|
|
|
255
|
+
# - start health metrics monitor
|
|
256
|
+
self._health_monitor.start()
|
|
257
|
+
|
|
248
258
|
# - update universe with initial instruments after the strategy is initialized
|
|
249
259
|
self.set_universe(self._initial_instruments, skip_callback=True)
|
|
250
260
|
|
|
@@ -299,6 +309,9 @@ class StrategyContext(IStrategyContext):
|
|
|
299
309
|
# - stop account processing
|
|
300
310
|
self.account.stop()
|
|
301
311
|
|
|
312
|
+
# - stop health metrics monitor
|
|
313
|
+
self._health_monitor.stop()
|
|
314
|
+
|
|
302
315
|
# - close logging
|
|
303
316
|
self._logging.close()
|
|
304
317
|
|
|
@@ -513,22 +526,30 @@ class StrategyContext(IStrategyContext):
|
|
|
513
526
|
def __process_incoming_data_loop(self, channel: CtrlChannel):
|
|
514
527
|
logger.info("[StrategyContext] :: Start processing market data")
|
|
515
528
|
while channel.control.is_set():
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
529
|
+
try:
|
|
530
|
+
# - waiting for incoming market data
|
|
531
|
+
instrument, d_type, data, hist = channel.receive()
|
|
532
|
+
|
|
533
|
+
_should_record = isinstance(data, Timestamped) and not hist
|
|
534
|
+
if _should_record:
|
|
535
|
+
self._health_monitor.record_start_processing(d_type, dt_64(data.time, "ns"))
|
|
536
|
+
|
|
537
|
+
if self.process_data(instrument, d_type, data, hist):
|
|
524
538
|
channel.stop()
|
|
525
539
|
break
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
540
|
+
|
|
541
|
+
if _should_record:
|
|
542
|
+
self._health_monitor.record_end_processing(d_type, dt_64(data.time, "ns"))
|
|
543
|
+
|
|
544
|
+
except StrategyExceededMaxNumberOfRuntimeFailuresError:
|
|
545
|
+
channel.stop()
|
|
546
|
+
break
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.error(f"Error processing market data: {e}")
|
|
549
|
+
logger.opt(colors=False).error(traceback.format_exc())
|
|
550
|
+
if self._lifecycle_notifier:
|
|
551
|
+
self._lifecycle_notifier.notify_error(self._strategy_name, e)
|
|
552
|
+
# Don't stop the channel here, let it continue processing
|
|
532
553
|
logger.info("[StrategyContext] :: Market data processing stopped")
|
|
533
554
|
|
|
534
555
|
def __instantiate_strategy(self, strategy: IStrategy, config: dict[str, Any] | None) -> IStrategy:
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fast, fixed-size circular buffer implementation using Numba for performance.
|
|
3
|
+
When the deque is full, pushing new elements overwrites the oldest elements.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numba import from_dtype, int32, types
|
|
8
|
+
from numba.experimental import jitclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_deque_class(element_dtype: np.dtype):
|
|
12
|
+
"""
|
|
13
|
+
Dynamically create and return a Deque jitclass that stores elements of the given NumPy dtype.
|
|
14
|
+
|
|
15
|
+
This is a fast, fixed-size circular buffer implementation using Numba for performance.
|
|
16
|
+
When the deque is full, pushing new elements overwrites the oldest elements.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
element_dtype (np.dtype): The NumPy dtype for elements to store
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
jitclass: A compiled Numba class for the deque
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If the element_dtype is not supported
|
|
26
|
+
"""
|
|
27
|
+
# Determine whether it's a structured dtype or a scalar dtype
|
|
28
|
+
if element_dtype.fields is not None:
|
|
29
|
+
# It's a structured dtype. Convert to a Numba 'record' type
|
|
30
|
+
element_type = from_dtype(element_dtype)
|
|
31
|
+
data_type = element_type[:] # 1D array of that struct
|
|
32
|
+
else:
|
|
33
|
+
# It's a scalar dtype (e.g., float32)
|
|
34
|
+
if element_dtype == np.float32:
|
|
35
|
+
data_type = types.float32[:]
|
|
36
|
+
elif element_dtype == np.float64:
|
|
37
|
+
data_type = types.float64[:]
|
|
38
|
+
elif element_dtype == np.int32:
|
|
39
|
+
data_type = types.int32[:]
|
|
40
|
+
elif element_dtype == np.int64:
|
|
41
|
+
data_type = types.int64[:]
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError(f"Unsupported scalar dtype: {element_dtype}")
|
|
44
|
+
|
|
45
|
+
# Build the class spec
|
|
46
|
+
spec = [
|
|
47
|
+
("data", data_type),
|
|
48
|
+
("capacity", int32),
|
|
49
|
+
("head", int32),
|
|
50
|
+
("tail", int32),
|
|
51
|
+
("size", int32),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
class Deque:
|
|
55
|
+
def __init__(self, capacity):
|
|
56
|
+
self.data = np.empty(capacity, dtype=element_dtype)
|
|
57
|
+
self.capacity = capacity
|
|
58
|
+
self.head = 0
|
|
59
|
+
self.tail = 0
|
|
60
|
+
self.size = 0
|
|
61
|
+
|
|
62
|
+
def push_back(self, record):
|
|
63
|
+
if self.size == self.capacity:
|
|
64
|
+
# Overwrite oldest from the front
|
|
65
|
+
self.head = (self.head + 1) % self.capacity
|
|
66
|
+
self.size -= 1
|
|
67
|
+
|
|
68
|
+
self.data[self.tail] = record
|
|
69
|
+
self.tail = (self.tail + 1) % self.capacity
|
|
70
|
+
self.size += 1
|
|
71
|
+
|
|
72
|
+
def push_front(self, record):
|
|
73
|
+
if self.size == self.capacity:
|
|
74
|
+
# Overwrite oldest from the back
|
|
75
|
+
self.tail = (self.tail - 1) % self.capacity
|
|
76
|
+
self.size -= 1
|
|
77
|
+
|
|
78
|
+
self.head = (self.head - 1) % self.capacity
|
|
79
|
+
self.data[self.head] = record
|
|
80
|
+
self.size += 1
|
|
81
|
+
|
|
82
|
+
def pop_front(self):
|
|
83
|
+
if self.size == 0:
|
|
84
|
+
raise IndexError("Deque is empty")
|
|
85
|
+
record = self.data[self.head]
|
|
86
|
+
self.head = (self.head + 1) % self.capacity
|
|
87
|
+
self.size -= 1
|
|
88
|
+
return record
|
|
89
|
+
|
|
90
|
+
def pop_back(self):
|
|
91
|
+
if self.size == 0:
|
|
92
|
+
raise IndexError("Deque is empty")
|
|
93
|
+
self.tail = (self.tail - 1) % self.capacity
|
|
94
|
+
record = self.data[self.tail]
|
|
95
|
+
self.size -= 1
|
|
96
|
+
return record
|
|
97
|
+
|
|
98
|
+
def is_empty(self):
|
|
99
|
+
return self.size == 0
|
|
100
|
+
|
|
101
|
+
def is_full(self):
|
|
102
|
+
return self.size == self.capacity
|
|
103
|
+
|
|
104
|
+
def get_size(self):
|
|
105
|
+
return self.size
|
|
106
|
+
|
|
107
|
+
def __len__(self):
|
|
108
|
+
return self.size
|
|
109
|
+
|
|
110
|
+
def front(self):
|
|
111
|
+
if self.size == 0:
|
|
112
|
+
raise IndexError("Deque is empty")
|
|
113
|
+
return self.data[self.head]
|
|
114
|
+
|
|
115
|
+
def back(self):
|
|
116
|
+
if self.size == 0:
|
|
117
|
+
raise IndexError("Deque is empty")
|
|
118
|
+
return self.data[(self.tail - 1) % self.capacity]
|
|
119
|
+
|
|
120
|
+
def __getitem__(self, idx):
|
|
121
|
+
if idx < 0 or idx >= self.size:
|
|
122
|
+
raise IndexError("Index out of bounds")
|
|
123
|
+
return self.data[(self.tail - idx - 1) % self.capacity]
|
|
124
|
+
|
|
125
|
+
def to_array(self):
|
|
126
|
+
"""Return a NumPy array of the current elements in the Deque, from oldest to newest."""
|
|
127
|
+
out = np.empty(self.size, dtype=self.data.dtype)
|
|
128
|
+
for i in range(self.size):
|
|
129
|
+
idx = (self.head + i) % self.capacity
|
|
130
|
+
out[i] = self.data[idx]
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
def push_back_fields(self): ...
|
|
134
|
+
|
|
135
|
+
# Generate push_back_fields(...) for structured dtypes
|
|
136
|
+
if element_dtype.fields is not None:
|
|
137
|
+
field_names = list(element_dtype.fields.keys())
|
|
138
|
+
arg_list = ", ".join(field_names)
|
|
139
|
+
lines = []
|
|
140
|
+
lines.append(f"def push_back_fields(self, {arg_list}):")
|
|
141
|
+
lines.append(" if self.size == self.capacity:")
|
|
142
|
+
lines.append(" self.head = (self.head + 1) % self.capacity")
|
|
143
|
+
lines.append(" self.size -= 1")
|
|
144
|
+
lines.append("")
|
|
145
|
+
for f in field_names:
|
|
146
|
+
lines.append(f" self.data[self.tail]['{f}'] = {f}")
|
|
147
|
+
lines.append(" self.tail = (self.tail + 1) % self.capacity")
|
|
148
|
+
lines.append(" self.size += 1")
|
|
149
|
+
|
|
150
|
+
method_src = "\n".join(lines)
|
|
151
|
+
tmp_ns = {}
|
|
152
|
+
exec(method_src, {}, tmp_ns)
|
|
153
|
+
push_back_fields_func = tmp_ns["push_back_fields"]
|
|
154
|
+
setattr(Deque, "push_back_fields", push_back_fields_func)
|
|
155
|
+
|
|
156
|
+
_Deque = jitclass(spec)(Deque)
|
|
157
|
+
return _Deque
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Pre-compiled deque types for common use cases
|
|
161
|
+
DequeFloat64 = create_deque_class(np.dtype(np.float64))
|
|
162
|
+
DequeFloat32 = create_deque_class(np.dtype(np.float32))
|
|
163
|
+
DequeInt64 = create_deque_class(np.dtype(np.int64))
|
|
164
|
+
DequeInt32 = create_deque_class(np.dtype(np.int32))
|
|
165
|
+
|
|
166
|
+
# Deque type for storing indicator values with timestamps
|
|
167
|
+
DequeIndicator = create_deque_class(
|
|
168
|
+
np.dtype(
|
|
169
|
+
[
|
|
170
|
+
("timestamp", np.int64),
|
|
171
|
+
("value", np.float64),
|
|
172
|
+
],
|
|
173
|
+
align=True,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Instance types for use in other jitclasses
|
|
178
|
+
DequeIndicator_instance = DequeIndicator.class_type.instance_type
|
|
179
|
+
DequeFloat64_instance = DequeFloat64.class_type.instance_type
|
|
180
|
+
DequeFloat32_instance = DequeFloat32.class_type.instance_type
|
|
181
|
+
DequeInt64_instance = DequeInt64.class_type.instance_type
|
|
182
|
+
DequeInt32_instance = DequeInt32.class_type.instance_type
|