Qubx 0.6.94__tar.gz → 0.6.95__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.94 → qubx-0.6.95}/PKG-INFO +1 -1
- {qubx-0.6.94 → qubx-0.6.95}/pyproject.toml +1 -1
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/constants.py +4 -2
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/data.py +30 -1
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/base.py +12 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/orderbook.py +9 -1
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/websocket.py +36 -17
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/basics.py +5 -1
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/metrics.py +12 -6
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/misc.py +6 -1
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/orderbook.py +193 -18
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/configs.py +14 -7
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/runner.py +3 -0
- qubx-0.6.95/src/qubx/utils/websocket_manager.py +445 -0
- qubx-0.6.94/src/qubx/utils/websocket_manager.py +0 -592
- {qubx-0.6.94 → qubx-0.6.95}/LICENSE +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/README.md +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/build.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/transfers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/account.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/broker.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/client.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/extensions.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/factory.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/quote.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/stats.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/trades.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/instruments.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/parsers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/reader.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/account.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/context.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/helpers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/interfaces.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/containers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/readers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/storage.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/storages/csv.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/storages/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/transformers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/core.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/price.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/health/base.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/base.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.94 → qubx-0.6.95}/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.95"
|
|
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"
|
|
@@ -70,9 +70,11 @@ API_BASE_TESTNET = "https://testnet.zklighter.elliot.ai"
|
|
|
70
70
|
WS_BASE_MAINNET = "wss://mainnet.zklighter.elliot.ai/stream"
|
|
71
71
|
WS_BASE_TESTNET = "wss://testnet.zklighter.elliot.ai/stream"
|
|
72
72
|
|
|
73
|
-
DEFAULT_PING_INTERVAL =
|
|
74
|
-
DEFAULT_PING_TIMEOUT =
|
|
73
|
+
DEFAULT_PING_INTERVAL = None
|
|
74
|
+
DEFAULT_PING_TIMEOUT = None
|
|
75
75
|
DEFAULT_MAX_RETRIES = 10
|
|
76
|
+
DEFAULT_MAX_SIZE = None
|
|
77
|
+
DEFAULT_MAX_QUEUE = 5000
|
|
76
78
|
|
|
77
79
|
|
|
78
80
|
# Enums for type safety (kept for backward compatibility)
|
|
@@ -82,6 +82,9 @@ class LighterDataProvider(IDataProvider):
|
|
|
82
82
|
# Track if market_stats:all is subscribed (single subscription for all instruments)
|
|
83
83
|
self._market_stats_subscribed: bool = False
|
|
84
84
|
|
|
85
|
+
# Track if reconnection callback has been registered
|
|
86
|
+
self._reconnection_callback_registered: bool = False
|
|
87
|
+
|
|
85
88
|
logger.info("LighterDataProvider initialized")
|
|
86
89
|
|
|
87
90
|
@property
|
|
@@ -160,6 +163,25 @@ class LighterDataProvider(IDataProvider):
|
|
|
160
163
|
f"Subscribed to {subscription_type} for {len(instruments)} instruments: {[i.symbol for i in instruments]}"
|
|
161
164
|
)
|
|
162
165
|
|
|
166
|
+
async def _on_reconnected(self) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Callback invoked after WebSocket reconnection.
|
|
169
|
+
|
|
170
|
+
Resets all handler states to ensure clean state after reconnection.
|
|
171
|
+
This is particularly important for stateful handlers like OrderbookHandler
|
|
172
|
+
which maintain incremental state that becomes invalid after disconnection.
|
|
173
|
+
"""
|
|
174
|
+
logger.info("WebSocket reconnected, resetting all handler states")
|
|
175
|
+
|
|
176
|
+
# Reset all handlers (stateless handlers have empty reset() implementation)
|
|
177
|
+
for handler in self._handlers.values():
|
|
178
|
+
try:
|
|
179
|
+
handler.reset()
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Error resetting handler {handler.__class__.__name__}: {e}")
|
|
182
|
+
|
|
183
|
+
logger.debug(f"Reset {len(self._handlers)} handlers after reconnection")
|
|
184
|
+
|
|
163
185
|
def _ensure_websocket_connected(self, timeout: float = 5.0) -> None:
|
|
164
186
|
"""
|
|
165
187
|
Ensure WebSocket is connected, wait if necessary.
|
|
@@ -192,6 +214,12 @@ class LighterDataProvider(IDataProvider):
|
|
|
192
214
|
await self._ws_manager.connect()
|
|
193
215
|
self._ws_connected = True
|
|
194
216
|
|
|
217
|
+
# Register reconnection callback (one-time setup)
|
|
218
|
+
if not self._reconnection_callback_registered:
|
|
219
|
+
self._ws_manager.on_reconnected(self._on_reconnected)
|
|
220
|
+
self._reconnection_callback_registered = True
|
|
221
|
+
logger.debug("Registered reconnection callback for handler state reset")
|
|
222
|
+
|
|
195
223
|
# Submit and WAIT for connection
|
|
196
224
|
future = self._async_loop.submit(_connect())
|
|
197
225
|
try:
|
|
@@ -302,7 +330,8 @@ class LighterDataProvider(IDataProvider):
|
|
|
302
330
|
handler = cast(OrderbookHandler, self._handlers.get(handler_key))
|
|
303
331
|
|
|
304
332
|
if handler and handler.can_handle(message):
|
|
305
|
-
orderbook =
|
|
333
|
+
orderbook = handler.handle(message)
|
|
334
|
+
orderbook = cast(OrderBook, orderbook)
|
|
306
335
|
if orderbook:
|
|
307
336
|
# Send to channel
|
|
308
337
|
self.channel.send((instrument, "orderbook", orderbook, False))
|
|
@@ -102,3 +102,15 @@ class BaseHandler(ABC, Generic[T]):
|
|
|
102
102
|
"""Reset statistics counters"""
|
|
103
103
|
self._message_count = 0
|
|
104
104
|
self._error_count = 0
|
|
105
|
+
|
|
106
|
+
def reset(self):
|
|
107
|
+
"""
|
|
108
|
+
Reset handler internal state.
|
|
109
|
+
|
|
110
|
+
This method is called when the WebSocket connection is reestablished
|
|
111
|
+
to ensure handlers start with clean state. Handlers with stateful
|
|
112
|
+
components should override this method to reset their state.
|
|
113
|
+
|
|
114
|
+
The default implementation does nothing (stateless handlers).
|
|
115
|
+
"""
|
|
116
|
+
pass
|
|
@@ -58,7 +58,11 @@ class OrderbookHandler(BaseHandler[OrderBook]):
|
|
|
58
58
|
self.max_levels = max_levels
|
|
59
59
|
self.tick_size_pct = tick_size_pct
|
|
60
60
|
self.instrument = instrument
|
|
61
|
-
|
|
61
|
+
|
|
62
|
+
# Initialize state manager with sufficient buffer size
|
|
63
|
+
# Use larger buffer to accommodate raw orderbook before aggregation
|
|
64
|
+
state_manager_max_levels = max(1000, 2 * max_levels)
|
|
65
|
+
self._state_manager = OrderBookStateManager(max_levels=state_manager_max_levels)
|
|
62
66
|
|
|
63
67
|
def can_handle(self, message: dict[str, Any]) -> bool:
|
|
64
68
|
channel = message.get("channel", "")
|
|
@@ -96,6 +100,10 @@ class OrderbookHandler(BaseHandler[OrderBook]):
|
|
|
96
100
|
logger.warning("Missing order_book in message")
|
|
97
101
|
return None
|
|
98
102
|
|
|
103
|
+
is_update = message.get("type") == "update/order_book"
|
|
104
|
+
if not is_update:
|
|
105
|
+
self._state_manager.reset()
|
|
106
|
+
|
|
99
107
|
bids = book.get("bids", [])
|
|
100
108
|
asks = book.get("asks", [])
|
|
101
109
|
bids = [(float(bid["price"]), float(bid["size"])) for bid in bids]
|
|
@@ -13,7 +13,6 @@ from qubx.utils.websocket_manager import BaseWebSocketManager, ReconnectionConfi
|
|
|
13
13
|
from .client import LighterClient
|
|
14
14
|
from .constants import (
|
|
15
15
|
DEFAULT_MAX_RETRIES,
|
|
16
|
-
DEFAULT_PING_INTERVAL,
|
|
17
16
|
DEFAULT_PING_TIMEOUT,
|
|
18
17
|
WS_BASE_MAINNET,
|
|
19
18
|
WS_BASE_TESTNET,
|
|
@@ -43,8 +42,9 @@ class LighterWebSocketManager(BaseWebSocketManager):
|
|
|
43
42
|
client: LighterClient,
|
|
44
43
|
testnet: bool = False,
|
|
45
44
|
reconnection_config: Optional[ReconnectionConfig] = None,
|
|
46
|
-
ping_interval: float | None =
|
|
45
|
+
ping_interval: float | None = None,
|
|
47
46
|
ping_timeout: float | None = DEFAULT_PING_TIMEOUT,
|
|
47
|
+
application_ping_interval: float | None = 20.0,
|
|
48
48
|
):
|
|
49
49
|
"""
|
|
50
50
|
Initialize Lighter WebSocket manager.
|
|
@@ -52,19 +52,25 @@ class LighterWebSocketManager(BaseWebSocketManager):
|
|
|
52
52
|
Args:
|
|
53
53
|
testnet: If True, connect to testnet. Otherwise mainnet.
|
|
54
54
|
reconnection_config: Configuration for reconnection behavior
|
|
55
|
-
ping_interval: Interval
|
|
55
|
+
ping_interval: Interval for protocol-level pings (seconds), or None to disable (default: None)
|
|
56
56
|
ping_timeout: Timeout for ping response (seconds)
|
|
57
|
+
application_ping_interval: Interval for application-level pings (seconds), or None to disable (default: 20.0)
|
|
57
58
|
"""
|
|
58
|
-
url = WS_BASE_TESTNET if testnet else WS_BASE_MAINNET
|
|
59
|
-
|
|
60
59
|
if reconnection_config is None:
|
|
61
60
|
reconnection_config = ReconnectionConfig(max_retries=DEFAULT_MAX_RETRIES)
|
|
62
61
|
|
|
63
62
|
super().__init__(
|
|
64
|
-
url=
|
|
65
|
-
|
|
66
|
-
ping_interval=
|
|
67
|
-
ping_timeout=
|
|
63
|
+
url=WS_BASE_TESTNET if testnet else WS_BASE_MAINNET,
|
|
64
|
+
reconnection=reconnection_config,
|
|
65
|
+
ping_interval=None, # use app-level pings
|
|
66
|
+
ping_timeout=None,
|
|
67
|
+
max_size=16 * 1024 * 1024,
|
|
68
|
+
max_queue=None,
|
|
69
|
+
compression=None,
|
|
70
|
+
inbox_size=5000,
|
|
71
|
+
workers=1,
|
|
72
|
+
app_ping_interval=20.0, # send {"type":"ping"} every 20s
|
|
73
|
+
no_rx_reconnect_after=60.0,
|
|
68
74
|
)
|
|
69
75
|
|
|
70
76
|
self.testnet = testnet
|
|
@@ -334,11 +340,14 @@ class LighterWebSocketManager(BaseWebSocketManager):
|
|
|
334
340
|
Returns:
|
|
335
341
|
Channel identifier or None
|
|
336
342
|
"""
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
343
|
+
try:
|
|
344
|
+
channel = message.get("channel", None)
|
|
345
|
+
if channel:
|
|
346
|
+
# Convert "order_book:0" back to "order_book/0"
|
|
347
|
+
channel = channel.replace(":", "/")
|
|
348
|
+
return channel
|
|
349
|
+
except Exception:
|
|
350
|
+
return None
|
|
342
351
|
|
|
343
352
|
async def _handle_error_message(self, error: dict) -> None:
|
|
344
353
|
"""
|
|
@@ -352,13 +361,16 @@ class LighterWebSocketManager(BaseWebSocketManager):
|
|
|
352
361
|
"""
|
|
353
362
|
error_code = error.get("code", -1)
|
|
354
363
|
error_message = error.get("message", "Unknown error")
|
|
355
|
-
logger.error(f"Lighter WebSocket error [{error_code}] {error_message}")
|
|
356
364
|
match error_code:
|
|
357
365
|
case 20013:
|
|
358
366
|
# expired token
|
|
367
|
+
logger.warning("Auth token expired, generating new one")
|
|
359
368
|
self._update_auth_token()
|
|
369
|
+
case 30003:
|
|
370
|
+
# Alread subscribed
|
|
371
|
+
logger.debug(f"Already subscribed to {error['channel']}")
|
|
360
372
|
case _:
|
|
361
|
-
|
|
373
|
+
logger.warning(f"Lighter WebSocket error [{error_code}] {error_message}")
|
|
362
374
|
|
|
363
375
|
def _update_auth_token(self):
|
|
364
376
|
"""
|
|
@@ -423,6 +435,9 @@ class LighterWebSocketManager(BaseWebSocketManager):
|
|
|
423
435
|
if self._on_connected_callback:
|
|
424
436
|
await self._on_connected_callback()
|
|
425
437
|
|
|
438
|
+
elif msg_type == "pong":
|
|
439
|
+
pass
|
|
440
|
+
|
|
426
441
|
elif msg_type == "ping":
|
|
427
442
|
# Application-level ping - must respond with pong
|
|
428
443
|
logger.debug("Received application-level ping, sending pong")
|
|
@@ -440,4 +455,8 @@ class LighterWebSocketManager(BaseWebSocketManager):
|
|
|
440
455
|
|
|
441
456
|
else:
|
|
442
457
|
# Log unknown messages for debugging
|
|
443
|
-
logger.debug(f"Unhandled Lighter message: {message}")
|
|
458
|
+
# logger.debug(f"Unhandled Lighter message: {message}")
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
def _app_ping_payload(self) -> Optional[dict]:
|
|
462
|
+
return {"type": "ping"}
|
|
@@ -295,7 +295,7 @@ class MarketType(StrEnum):
|
|
|
295
295
|
OPTION = "OPTION"
|
|
296
296
|
|
|
297
297
|
|
|
298
|
-
@dataclass
|
|
298
|
+
@dataclass(order=True)
|
|
299
299
|
class Instrument:
|
|
300
300
|
"""
|
|
301
301
|
Instrument class.
|
|
@@ -326,6 +326,10 @@ class Instrument:
|
|
|
326
326
|
delist_date: datetime | None = None # date when instrument is delisted
|
|
327
327
|
inverse: bool = False # if true, then the future is inverse
|
|
328
328
|
|
|
329
|
+
def __post_init__(self):
|
|
330
|
+
# define how ordering works
|
|
331
|
+
object.__setattr__(self, "sort_index", f"{self.exchange}:{self.market_type}:{self.symbol}")
|
|
332
|
+
|
|
329
333
|
@property
|
|
330
334
|
def price_precision(self):
|
|
331
335
|
if not hasattr(self, "_price_precision"):
|
|
@@ -980,7 +980,8 @@ class TradingSessionResult:
|
|
|
980
980
|
perf = {
|
|
981
981
|
"cagr": 0.0,
|
|
982
982
|
"sharpe": 0.0,
|
|
983
|
-
"
|
|
983
|
+
"mdd_pct": 0.0,
|
|
984
|
+
"daily_turnover": 0.0,
|
|
984
985
|
"gain": 0.0,
|
|
985
986
|
"calmar": 0.0,
|
|
986
987
|
"sortino": 0.0,
|
|
@@ -1000,7 +1001,8 @@ class TradingSessionResult:
|
|
|
1000
1001
|
"performance": {
|
|
1001
1002
|
"cagr": perf.get("cagr", 0.0),
|
|
1002
1003
|
"sharpe": perf.get("sharpe", 0.0),
|
|
1003
|
-
"
|
|
1004
|
+
"mdd_pct": perf.get("mdd_pct", 0.0),
|
|
1005
|
+
"daily_turnover": perf.get("daily_turnover", 0.0),
|
|
1004
1006
|
"total_return": perf.get("gain", 0.0) / self.get_total_capital() * 100
|
|
1005
1007
|
if self.get_total_capital() > 0
|
|
1006
1008
|
else 0.0,
|
|
@@ -1139,7 +1141,7 @@ class TradingSessionResult:
|
|
|
1139
1141
|
params = info["parameters"]
|
|
1140
1142
|
|
|
1141
1143
|
# - performance extracting
|
|
1142
|
-
_dd_mtrx = ["
|
|
1144
|
+
_dd_mtrx = ["mdd_pct", "mdd_usd", "mdd_start", "mdd_peak", "mdd_recover"]
|
|
1143
1145
|
perf_main = {"".join(list(map(str.capitalize, c.split("_")))): v for c, v in perf.items() if c not in _dd_mtrx}
|
|
1144
1146
|
perf_dd = {"".join(list(map(str.capitalize, c.split("_")))): v for c, v in perf.items() if c in _dd_mtrx}
|
|
1145
1147
|
perf_dd["MddStart"] = pd.Timestamp(perf_dd["MddStart"]).strftime("%Y-%m-%d %H:%M:%S")
|
|
@@ -1233,7 +1235,11 @@ description: {_desc}
|
|
|
1233
1235
|
executions = pd.DataFrame()
|
|
1234
1236
|
try:
|
|
1235
1237
|
signals = pd.read_csv(
|
|
1236
|
-
zip_ref.open("signals.csv"),
|
|
1238
|
+
zip_ref.open("signals.csv"),
|
|
1239
|
+
index_col=["timestamp"],
|
|
1240
|
+
parse_dates=["timestamp"],
|
|
1241
|
+
date_format="mixed",
|
|
1242
|
+
low_memory=False,
|
|
1237
1243
|
)
|
|
1238
1244
|
except:
|
|
1239
1245
|
signals = pd.DataFrame()
|
|
@@ -1491,7 +1497,7 @@ def portfolio_metrics(
|
|
|
1491
1497
|
sheet["drawdown_pct"] = mdd_pct
|
|
1492
1498
|
sheet["drawdown_usd"] = dd_data
|
|
1493
1499
|
# 25-May-2019: MDE fixed Max DD pct calculations
|
|
1494
|
-
# sheet["
|
|
1500
|
+
# sheet["mdd_pct_on_init"] = 100 * mdd / init_cash
|
|
1495
1501
|
sheet["mdd_start"] = equity.index[ddstart]
|
|
1496
1502
|
sheet["mdd_peak"] = equity.index[ddpeak]
|
|
1497
1503
|
sheet["mdd_recover"] = equity.index[ddrecover]
|
|
@@ -1672,7 +1678,7 @@ def tearsheet(
|
|
|
1672
1678
|
report = report.set_index("id", append=True).swaplevel()
|
|
1673
1679
|
if sort_by:
|
|
1674
1680
|
report = report.sort_values(by=sort_by, ascending=sort_ascending)
|
|
1675
|
-
return report
|
|
1681
|
+
return report.round(2)
|
|
1676
1682
|
|
|
1677
1683
|
else:
|
|
1678
1684
|
return _tearsheet_single(
|
|
@@ -447,7 +447,12 @@ class AsyncThreadLoop:
|
|
|
447
447
|
self.loop = loop
|
|
448
448
|
|
|
449
449
|
def submit(self, coro: Awaitable) -> concurrent.futures.Future:
|
|
450
|
-
return asyncio.run_coroutine_threadsafe(coro, self.loop)
|
|
450
|
+
return asyncio.run_coroutine_threadsafe(coro, self.loop) # type: ignore
|
|
451
|
+
|
|
452
|
+
async def run_in_executor(
|
|
453
|
+
self, executor: concurrent.futures.Executor, func: Callable, *args, **kwargs
|
|
454
|
+
) -> concurrent.futures.Future:
|
|
455
|
+
return await self.loop.run_in_executor(executor, func, *args, **kwargs)
|
|
451
456
|
|
|
452
457
|
|
|
453
458
|
def synchronized(func: Callable):
|
|
@@ -13,6 +13,7 @@ import numpy as np
|
|
|
13
13
|
import pandas as pd
|
|
14
14
|
from numba import njit, types
|
|
15
15
|
from numba.typed import Dict
|
|
16
|
+
from sortedcontainers import SortedDict
|
|
16
17
|
from tqdm.auto import tqdm
|
|
17
18
|
|
|
18
19
|
from qubx import logger
|
|
@@ -573,32 +574,120 @@ class OrderBookState:
|
|
|
573
574
|
|
|
574
575
|
|
|
575
576
|
class OrderBookStateManager:
|
|
576
|
-
|
|
577
|
+
"""
|
|
578
|
+
Manages orderbook state with efficient updates and lookups.
|
|
579
|
+
|
|
580
|
+
Uses SortedDict for maintaining price levels in sorted order without
|
|
581
|
+
explicit sorting on each access. Pre-allocates numpy buffers to avoid
|
|
582
|
+
allocation overhead on frequent orderbook generation.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
max_levels: Maximum number of orderbook levels to support (default: 1000)
|
|
586
|
+
Used for buffer pre-allocation to avoid repeated allocations
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
def __init__(self, max_levels: int = 1000):
|
|
590
|
+
self.time = None
|
|
591
|
+
self.bids: SortedDict = SortedDict() # Price -> Size (maintained in sorted order)
|
|
592
|
+
self.asks: SortedDict = SortedDict() # Price -> Size (maintained in sorted order)
|
|
593
|
+
self.max_levels = max_levels
|
|
594
|
+
|
|
595
|
+
# Pre-allocate buffers to avoid allocation overhead
|
|
596
|
+
# These are reused across get_orderbook() calls
|
|
597
|
+
self._bids_buffer = np.zeros(max_levels, dtype=np.float64)
|
|
598
|
+
self._asks_buffer = np.zeros(max_levels, dtype=np.float64)
|
|
599
|
+
|
|
600
|
+
def reset(self):
|
|
601
|
+
"""
|
|
602
|
+
Reset orderbook state to initial empty state.
|
|
603
|
+
|
|
604
|
+
This method clears all bid/ask price levels and resets the timestamp.
|
|
605
|
+
Should be called when the WebSocket connection is reestablished to ensure
|
|
606
|
+
clean state before processing new updates.
|
|
607
|
+
"""
|
|
608
|
+
# Recreate SortedDict instances to ensure clean state
|
|
609
|
+
self.bids = SortedDict()
|
|
610
|
+
self.asks = SortedDict()
|
|
577
611
|
self.time = None
|
|
578
|
-
self.bids = {}
|
|
579
|
-
self.asks = {}
|
|
580
612
|
|
|
581
613
|
def get_state(self):
|
|
582
|
-
|
|
583
|
-
|
|
614
|
+
"""
|
|
615
|
+
Get current orderbook state as OrderBookState object.
|
|
616
|
+
|
|
617
|
+
Returns sorted price levels from SortedDict (no explicit sorting needed).
|
|
618
|
+
"""
|
|
619
|
+
# SortedDict maintains sorted order, just convert to list
|
|
620
|
+
bids = list(self.bids.items())
|
|
621
|
+
asks = list(self.asks.items())
|
|
584
622
|
return OrderBookState(bids=bids, asks=asks)
|
|
585
623
|
|
|
586
624
|
def get_orderbook(self, tick_size: float, levels: int) -> OrderBook | None:
|
|
625
|
+
"""
|
|
626
|
+
Generate OrderBook from current state with aggregation.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
tick_size: Price tick size for aggregation
|
|
630
|
+
levels: Number of price levels to include
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
OrderBook object, or None if state is empty or invalid
|
|
634
|
+
|
|
635
|
+
Note:
|
|
636
|
+
Detects crossed orderbook (bid >= ask) which indicates corrupted state
|
|
637
|
+
from missed or out-of-order updates. Automatically resets state on detection.
|
|
638
|
+
"""
|
|
587
639
|
# Check if we have bids and asks
|
|
588
640
|
if not self.bids or not self.asks:
|
|
589
641
|
return None
|
|
590
642
|
|
|
591
|
-
#
|
|
592
|
-
|
|
593
|
-
|
|
643
|
+
# Validate levels parameter
|
|
644
|
+
if levels > self.max_levels:
|
|
645
|
+
logger.warning(
|
|
646
|
+
f"Requested levels ({levels}) exceeds max_levels ({self.max_levels}). "
|
|
647
|
+
f"Using max_levels={self.max_levels}"
|
|
648
|
+
)
|
|
649
|
+
levels = self.max_levels
|
|
650
|
+
|
|
651
|
+
# Get best bid/ask for crossed orderbook detection
|
|
652
|
+
# SortedDict: keys are in ascending order
|
|
653
|
+
best_bid = self.bids.keys()[-1] # Highest bid (last key)
|
|
654
|
+
best_ask = self.asks.keys()[0] # Lowest ask (first key)
|
|
655
|
+
|
|
656
|
+
# Detect crossed orderbook (invalid state)
|
|
657
|
+
# This should rarely happen now with proactive cleaning in update_state()
|
|
658
|
+
if best_bid >= best_ask:
|
|
659
|
+
logger.error(
|
|
660
|
+
f"Crossed orderbook detected AFTER proactive cleaning: "
|
|
661
|
+
f"best_bid={best_bid:.2f} >= best_ask={best_ask:.2f}. "
|
|
662
|
+
f"This indicates extreme message disorder or logic bug. Resetting state."
|
|
663
|
+
)
|
|
664
|
+
self.reset()
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
# Get bids descending (highest first) and asks ascending (lowest first)
|
|
668
|
+
# SortedDict maintains ascending order, so reverse bids
|
|
669
|
+
sorted_bids = list(reversed(self.bids.items()))[:levels]
|
|
670
|
+
sorted_asks = list(self.asks.items())[:levels]
|
|
671
|
+
|
|
672
|
+
if levels == 1:
|
|
673
|
+
return OrderBook(
|
|
674
|
+
time=time_as_nsec(self.time),
|
|
675
|
+
top_bid=best_bid,
|
|
676
|
+
top_ask=best_ask,
|
|
677
|
+
tick_size=tick_size,
|
|
678
|
+
bids=np.array([sorted_bids[0][1]]),
|
|
679
|
+
asks=np.array([sorted_asks[0][1]]),
|
|
680
|
+
)
|
|
594
681
|
|
|
595
|
-
|
|
596
|
-
|
|
682
|
+
# Clear pre-allocated buffers for requested levels
|
|
683
|
+
self._bids_buffer[:levels].fill(0.0)
|
|
684
|
+
self._asks_buffer[:levels].fill(0.0)
|
|
597
685
|
|
|
598
686
|
# Apply accumulation to aggregate into uniform grid
|
|
687
|
+
# Use slices of pre-allocated buffers to avoid allocation
|
|
599
688
|
top_bid_agg, bids_accumulated = accumulate_orderbook_levels(
|
|
600
689
|
np.array(sorted_bids, dtype=np.float64),
|
|
601
|
-
|
|
690
|
+
self._bids_buffer[:levels],
|
|
602
691
|
tick_size,
|
|
603
692
|
True,
|
|
604
693
|
levels,
|
|
@@ -606,12 +695,13 @@ class OrderBookStateManager:
|
|
|
606
695
|
)
|
|
607
696
|
top_ask_agg, asks_accumulated = accumulate_orderbook_levels(
|
|
608
697
|
np.array(sorted_asks, dtype=np.float64),
|
|
609
|
-
|
|
698
|
+
self._asks_buffer[:levels],
|
|
610
699
|
tick_size,
|
|
611
700
|
False,
|
|
612
701
|
levels,
|
|
613
702
|
False, # is_bid=False, sizes_in_quoted=False
|
|
614
703
|
)
|
|
704
|
+
|
|
615
705
|
return OrderBook(
|
|
616
706
|
time=time_as_nsec(self.time),
|
|
617
707
|
top_bid=top_bid_agg,
|
|
@@ -622,13 +712,98 @@ class OrderBookStateManager:
|
|
|
622
712
|
)
|
|
623
713
|
|
|
624
714
|
def update_state(self, time: dt_64, bids: list[tuple[float, float]], asks: list[tuple[float, float]]):
|
|
715
|
+
"""
|
|
716
|
+
Update orderbook state and proactively clean crossed levels.
|
|
717
|
+
|
|
718
|
+
Uses extreme prices from the UPDATE data (not state) for cross-checking:
|
|
719
|
+
- Highest bid in updates is the fresh price to check against asks
|
|
720
|
+
- Lowest ask in updates is the fresh price to check against bids
|
|
721
|
+
|
|
722
|
+
This ensures we only clean based on fresh data, not potentially stale state.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
time: Timestamp of update
|
|
726
|
+
bids: List of (price, size) tuples for bid updates
|
|
727
|
+
asks: List of (price, size) tuples for ask updates
|
|
728
|
+
"""
|
|
625
729
|
self.time = time
|
|
626
|
-
self._update_state(self.bids, bids)
|
|
627
|
-
self._update_state(self.asks, asks)
|
|
628
730
|
|
|
629
|
-
|
|
630
|
-
|
|
731
|
+
# Find highest bid price in updates (excluding zero sizes)
|
|
732
|
+
highest_bid_update = None
|
|
733
|
+
if bids:
|
|
734
|
+
non_zero_bids = [price for price, size in bids if size > 0]
|
|
735
|
+
if non_zero_bids:
|
|
736
|
+
highest_bid_update = max(non_zero_bids)
|
|
737
|
+
|
|
738
|
+
# Apply all bid updates
|
|
739
|
+
for price, size in bids:
|
|
631
740
|
if size == 0:
|
|
632
|
-
|
|
741
|
+
self.bids.pop(price, None)
|
|
633
742
|
else:
|
|
634
|
-
|
|
743
|
+
self.bids[price] = size
|
|
744
|
+
|
|
745
|
+
# Clean crossed asks using highest bid from UPDATE (not state)
|
|
746
|
+
if highest_bid_update is not None:
|
|
747
|
+
self._clean_crossed_asks(highest_bid_update)
|
|
748
|
+
|
|
749
|
+
# Find lowest ask price in updates (excluding zero sizes)
|
|
750
|
+
lowest_ask_update = None
|
|
751
|
+
if asks:
|
|
752
|
+
non_zero_asks = [price for price, size in asks if size > 0]
|
|
753
|
+
if non_zero_asks:
|
|
754
|
+
lowest_ask_update = min(non_zero_asks)
|
|
755
|
+
|
|
756
|
+
# Apply all ask updates
|
|
757
|
+
for price, size in asks:
|
|
758
|
+
if size == 0:
|
|
759
|
+
self.asks.pop(price, None)
|
|
760
|
+
else:
|
|
761
|
+
self.asks[price] = size
|
|
762
|
+
|
|
763
|
+
# Clean crossed bids using lowest ask from UPDATE (not state)
|
|
764
|
+
if lowest_ask_update is not None:
|
|
765
|
+
self._clean_crossed_bids(lowest_ask_update)
|
|
766
|
+
|
|
767
|
+
def _clean_crossed_asks(self, bid_price: float) -> int:
|
|
768
|
+
"""
|
|
769
|
+
Remove all asks at or below the given bid price.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
bid_price: Bid price from fresh update that may cross asks
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
Number of crossed asks removed
|
|
776
|
+
"""
|
|
777
|
+
if not self.asks:
|
|
778
|
+
return 0
|
|
779
|
+
|
|
780
|
+
# Use irange() to efficiently find asks <= bid_price
|
|
781
|
+
crossed_keys = list(self.asks.irange(maximum=bid_price, inclusive=(True, True)))
|
|
782
|
+
|
|
783
|
+
if crossed_keys:
|
|
784
|
+
for key in crossed_keys:
|
|
785
|
+
del self.asks[key]
|
|
786
|
+
|
|
787
|
+
return len(crossed_keys)
|
|
788
|
+
|
|
789
|
+
def _clean_crossed_bids(self, ask_price: float) -> int:
|
|
790
|
+
"""
|
|
791
|
+
Remove all bids at or above the given ask price.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
ask_price: Ask price from fresh update that may cross bids
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
Number of crossed bids removed
|
|
798
|
+
"""
|
|
799
|
+
if not self.bids:
|
|
800
|
+
return 0
|
|
801
|
+
|
|
802
|
+
# Use irange() to efficiently find bids >= ask_price
|
|
803
|
+
crossed_keys = list(self.bids.irange(minimum=ask_price, inclusive=(True, True)))
|
|
804
|
+
|
|
805
|
+
if crossed_keys:
|
|
806
|
+
for key in crossed_keys:
|
|
807
|
+
del self.bids[key]
|
|
808
|
+
|
|
809
|
+
return len(crossed_keys)
|