Qubx 0.6.72__tar.gz → 0.6.73__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.72 → qubx-0.6.73}/PKG-INFO +1 -1
- {qubx-0.6.72 → qubx-0.6.73}/pyproject.toml +1 -1
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/data.py +16 -6
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/runner.py +1 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +1 -4
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/connection_manager.py +122 -133
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/data.py +108 -43
- qubx-0.6.73/src/qubx/connectors/ccxt/exchanges/base.py +63 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +159 -154
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +3 -1
- qubx-0.6.73/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +6 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +3 -4
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +3 -1
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/funding_rate.py +88 -88
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/liquidation.py +1 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/ohlc.py +63 -45
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/open_interest.py +12 -13
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/orderbook.py +65 -39
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/quote.py +3 -1
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/trade.py +15 -1
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/reader.py +8 -13
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/subscription_config.py +39 -34
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/subscription_manager.py +103 -118
- qubx-0.6.73/src/qubx/connectors/ccxt/subscription_orchestrator.py +365 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/account.py +5 -5
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/basics.py +19 -1
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/initializer.py +7 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/interfaces.py +21 -5
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/mixins/subscription.py +6 -1
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/data/readers.py +11 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/charting/mpl_helpers.py +134 -0
- qubx-0.6.73/src/qubx/utils/charting/orderbook.py +314 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/runner/runner.py +9 -0
- qubx-0.6.72/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -3
- qubx-0.6.72/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -328
- {qubx-0.6.72 → qubx-0.6.73}/LICENSE +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/README.md +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/build.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/context.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/helpers.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/metrics.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/stale_data_detector.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/features/core.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/features/price.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/health/base.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/base.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.72 → qubx-0.6.73}/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.73"
|
|
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"
|
|
@@ -27,7 +27,7 @@ def _get_first_existing(data: dict, keys: list, default: T = None) -> T:
|
|
|
27
27
|
data_get = data.get # Cache method lookup
|
|
28
28
|
sentinel = object()
|
|
29
29
|
for key in keys:
|
|
30
|
-
if (value := data_get(key, sentinel)) is not sentinel:
|
|
30
|
+
if (value := data_get(key, sentinel)) is not sentinel and value is not None:
|
|
31
31
|
return value
|
|
32
32
|
return default
|
|
33
33
|
|
|
@@ -169,14 +169,24 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
169
169
|
if _b_ts_0 <= cut_time_ns and cut_time_ns < _b_ts_1:
|
|
170
170
|
break
|
|
171
171
|
|
|
172
|
+
# Handle None values in OHLC data
|
|
173
|
+
open_price = r.data["open"]
|
|
174
|
+
high_price = r.data["high"]
|
|
175
|
+
low_price = r.data["low"]
|
|
176
|
+
close_price = r.data["close"]
|
|
177
|
+
|
|
178
|
+
# Skip this record if any OHLC value is None
|
|
179
|
+
if open_price is None or high_price is None or low_price is None or close_price is None:
|
|
180
|
+
continue
|
|
181
|
+
|
|
172
182
|
bars.append(
|
|
173
183
|
Bar(
|
|
174
184
|
_b_ts_0,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
volume=r.data.get("volume", 0),
|
|
185
|
+
open_price,
|
|
186
|
+
high_price,
|
|
187
|
+
low_price,
|
|
188
|
+
close_price,
|
|
189
|
+
volume=r.data.get("volume", 0) or 0, # Handle None volume
|
|
180
190
|
bought_volume=_get_first_existing(r.data, ["taker_buy_volume", "bought_volume"], 0),
|
|
181
191
|
volume_quote=_get_first_existing(r.data, ["quote_volume", "volume_quote"], 0),
|
|
182
192
|
bought_volume_quote=_get_first_existing(
|
|
@@ -98,9 +98,6 @@ class PollingToWebSocketAdapter:
|
|
|
98
98
|
current_symbols = list(self._symbols)
|
|
99
99
|
symbols_changed = self._symbols_changed
|
|
100
100
|
|
|
101
|
-
if not current_symbols:
|
|
102
|
-
raise ValueError(f"No symbols configured for adapter {self.adapter_id}")
|
|
103
|
-
|
|
104
101
|
# If symbols changed, poll immediately
|
|
105
102
|
if symbols_changed:
|
|
106
103
|
logger.debug(f"Symbols changed, polling immediately for adapter {self.adapter_id}")
|
|
@@ -161,7 +158,7 @@ class PollingToWebSocketAdapter:
|
|
|
161
158
|
self._poll_count += 1
|
|
162
159
|
self._last_poll_time = time.time()
|
|
163
160
|
|
|
164
|
-
logger.debug(f"Polling {len(symbols)} symbols for adapter {self.adapter_id}")
|
|
161
|
+
logger.debug(f"Polling {len(symbols) if symbols else 'all'} symbols for adapter {self.adapter_id}")
|
|
165
162
|
|
|
166
163
|
try:
|
|
167
164
|
# Filter out adapter-specific parameters
|
|
@@ -7,14 +7,16 @@ separating connection concerns from subscription state and data handling.
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import concurrent.futures
|
|
10
|
+
import time
|
|
10
11
|
from asyncio.exceptions import CancelledError
|
|
11
12
|
from collections import defaultdict
|
|
12
|
-
from typing import Awaitable, Callable
|
|
13
|
+
from typing import Awaitable, Callable
|
|
13
14
|
|
|
14
15
|
from ccxt import ExchangeClosedByUser, ExchangeError, ExchangeNotAvailable, NetworkError
|
|
15
16
|
from ccxt.pro import Exchange
|
|
16
17
|
from qubx import logger
|
|
17
18
|
from qubx.core.basics import CtrlChannel
|
|
19
|
+
from qubx.utils.misc import AsyncThreadLoop
|
|
18
20
|
|
|
19
21
|
from .exceptions import CcxtSymbolNotRecognized
|
|
20
22
|
from .subscription_manager import SubscriptionManager
|
|
@@ -23,46 +25,51 @@ from .subscription_manager import SubscriptionManager
|
|
|
23
25
|
class ConnectionManager:
|
|
24
26
|
"""
|
|
25
27
|
Manages WebSocket connections and stream lifecycle for CCXT data provider.
|
|
26
|
-
|
|
28
|
+
|
|
27
29
|
Responsibilities:
|
|
28
30
|
- Handle WebSocket connection establishment and management
|
|
29
31
|
- Implement retry logic and error handling
|
|
30
32
|
- Manage stream lifecycle (start, stop, cleanup)
|
|
31
33
|
- Coordinate with SubscriptionManager for state updates
|
|
32
34
|
"""
|
|
33
|
-
|
|
35
|
+
|
|
34
36
|
def __init__(
|
|
35
|
-
self,
|
|
36
|
-
exchange_id: str,
|
|
37
|
+
self,
|
|
38
|
+
exchange_id: str,
|
|
39
|
+
loop: AsyncThreadLoop,
|
|
37
40
|
max_ws_retries: int = 10,
|
|
38
|
-
subscription_manager: SubscriptionManager | None = None
|
|
41
|
+
subscription_manager: SubscriptionManager | None = None,
|
|
42
|
+
cleanup_timeout: float = 3.0,
|
|
39
43
|
):
|
|
40
44
|
self._exchange_id = exchange_id
|
|
45
|
+
self._loop = loop
|
|
41
46
|
self.max_ws_retries = max_ws_retries
|
|
42
47
|
self._subscription_manager = subscription_manager
|
|
43
|
-
|
|
48
|
+
self._cleanup_timeout = cleanup_timeout
|
|
49
|
+
|
|
44
50
|
# Stream state management
|
|
45
|
-
self._is_stream_enabled:
|
|
46
|
-
self._stream_to_unsubscriber:
|
|
47
|
-
|
|
51
|
+
self._is_stream_enabled: dict[str, bool] = defaultdict(lambda: False)
|
|
52
|
+
self._stream_to_unsubscriber: dict[str, Callable[[], Awaitable[None]]] = {}
|
|
53
|
+
|
|
48
54
|
# Connection tracking
|
|
49
|
-
self._stream_to_coro:
|
|
50
|
-
|
|
55
|
+
self._stream_to_coro: dict[str, concurrent.futures.Future] = {}
|
|
56
|
+
|
|
51
57
|
def set_subscription_manager(self, subscription_manager: SubscriptionManager) -> None:
|
|
52
58
|
"""Set the subscription manager for state coordination."""
|
|
53
59
|
self._subscription_manager = subscription_manager
|
|
54
|
-
|
|
60
|
+
|
|
55
61
|
async def listen_to_stream(
|
|
56
62
|
self,
|
|
57
63
|
subscriber: Callable[[], Awaitable[None]],
|
|
58
64
|
exchange: Exchange,
|
|
59
65
|
channel: CtrlChannel,
|
|
66
|
+
subscription_type: str,
|
|
60
67
|
stream_name: str,
|
|
61
68
|
unsubscriber: Callable[[], Awaitable[None]] | None = None,
|
|
62
69
|
) -> None:
|
|
63
70
|
"""
|
|
64
71
|
Listen to a WebSocket stream with error handling and retry logic.
|
|
65
|
-
|
|
72
|
+
|
|
66
73
|
Args:
|
|
67
74
|
subscriber: Async function that handles the stream data
|
|
68
75
|
exchange: CCXT exchange instance
|
|
@@ -71,32 +78,30 @@ class ConnectionManager:
|
|
|
71
78
|
unsubscriber: Optional cleanup function for graceful unsubscription
|
|
72
79
|
"""
|
|
73
80
|
logger.info(f"<yellow>{self._exchange_id}</yellow> Listening to {stream_name}")
|
|
74
|
-
|
|
81
|
+
|
|
75
82
|
# Register unsubscriber for cleanup
|
|
76
83
|
if unsubscriber is not None:
|
|
77
84
|
self._stream_to_unsubscriber[stream_name] = unsubscriber
|
|
78
|
-
|
|
85
|
+
|
|
79
86
|
# Enable the stream
|
|
80
87
|
self._is_stream_enabled[stream_name] = True
|
|
81
88
|
n_retry = 0
|
|
82
89
|
connection_established = False
|
|
83
|
-
|
|
90
|
+
|
|
84
91
|
while channel.control.is_set() and self._is_stream_enabled[stream_name]:
|
|
85
92
|
try:
|
|
86
93
|
await subscriber()
|
|
87
94
|
n_retry = 0 # Reset retry counter on success
|
|
88
|
-
|
|
95
|
+
|
|
89
96
|
# Mark subscription as active on first successful data reception
|
|
90
97
|
if not connection_established and self._subscription_manager:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
connection_established = True
|
|
95
|
-
|
|
98
|
+
self._subscription_manager.mark_subscription_active(subscription_type)
|
|
99
|
+
connection_established = True
|
|
100
|
+
|
|
96
101
|
# Check if stream was disabled during subscriber execution
|
|
97
102
|
if not self._is_stream_enabled[stream_name]:
|
|
98
103
|
break
|
|
99
|
-
|
|
104
|
+
|
|
100
105
|
except CcxtSymbolNotRecognized:
|
|
101
106
|
# Skip unrecognized symbols but continue listening
|
|
102
107
|
continue
|
|
@@ -109,7 +114,9 @@ class ConnectionManager:
|
|
|
109
114
|
break
|
|
110
115
|
except (NetworkError, ExchangeError, ExchangeNotAvailable) as e:
|
|
111
116
|
# Network/exchange errors - retry after short delay
|
|
112
|
-
logger.error(
|
|
117
|
+
logger.error(
|
|
118
|
+
f"<yellow>{self._exchange_id}</yellow> {e.__class__.__name__} :: Error in {stream_name} : {e}"
|
|
119
|
+
)
|
|
113
120
|
await asyncio.sleep(1)
|
|
114
121
|
continue
|
|
115
122
|
except Exception as e:
|
|
@@ -117,10 +124,10 @@ class ConnectionManager:
|
|
|
117
124
|
if not channel.control.is_set() or not self._is_stream_enabled[stream_name]:
|
|
118
125
|
# Channel closed or stream disabled, exit gracefully
|
|
119
126
|
break
|
|
120
|
-
|
|
127
|
+
|
|
121
128
|
logger.error(f"<yellow>{self._exchange_id}</yellow> Exception in {stream_name}: {e}")
|
|
122
129
|
logger.exception(e)
|
|
123
|
-
|
|
130
|
+
|
|
124
131
|
n_retry += 1
|
|
125
132
|
if n_retry >= self.max_ws_retries:
|
|
126
133
|
logger.error(
|
|
@@ -129,182 +136,164 @@ class ConnectionManager:
|
|
|
129
136
|
# Clean up exchange reference to force reconnection
|
|
130
137
|
del exchange
|
|
131
138
|
break
|
|
132
|
-
|
|
139
|
+
|
|
133
140
|
# Exponential backoff with cap at 60 seconds
|
|
134
141
|
await asyncio.sleep(min(2**n_retry, 60))
|
|
135
|
-
|
|
142
|
+
|
|
136
143
|
# Stream ended, cleanup
|
|
137
144
|
logger.debug(f"<yellow>{self._exchange_id}</yellow> Stream {stream_name} ended")
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
self,
|
|
141
|
-
stream_name: str,
|
|
142
|
-
future: concurrent.futures.Future | None = None,
|
|
143
|
-
is_resubscription: bool = False
|
|
144
|
-
) -> None:
|
|
145
|
+
|
|
146
|
+
def stop_stream(self, stream_name: str, wait: bool = True) -> None:
|
|
145
147
|
"""
|
|
146
|
-
Stop a stream
|
|
147
|
-
|
|
148
|
+
Stop a stream (signal it to stop).
|
|
149
|
+
|
|
148
150
|
Args:
|
|
149
151
|
stream_name: Name of the stream to stop
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
wait: If True, wait for stream and unsubscriber to complete (default).
|
|
153
|
+
If False, cancel asynchronously without waiting.
|
|
152
154
|
"""
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# For resubscription, only clean up if the stream is actually disabled
|
|
184
|
-
# (avoid interfering with new streams using the same name)
|
|
185
|
-
if stream_name in self._is_stream_enabled and not self._is_stream_enabled[stream_name]:
|
|
186
|
-
del self._is_stream_enabled[stream_name]
|
|
187
|
-
else:
|
|
188
|
-
# For regular stops, always clean up completely
|
|
189
|
-
self._is_stream_enabled.pop(stream_name, None)
|
|
190
|
-
self._stream_to_coro.pop(stream_name, None)
|
|
191
|
-
|
|
192
|
-
logger.debug(f"<yellow>{self._exchange_id}</yellow> {context.title()} {stream_name} stopped")
|
|
193
|
-
|
|
194
|
-
except Exception as e:
|
|
195
|
-
logger.error(f"<yellow>{self._exchange_id}</yellow> Error stopping {stream_name}")
|
|
196
|
-
logger.exception(e)
|
|
197
|
-
|
|
198
|
-
def register_stream_future(
|
|
199
|
-
self,
|
|
200
|
-
stream_name: str,
|
|
201
|
-
future: concurrent.futures.Future
|
|
202
|
-
) -> None:
|
|
155
|
+
assert self._subscription_manager is not None
|
|
156
|
+
|
|
157
|
+
logger.debug(f"Stopping stream: {stream_name}, wait={wait}")
|
|
158
|
+
|
|
159
|
+
self._is_stream_enabled[stream_name] = False
|
|
160
|
+
|
|
161
|
+
stream_future = self.get_stream_future(stream_name)
|
|
162
|
+
if stream_future:
|
|
163
|
+
stream_future.cancel()
|
|
164
|
+
if wait:
|
|
165
|
+
self._wait(stream_future, stream_name)
|
|
166
|
+
else:
|
|
167
|
+
logger.warning(f"[CONNECTION] No stream future found for {stream_name}")
|
|
168
|
+
|
|
169
|
+
unsubscriber = self.get_stream_unsubscriber(stream_name)
|
|
170
|
+
if unsubscriber:
|
|
171
|
+
logger.debug(f"Calling unsubscriber for {stream_name}")
|
|
172
|
+
unsub_task = self._loop.submit(unsubscriber())
|
|
173
|
+
if wait:
|
|
174
|
+
self._wait(unsub_task, f"unsubscriber for {stream_name}")
|
|
175
|
+
# Wait for 1 second just in case
|
|
176
|
+
self._loop.submit(asyncio.sleep(1)).result()
|
|
177
|
+
else:
|
|
178
|
+
logger.debug(f"No unsubscriber found for {stream_name}")
|
|
179
|
+
|
|
180
|
+
self._is_stream_enabled.pop(stream_name, None)
|
|
181
|
+
self._stream_to_coro.pop(stream_name, None)
|
|
182
|
+
self._stream_to_unsubscriber.pop(stream_name, None)
|
|
183
|
+
|
|
184
|
+
def register_stream_future(self, stream_name: str, future: concurrent.futures.Future) -> None:
|
|
203
185
|
"""
|
|
204
186
|
Register a future for a stream for tracking and cleanup.
|
|
205
|
-
|
|
187
|
+
|
|
206
188
|
Args:
|
|
207
189
|
stream_name: Name of the stream
|
|
208
190
|
future: Future representing the stream task
|
|
209
191
|
"""
|
|
192
|
+
# Add done callback to handle any exceptions and prevent "Future exception was never retrieved"
|
|
193
|
+
future.add_done_callback(lambda f: self._handle_stream_completion(f, stream_name))
|
|
210
194
|
self._stream_to_coro[stream_name] = future
|
|
211
|
-
|
|
195
|
+
|
|
212
196
|
def is_stream_enabled(self, stream_name: str) -> bool:
|
|
213
197
|
"""
|
|
214
198
|
Check if a stream is enabled.
|
|
215
|
-
|
|
199
|
+
|
|
216
200
|
Args:
|
|
217
201
|
stream_name: Name of the stream to check
|
|
218
|
-
|
|
202
|
+
|
|
219
203
|
Returns:
|
|
220
204
|
True if stream is enabled, False otherwise
|
|
221
205
|
"""
|
|
222
206
|
return self._is_stream_enabled.get(stream_name, False)
|
|
223
|
-
|
|
207
|
+
|
|
224
208
|
def get_stream_future(self, stream_name: str) -> concurrent.futures.Future | None:
|
|
225
209
|
"""
|
|
226
210
|
Get the future for a stream.
|
|
227
|
-
|
|
211
|
+
|
|
228
212
|
Args:
|
|
229
213
|
stream_name: Name of the stream
|
|
230
|
-
|
|
214
|
+
|
|
231
215
|
Returns:
|
|
232
216
|
Future if exists, None otherwise
|
|
233
217
|
"""
|
|
234
218
|
return self._stream_to_coro.get(stream_name)
|
|
235
|
-
|
|
236
|
-
def disable_stream(self, stream_name: str) -> None:
|
|
237
|
-
"""
|
|
238
|
-
Disable a stream (signal it to stop).
|
|
239
|
-
|
|
240
|
-
Args:
|
|
241
|
-
stream_name: Name of the stream to disable
|
|
242
|
-
"""
|
|
243
|
-
self._is_stream_enabled[stream_name] = False
|
|
244
|
-
|
|
219
|
+
|
|
245
220
|
def enable_stream(self, stream_name: str) -> None:
|
|
246
221
|
"""
|
|
247
222
|
Enable a stream.
|
|
248
|
-
|
|
223
|
+
|
|
249
224
|
Args:
|
|
250
225
|
stream_name: Name of the stream to enable
|
|
251
226
|
"""
|
|
252
227
|
self._is_stream_enabled[stream_name] = True
|
|
253
|
-
|
|
254
|
-
def set_stream_unsubscriber(
|
|
255
|
-
self,
|
|
256
|
-
stream_name: str,
|
|
257
|
-
unsubscriber: Callable[[], Awaitable[None]]
|
|
258
|
-
) -> None:
|
|
228
|
+
|
|
229
|
+
def set_stream_unsubscriber(self, stream_name: str, unsubscriber: Callable[[], Awaitable[None]]) -> None:
|
|
259
230
|
"""
|
|
260
231
|
Set unsubscriber function for a stream.
|
|
261
|
-
|
|
232
|
+
|
|
262
233
|
Args:
|
|
263
234
|
stream_name: Name of the stream
|
|
264
235
|
unsubscriber: Async function to call for unsubscription
|
|
265
236
|
"""
|
|
266
237
|
self._stream_to_unsubscriber[stream_name] = unsubscriber
|
|
267
|
-
|
|
238
|
+
|
|
268
239
|
def get_stream_unsubscriber(self, stream_name: str) -> Callable[[], Awaitable[None]] | None:
|
|
269
240
|
"""
|
|
270
241
|
Get unsubscriber function for a stream.
|
|
271
|
-
|
|
242
|
+
|
|
272
243
|
Args:
|
|
273
244
|
stream_name: Name of the stream
|
|
274
|
-
|
|
245
|
+
|
|
275
246
|
Returns:
|
|
276
247
|
Unsubscriber function if exists, None otherwise
|
|
277
248
|
"""
|
|
278
249
|
return self._stream_to_unsubscriber.get(stream_name)
|
|
279
|
-
|
|
280
|
-
def set_stream_coro(
|
|
281
|
-
self,
|
|
282
|
-
stream_name: str,
|
|
283
|
-
coro: concurrent.futures.Future
|
|
284
|
-
) -> None:
|
|
250
|
+
|
|
251
|
+
def set_stream_coro(self, stream_name: str, coro: concurrent.futures.Future) -> None:
|
|
285
252
|
"""
|
|
286
253
|
Set coroutine/future for a stream.
|
|
287
|
-
|
|
254
|
+
|
|
288
255
|
Args:
|
|
289
256
|
stream_name: Name of the stream
|
|
290
257
|
coro: Future representing the stream task
|
|
291
258
|
"""
|
|
292
259
|
self._stream_to_coro[stream_name] = coro
|
|
293
|
-
|
|
260
|
+
|
|
294
261
|
def get_stream_coro(self, stream_name: str) -> concurrent.futures.Future | None:
|
|
295
262
|
"""
|
|
296
263
|
Get coroutine/future for a stream.
|
|
297
|
-
|
|
264
|
+
|
|
298
265
|
Args:
|
|
299
266
|
stream_name: Name of the stream
|
|
300
|
-
|
|
267
|
+
|
|
301
268
|
Returns:
|
|
302
269
|
Future if exists, None otherwise
|
|
303
270
|
"""
|
|
304
271
|
return self._stream_to_coro.get(stream_name)
|
|
305
272
|
|
|
306
|
-
def
|
|
307
|
-
"""
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
273
|
+
def _handle_stream_completion(self, future: concurrent.futures.Future, stream_name: str) -> None:
|
|
274
|
+
"""
|
|
275
|
+
Handle stream future completion and any exceptions to prevent 'Future exception was never retrieved'.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
future: The completed future
|
|
279
|
+
stream_name: Name of the stream for logging
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
future.result() # Retrieve result to handle any exceptions
|
|
283
|
+
except Exception:
|
|
284
|
+
pass # Silent handling to prevent "Future exception was never retrieved"
|
|
285
|
+
|
|
286
|
+
def _wait(self, future: concurrent.futures.Future, context: str) -> None:
|
|
287
|
+
"""Wait for future completion with timeout and exception handling."""
|
|
288
|
+
start_wait = time.time()
|
|
289
|
+
while future.running() and (time.time() - start_wait) < self._cleanup_timeout:
|
|
290
|
+
time.sleep(0.1)
|
|
291
|
+
|
|
292
|
+
if future.running():
|
|
293
|
+
logger.warning(f"[{self._exchange_id}] {context} still running after {self._cleanup_timeout}s timeout")
|
|
294
|
+
else:
|
|
295
|
+
# Always retrieve result to handle exceptions properly and prevent "Future exception was never retrieved"
|
|
296
|
+
try:
|
|
297
|
+
future.result() # This will raise any exception that occurred
|
|
298
|
+
except Exception:
|
|
299
|
+
pass # Silent handling during cleanup - UnsubscribeError is expected
|