Qubx 0.6.71__tar.gz → 0.6.72__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.71 → qubx-0.6.72}/PKG-INFO +1 -1
- {qubx-0.6.71 → qubx-0.6.72}/pyproject.toml +1 -1
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/runner.py +6 -4
- qubx-0.6.72/src/qubx/connectors/ccxt/adapters/polling_adapter.py +250 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/__init__.py +26 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +80 -65
- qubx-0.6.72/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +3 -0
- qubx-0.6.72/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +69 -0
- qubx-0.6.72/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +520 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/factory.py +4 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/base.py +2 -1
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/reader.py +181 -69
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/utils.py +50 -26
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/basics.py +6 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/advanced.py +41 -4
- qubx-0.6.71/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -439
- qubx-0.6.71/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -1
- qubx-0.6.71/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -161
- {qubx-0.6.71 → qubx-0.6.72}/LICENSE +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/README.md +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/build.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/account.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/context.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/helpers.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/interfaces.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/metrics.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/stale_data_detector.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/readers.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/core.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/price.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/health/base.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/base.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/runner.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.71 → qubx-0.6.72}/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.72"
|
|
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"
|
|
@@ -4,10 +4,9 @@ import numpy as np
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from tqdm.auto import tqdm
|
|
6
6
|
|
|
7
|
-
from qubx import logger
|
|
7
|
+
from qubx import QubxLogConfig, logger
|
|
8
8
|
from qubx.backtester.sentinels import NoDataContinue
|
|
9
9
|
from qubx.backtester.simulated_data import IterableSimulationData
|
|
10
|
-
from qubx.backtester.utils import SimulationDataConfig, TimeGuardedWrapper
|
|
11
10
|
from qubx.core.account import CompositeAccountProcessor
|
|
12
11
|
from qubx.core.basics import SW, DataType, Instrument, TransactionCostsCalculator
|
|
13
12
|
from qubx.core.context import StrategyContext
|
|
@@ -41,6 +40,7 @@ from .utils import (
|
|
|
41
40
|
SimulatedTimeProvider,
|
|
42
41
|
SimulationDataConfig,
|
|
43
42
|
SimulationSetup,
|
|
43
|
+
TimeGuardedWrapper,
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
|
|
@@ -356,8 +356,9 @@ class SimulationRunner:
|
|
|
356
356
|
return False # No scheduled events, stop simulation
|
|
357
357
|
|
|
358
358
|
def print_latency_report(self) -> None:
|
|
359
|
-
_l_r
|
|
360
|
-
|
|
359
|
+
if (_l_r := SW.latency_report()) is not None:
|
|
360
|
+
_llvl = QubxLogConfig.get_log_level()
|
|
361
|
+
QubxLogConfig.set_log_level("INFO")
|
|
361
362
|
logger.info(
|
|
362
363
|
"<BLUE> Time spent in simulation report </BLUE>\n<r>"
|
|
363
364
|
+ _frame_to_str(
|
|
@@ -365,6 +366,7 @@ class SimulationRunner:
|
|
|
365
366
|
)
|
|
366
367
|
+ "</r>"
|
|
367
368
|
)
|
|
369
|
+
QubxLogConfig.set_log_level(_llvl)
|
|
368
370
|
|
|
369
371
|
def _create_backtest_context(self) -> IStrategyContext:
|
|
370
372
|
logger.debug(
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simplified polling adapter to convert CCXT fetch_* methods into watch_* behavior.
|
|
3
|
+
|
|
4
|
+
This adapter provides a much simpler approach:
|
|
5
|
+
- No background tasks or queues
|
|
6
|
+
- get_next_data() waits until it's time to poll, then polls synchronously
|
|
7
|
+
- Time-aligned polling (e.g., 11:30, 11:35, 11:40 for 5-minute intervals)
|
|
8
|
+
- Immediate polling when symbols change
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import math
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
16
|
+
|
|
17
|
+
from qubx import logger
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
DEFAULT_POLL_INTERVAL = 300 # 5 minutes
|
|
21
|
+
MIN_POLL_INTERVAL = 1 # 1 second minimum
|
|
22
|
+
MAX_POLL_INTERVAL = 3600 # 1 hour maximum
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PollingConfig:
|
|
27
|
+
"""Configuration for polling adapter."""
|
|
28
|
+
|
|
29
|
+
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL
|
|
30
|
+
|
|
31
|
+
def __post_init__(self):
|
|
32
|
+
"""Validate configuration after initialization."""
|
|
33
|
+
if not MIN_POLL_INTERVAL <= self.poll_interval_seconds <= MAX_POLL_INTERVAL:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"poll_interval_seconds must be between {MIN_POLL_INTERVAL} and {MAX_POLL_INTERVAL}, "
|
|
36
|
+
f"got {self.poll_interval_seconds}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PollingToWebSocketAdapter:
|
|
41
|
+
"""
|
|
42
|
+
Simplified polling adapter that polls synchronously when data is requested.
|
|
43
|
+
|
|
44
|
+
Key features:
|
|
45
|
+
- No background tasks or queues
|
|
46
|
+
- Time-aligned polling (respects clock boundaries)
|
|
47
|
+
- Immediate polling when symbols change
|
|
48
|
+
- Thread-safe symbol management
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
fetch_method: Callable,
|
|
54
|
+
symbols: Optional[List[str]] = None,
|
|
55
|
+
params: Optional[Dict[str, Any]] = None,
|
|
56
|
+
config: Optional[PollingConfig] = None,
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
Initialize the simplified polling adapter.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
fetch_method: The CCXT fetch_* method to call
|
|
63
|
+
symbols: Initial list of symbols to watch
|
|
64
|
+
params: Additional parameters for fetch_method
|
|
65
|
+
config: PollingConfig instance (uses default if None)
|
|
66
|
+
"""
|
|
67
|
+
self.config = config if config is not None else PollingConfig()
|
|
68
|
+
self.fetch_method = fetch_method
|
|
69
|
+
self.params = params or {}
|
|
70
|
+
self.adapter_id = f"polling_adapter_{id(self)}"
|
|
71
|
+
|
|
72
|
+
# Thread-safe symbol management
|
|
73
|
+
self._symbols_lock = asyncio.Lock()
|
|
74
|
+
self._symbols: Set[str] = set(symbols or [])
|
|
75
|
+
|
|
76
|
+
# Polling state
|
|
77
|
+
self._last_poll_time: Optional[float] = None
|
|
78
|
+
self._symbols_changed = False # Flag to trigger immediate poll
|
|
79
|
+
|
|
80
|
+
# Statistics
|
|
81
|
+
self._poll_count = 0
|
|
82
|
+
self._error_count = 0
|
|
83
|
+
|
|
84
|
+
async def get_next_data(self) -> Dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
Get the next available data by waiting until it's time to poll, then polling.
|
|
87
|
+
|
|
88
|
+
This method:
|
|
89
|
+
1. Checks if symbols changed (immediate poll)
|
|
90
|
+
2. Calculates when next poll should happen based on time alignment
|
|
91
|
+
3. Waits until that time
|
|
92
|
+
4. Polls and returns fresh data
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Dictionary containing fetched data for symbols
|
|
96
|
+
"""
|
|
97
|
+
async with self._symbols_lock:
|
|
98
|
+
current_symbols = list(self._symbols)
|
|
99
|
+
symbols_changed = self._symbols_changed
|
|
100
|
+
|
|
101
|
+
if not current_symbols:
|
|
102
|
+
raise ValueError(f"No symbols configured for adapter {self.adapter_id}")
|
|
103
|
+
|
|
104
|
+
# If symbols changed, poll immediately
|
|
105
|
+
if symbols_changed:
|
|
106
|
+
logger.debug(f"Symbols changed, polling immediately for adapter {self.adapter_id}")
|
|
107
|
+
async with self._symbols_lock:
|
|
108
|
+
self._symbols_changed = False
|
|
109
|
+
return await self._poll_now(current_symbols)
|
|
110
|
+
|
|
111
|
+
# Calculate wait time for next aligned poll
|
|
112
|
+
wait_time = self._calculate_wait_time()
|
|
113
|
+
|
|
114
|
+
if wait_time > 0:
|
|
115
|
+
logger.debug(f"Waiting {wait_time:.1f}s for next poll cycle for adapter {self.adapter_id}")
|
|
116
|
+
await asyncio.sleep(wait_time)
|
|
117
|
+
|
|
118
|
+
# Time to poll
|
|
119
|
+
logger.debug(f"Polling now for adapter {self.adapter_id}")
|
|
120
|
+
return await self._poll_now(current_symbols)
|
|
121
|
+
|
|
122
|
+
def _calculate_wait_time(self) -> float:
|
|
123
|
+
"""
|
|
124
|
+
Calculate how long to wait until the next aligned poll time.
|
|
125
|
+
|
|
126
|
+
For intervals >= 1 minute: aligns to clock boundaries (11:30, 11:35, 11:40)
|
|
127
|
+
For intervals < 1 minute: uses simple interval-based timing
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Number of seconds to wait (0 if should poll now)
|
|
131
|
+
"""
|
|
132
|
+
current_time = time.time()
|
|
133
|
+
interval_seconds = self.config.poll_interval_seconds
|
|
134
|
+
|
|
135
|
+
# First poll is always immediate
|
|
136
|
+
if self._last_poll_time is None:
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
if interval_seconds >= 60:
|
|
140
|
+
# Time-aligned polling for intervals >= 1 minute using UTC
|
|
141
|
+
# Calculate next boundary based on seconds since epoch
|
|
142
|
+
next_boundary = math.ceil(current_time / interval_seconds) * interval_seconds
|
|
143
|
+
wait_time = next_boundary - current_time
|
|
144
|
+
return max(0, wait_time)
|
|
145
|
+
else:
|
|
146
|
+
# Simple interval-based polling for sub-minute intervals
|
|
147
|
+
next_poll_time = self._last_poll_time + interval_seconds
|
|
148
|
+
wait_time = next_poll_time - current_time
|
|
149
|
+
return max(0, wait_time)
|
|
150
|
+
|
|
151
|
+
async def _poll_now(self, symbols: List[str]) -> Dict[str, Any]:
|
|
152
|
+
"""
|
|
153
|
+
Perform a poll operation immediately.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
symbols: List of symbols to poll for
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dictionary containing fetched data for symbols
|
|
160
|
+
"""
|
|
161
|
+
self._poll_count += 1
|
|
162
|
+
self._last_poll_time = time.time()
|
|
163
|
+
|
|
164
|
+
logger.debug(f"Polling {len(symbols)} symbols for adapter {self.adapter_id}")
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
# Filter out adapter-specific parameters
|
|
168
|
+
adapter_params = {"pollInterval", "interval", "updateInterval", "poll_interval_minutes"}
|
|
169
|
+
fetch_params = {k: v for k, v in self.params.items() if k not in adapter_params}
|
|
170
|
+
|
|
171
|
+
# Call the fetch method
|
|
172
|
+
result = await self.fetch_method(symbols, **fetch_params)
|
|
173
|
+
|
|
174
|
+
logger.debug(f"Poll completed successfully for adapter {self.adapter_id}")
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
self._error_count += 1
|
|
179
|
+
logger.error(f"Poll failed for adapter {self.adapter_id}: {e}")
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
async def update_symbols(self, new_symbols: List[str]) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Update the symbol list.
|
|
185
|
+
|
|
186
|
+
If symbols changed, the next call to get_next_data() will poll immediately.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
new_symbols: New complete list of symbols to watch
|
|
190
|
+
"""
|
|
191
|
+
async with self._symbols_lock:
|
|
192
|
+
old_symbols = self._symbols.copy()
|
|
193
|
+
self._symbols = set(new_symbols or [])
|
|
194
|
+
symbols_changed = old_symbols != self._symbols
|
|
195
|
+
|
|
196
|
+
if symbols_changed:
|
|
197
|
+
self._symbols_changed = True
|
|
198
|
+
logger.debug(
|
|
199
|
+
f"Symbols updated for adapter {self.adapter_id}: {len(old_symbols)} -> {len(self._symbols)}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def add_symbols(self, new_symbols: List[str]) -> None:
|
|
203
|
+
"""Add symbols to the existing watch list."""
|
|
204
|
+
if not new_symbols:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
async with self._symbols_lock:
|
|
208
|
+
before_count = len(self._symbols)
|
|
209
|
+
self._symbols.update(new_symbols)
|
|
210
|
+
after_count = len(self._symbols)
|
|
211
|
+
|
|
212
|
+
if after_count > before_count:
|
|
213
|
+
self._symbols_changed = True
|
|
214
|
+
logger.debug(f"Added {after_count - before_count} symbols to adapter {self.adapter_id}")
|
|
215
|
+
|
|
216
|
+
async def remove_symbols(self, symbols_to_remove: List[str]) -> None:
|
|
217
|
+
"""Remove symbols from the watch list."""
|
|
218
|
+
if not symbols_to_remove:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
async with self._symbols_lock:
|
|
222
|
+
before_count = len(self._symbols)
|
|
223
|
+
self._symbols.difference_update(symbols_to_remove)
|
|
224
|
+
after_count = len(self._symbols)
|
|
225
|
+
|
|
226
|
+
if after_count < before_count:
|
|
227
|
+
self._symbols_changed = True
|
|
228
|
+
logger.debug(f"Removed {before_count - after_count} symbols from adapter {self.adapter_id}")
|
|
229
|
+
|
|
230
|
+
def is_watching(self, symbol: Optional[str] = None) -> bool:
|
|
231
|
+
"""Check if adapter has symbols configured to watch."""
|
|
232
|
+
if symbol is None:
|
|
233
|
+
return len(self._symbols) > 0
|
|
234
|
+
else:
|
|
235
|
+
return symbol in self._symbols
|
|
236
|
+
|
|
237
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
238
|
+
"""Get adapter statistics for monitoring."""
|
|
239
|
+
return {
|
|
240
|
+
"adapter_id": self.adapter_id,
|
|
241
|
+
"symbol_count": len(self._symbols),
|
|
242
|
+
"poll_count": self._poll_count,
|
|
243
|
+
"error_count": self._error_count,
|
|
244
|
+
"last_poll_time": self._last_poll_time,
|
|
245
|
+
"poll_interval_seconds": self.config.poll_interval_seconds,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async def stop(self) -> None:
|
|
249
|
+
"""Stop the adapter (cleanup method for compatibility)."""
|
|
250
|
+
logger.debug(f"Adapter {self.adapter_id} stopped (polled {self._poll_count} times, {self._error_count} errors)")
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
This module contains the CCXT connectors for the exchanges.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from functools import partial
|
|
6
7
|
|
|
7
8
|
import ccxt.pro as cxp
|
|
@@ -11,9 +12,21 @@ from .binance.broker import BinanceCcxtBroker
|
|
|
11
12
|
from .binance.exchange import BINANCE_UM_MM, BinancePortfolioMargin, BinanceQV, BinanceQVUSDM
|
|
12
13
|
from .bitfinex.bitfinex import BitfinexF
|
|
13
14
|
from .bitfinex.bitfinex_account import BitfinexAccountProcessor
|
|
15
|
+
from .hyperliquid.broker import HyperliquidCcxtBroker
|
|
14
16
|
from .hyperliquid.hyperliquid import Hyperliquid, HyperliquidF
|
|
15
17
|
from .kraken.kraken import CustomKrakenFutures
|
|
16
18
|
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ReaderCapabilities:
|
|
22
|
+
"""Configuration for exchange-specific reader capabilities."""
|
|
23
|
+
|
|
24
|
+
supports_bulk_funding: bool = True
|
|
25
|
+
supports_bulk_ohlcv: bool = True
|
|
26
|
+
max_symbols_per_request: int = 1000
|
|
27
|
+
default_funding_interval_hours: float = 8.0 # Default for most exchanges (Binance, etc.)
|
|
28
|
+
|
|
29
|
+
|
|
17
30
|
EXCHANGE_ALIASES = {
|
|
18
31
|
"binance": "binanceqv",
|
|
19
32
|
"binance.um": "binanceqv_usdm",
|
|
@@ -32,12 +45,25 @@ CUSTOM_BROKERS = {
|
|
|
32
45
|
"binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
33
46
|
"binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
|
|
34
47
|
"bitfinex.f": partial(CcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
|
|
48
|
+
"hyperliquid": partial(HyperliquidCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
49
|
+
"hyperliquid.f": partial(HyperliquidCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
CUSTOM_ACCOUNTS = {
|
|
38
53
|
"bitfinex.f": BitfinexAccountProcessor,
|
|
39
54
|
}
|
|
40
55
|
|
|
56
|
+
READER_CAPABILITIES = {
|
|
57
|
+
"hyperliquid": ReaderCapabilities(
|
|
58
|
+
supports_bulk_funding=False,
|
|
59
|
+
default_funding_interval_hours=1.0 # Hyperliquid uses 1-hour funding
|
|
60
|
+
),
|
|
61
|
+
"hyperliquid.f": ReaderCapabilities(
|
|
62
|
+
supports_bulk_funding=False,
|
|
63
|
+
default_funding_interval_hours=1.0 # Hyperliquid uses 1-hour funding
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
cxp.binanceqv = BinanceQV # type: ignore
|
|
42
68
|
cxp.binanceqv_usdm = BinanceQVUSDM # type: ignore
|
|
43
69
|
cxp.binancepm = BinancePortfolioMargin # type: ignore
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, List
|
|
1
|
+
from typing import Dict, List, cast
|
|
2
2
|
|
|
3
3
|
import ccxt.pro as cxp
|
|
4
4
|
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp
|
|
@@ -33,7 +33,7 @@ class BinanceQV(cxp.binance):
|
|
|
33
33
|
"watchTrades": {
|
|
34
34
|
"name": "aggTrade",
|
|
35
35
|
},
|
|
36
|
-
"localOrderBookLimit":
|
|
36
|
+
"localOrderBookLimit": 50_000, # set a large limit to avoid cutting off the orderbook
|
|
37
37
|
},
|
|
38
38
|
"exceptions": {
|
|
39
39
|
"exact": {
|
|
@@ -243,23 +243,23 @@ class BinanceQV(cxp.binance):
|
|
|
243
243
|
def clean_stream_state(self, subscription: dict):
|
|
244
244
|
"""
|
|
245
245
|
Clean up stream state mappings during unsubscription to prevent UnsubscribeError.
|
|
246
|
-
|
|
246
|
+
|
|
247
247
|
This fixes the root cause of bulk unsubscription failures by properly cleaning up
|
|
248
248
|
stale stream mappings that cause state conflicts in CCXT.
|
|
249
249
|
"""
|
|
250
|
-
symbols = self.safe_list(subscription,
|
|
251
|
-
topic = self.safe_string(subscription,
|
|
252
|
-
|
|
250
|
+
symbols = self.safe_list(subscription, "symbols", [])
|
|
251
|
+
topic = self.safe_string(subscription, "topic", "")
|
|
252
|
+
|
|
253
253
|
if topic and len(symbols) > 1:
|
|
254
254
|
# Clean up bulk subscription stream mappings
|
|
255
255
|
subscription_hash = f"multiple{topic.upper()}"
|
|
256
|
-
streamBySubscriptionsHash = self.safe_dict(self.options,
|
|
256
|
+
streamBySubscriptionsHash = self.safe_dict(self.options, "streamBySubscriptionsHash", {})
|
|
257
257
|
if subscription_hash in streamBySubscriptionsHash:
|
|
258
258
|
stream = streamBySubscriptionsHash[subscription_hash]
|
|
259
259
|
del streamBySubscriptionsHash[subscription_hash]
|
|
260
|
-
|
|
260
|
+
|
|
261
261
|
# Clean up subscription counts to maintain accurate state
|
|
262
|
-
numSubscriptionsByStream = self.safe_dict(self.options,
|
|
262
|
+
numSubscriptionsByStream = self.safe_dict(self.options, "numSubscriptionsByStream", {})
|
|
263
263
|
if stream in numSubscriptionsByStream:
|
|
264
264
|
current_count = numSubscriptionsByStream[stream]
|
|
265
265
|
new_count = max(0, current_count - len(symbols))
|
|
@@ -267,11 +267,11 @@ class BinanceQV(cxp.binance):
|
|
|
267
267
|
del numSubscriptionsByStream[stream]
|
|
268
268
|
else:
|
|
269
269
|
numSubscriptionsByStream[stream] = new_count
|
|
270
|
-
|
|
270
|
+
|
|
271
271
|
def clean_cache(self, subscription: dict):
|
|
272
272
|
"""
|
|
273
273
|
Override clean_cache to include stream state cleanup.
|
|
274
|
-
|
|
274
|
+
|
|
275
275
|
This ensures proper cleanup during unsubscription operations.
|
|
276
276
|
"""
|
|
277
277
|
super().clean_cache(subscription)
|
|
@@ -280,94 +280,100 @@ class BinanceQV(cxp.binance):
|
|
|
280
280
|
async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}):
|
|
281
281
|
"""
|
|
282
282
|
Enhanced bulk OHLCV unsubscription with proper state validation and cleanup.
|
|
283
|
-
|
|
283
|
+
|
|
284
284
|
This override fixes UnsubscribeError issues by:
|
|
285
285
|
1. Validating subscription state before attempting unsubscription
|
|
286
|
-
2. Filtering to only valid subscriptions
|
|
286
|
+
2. Filtering to only valid subscriptions
|
|
287
287
|
3. Implementing graceful error handling for state conflicts
|
|
288
288
|
4. Forcing cleanup of remaining state on errors
|
|
289
289
|
"""
|
|
290
290
|
await self.load_markets()
|
|
291
|
-
|
|
291
|
+
|
|
292
292
|
# Standard setup from parent class
|
|
293
|
-
type =
|
|
293
|
+
type = "spot"
|
|
294
294
|
marketType = None
|
|
295
295
|
firstMarket = None
|
|
296
|
-
|
|
296
|
+
|
|
297
297
|
# Handle futures market detection
|
|
298
298
|
if len(symbolsAndTimeframes) > 0:
|
|
299
299
|
firstSymbol = symbolsAndTimeframes[0][0]
|
|
300
300
|
firstMarket = self.market(firstSymbol)
|
|
301
|
-
marketType, params = self.handle_market_type_and_params(
|
|
302
|
-
if marketType !=
|
|
303
|
-
type =
|
|
304
|
-
|
|
301
|
+
marketType, params = self.handle_market_type_and_params("unWatchOHLCVForSymbols", firstMarket, params)
|
|
302
|
+
if marketType != "spot":
|
|
303
|
+
type = "future"
|
|
304
|
+
|
|
305
305
|
# Build subscription hashes
|
|
306
306
|
messageHashes = []
|
|
307
307
|
subMessageHashes = []
|
|
308
308
|
rawHashes = []
|
|
309
|
-
|
|
309
|
+
|
|
310
310
|
for symbolAndTimeframe in symbolsAndTimeframes:
|
|
311
311
|
symbol = symbolAndTimeframe[0]
|
|
312
312
|
timeframe = symbolAndTimeframe[1]
|
|
313
313
|
market = self.market(symbol)
|
|
314
|
-
lowercaseId = market[
|
|
314
|
+
lowercaseId = market["lowercaseId"]
|
|
315
315
|
interval = self.timeframes[timeframe]
|
|
316
|
-
rawHash = lowercaseId +
|
|
316
|
+
rawHash = lowercaseId + "@kline_" + interval
|
|
317
317
|
rawHashes.append(rawHash)
|
|
318
|
-
messageHash =
|
|
318
|
+
messageHash = "ohlcv:" + symbol + ":" + timeframe
|
|
319
319
|
messageHashes.append(messageHash)
|
|
320
320
|
subMessageHashes.append(rawHash)
|
|
321
|
-
|
|
321
|
+
|
|
322
322
|
# Get client and validate subscription state BEFORE attempting unsubscription
|
|
323
|
-
url = self.urls[
|
|
323
|
+
url = self.urls["api"]["ws"][type] + "/" + self.stream(type, "multipleOHLCV")
|
|
324
324
|
client = self.client(url)
|
|
325
|
-
|
|
325
|
+
|
|
326
326
|
# Filter to only valid subscriptions to prevent state conflicts
|
|
327
327
|
valid_messageHashes = []
|
|
328
328
|
valid_subMessageHashes = []
|
|
329
329
|
valid_rawHashes = []
|
|
330
|
-
|
|
330
|
+
|
|
331
331
|
for i, subHash in enumerate(subMessageHashes):
|
|
332
332
|
if subHash in client.subscriptions:
|
|
333
333
|
valid_messageHashes.append(messageHashes[i])
|
|
334
334
|
valid_subMessageHashes.append(subHash)
|
|
335
335
|
valid_rawHashes.append(rawHashes[i])
|
|
336
|
-
|
|
336
|
+
|
|
337
337
|
if not valid_messageHashes:
|
|
338
338
|
# Nothing to unsubscribe - return success
|
|
339
339
|
return True
|
|
340
|
-
|
|
340
|
+
|
|
341
341
|
# Build unsubscription request with only valid subscriptions
|
|
342
342
|
requestId = self.request_id(url)
|
|
343
343
|
request = {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
"method": "UNSUBSCRIBE",
|
|
345
|
+
"params": valid_rawHashes,
|
|
346
|
+
"id": requestId,
|
|
347
347
|
}
|
|
348
|
-
|
|
348
|
+
|
|
349
349
|
subscription = {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
350
|
+
"unsubscribe": True,
|
|
351
|
+
"id": str(requestId),
|
|
352
|
+
"subMessageHashes": valid_subMessageHashes,
|
|
353
|
+
"messageHashes": valid_messageHashes,
|
|
354
|
+
"symbols": [
|
|
355
|
+
st[0]
|
|
356
|
+
for st in symbolsAndTimeframes
|
|
357
|
+
if st[0] in [sh.split("@")[0].upper() for sh in valid_subMessageHashes]
|
|
358
|
+
],
|
|
359
|
+
"topic": "ohlcv",
|
|
356
360
|
}
|
|
357
|
-
|
|
361
|
+
|
|
358
362
|
try:
|
|
359
363
|
# Attempt unsubscription with validated state
|
|
360
|
-
return await self.watch_multiple(
|
|
361
|
-
|
|
364
|
+
return await self.watch_multiple(
|
|
365
|
+
url, valid_messageHashes, self.extend(request, params), valid_messageHashes, subscription
|
|
366
|
+
)
|
|
367
|
+
|
|
362
368
|
except Exception as e:
|
|
363
369
|
# Handle UnsubscribeError and other state conflicts gracefully
|
|
364
370
|
from ccxt.base.errors import UnsubscribeError
|
|
365
|
-
|
|
366
|
-
if isinstance(e, UnsubscribeError) or
|
|
371
|
+
|
|
372
|
+
if isinstance(e, UnsubscribeError) or "UnsubscribeError" in str(type(e)):
|
|
367
373
|
# Log the issue but don't crash - force cleanup instead
|
|
368
|
-
if hasattr(self,
|
|
374
|
+
if hasattr(self, "logger"):
|
|
369
375
|
self.logger.warning(f"Bulk OHLCV unsubscription state conflict, forcing cleanup: {e}")
|
|
370
|
-
|
|
376
|
+
|
|
371
377
|
# Force cleanup of remaining subscription state
|
|
372
378
|
for subHash in valid_subMessageHashes:
|
|
373
379
|
if subHash in client.subscriptions:
|
|
@@ -378,10 +384,10 @@ class BinanceQV(cxp.binance):
|
|
|
378
384
|
except:
|
|
379
385
|
pass
|
|
380
386
|
del client.futures[subHash]
|
|
381
|
-
|
|
387
|
+
|
|
382
388
|
# Clean up our stream state as well
|
|
383
389
|
self.clean_stream_state(subscription)
|
|
384
|
-
|
|
390
|
+
|
|
385
391
|
return True # Return success after cleanup
|
|
386
392
|
else:
|
|
387
393
|
# Re-raise other errors
|
|
@@ -418,32 +424,40 @@ class BinanceQVUSDM(cxp.binanceusdm, BinanceQV):
|
|
|
418
424
|
},
|
|
419
425
|
)
|
|
420
426
|
|
|
427
|
+
async def get_funding_interval_hours_for_symbol(self, symbol: str) -> float:
|
|
428
|
+
await self._update_funding_intervals()
|
|
429
|
+
return cast(float, self._funding_intervals.get(symbol, 8.0))
|
|
430
|
+
|
|
431
|
+
async def get_funding_interval_hours(self) -> dict[str, float]:
|
|
432
|
+
await self._update_funding_intervals()
|
|
433
|
+
return cast(dict[str, float], self._funding_intervals)
|
|
434
|
+
|
|
421
435
|
async def watch_funding_rates(self, symbols: List[str] | None = None):
|
|
422
436
|
symbol_count = len(symbols) if symbols else 0
|
|
423
|
-
|
|
437
|
+
|
|
424
438
|
try:
|
|
425
439
|
await self.load_markets()
|
|
426
440
|
await self._update_funding_intervals()
|
|
427
|
-
|
|
441
|
+
|
|
428
442
|
# Use watch_mark_prices which streams one symbol per WebSocket message
|
|
429
443
|
# This is normal behavior - WebSocket messages contain one symbol at a time
|
|
430
444
|
mark_prices = await self.watch_mark_prices(symbols)
|
|
431
|
-
|
|
445
|
+
|
|
432
446
|
if not mark_prices:
|
|
433
447
|
raise Exception("No mark price data received")
|
|
434
|
-
|
|
448
|
+
|
|
435
449
|
# Process whatever symbol(s) we received (usually 1 per WebSocket message)
|
|
436
450
|
funding_rates = {}
|
|
437
451
|
processed_count = 0
|
|
438
|
-
|
|
452
|
+
|
|
439
453
|
for symbol, info in mark_prices.items():
|
|
440
454
|
try:
|
|
441
455
|
interval = self._funding_intervals.get(symbol, "8h")
|
|
442
|
-
|
|
456
|
+
|
|
443
457
|
# Ensure we have the required fields for funding rate
|
|
444
458
|
if "info" not in info or "r" not in info["info"]:
|
|
445
459
|
continue
|
|
446
|
-
|
|
460
|
+
|
|
447
461
|
funding_rates[symbol] = {
|
|
448
462
|
"timestamp": info["timestamp"],
|
|
449
463
|
"interval": interval,
|
|
@@ -453,15 +467,15 @@ class BinanceQVUSDM(cxp.binanceusdm, BinanceQV):
|
|
|
453
467
|
"indexPrice": info["indexPrice"],
|
|
454
468
|
}
|
|
455
469
|
processed_count += 1
|
|
456
|
-
|
|
470
|
+
|
|
457
471
|
except Exception as e:
|
|
458
472
|
continue
|
|
459
|
-
|
|
473
|
+
|
|
460
474
|
if processed_count == 0:
|
|
461
475
|
raise Exception("No funding rates could be processed from mark price data")
|
|
462
|
-
|
|
476
|
+
|
|
463
477
|
return funding_rates
|
|
464
|
-
|
|
478
|
+
|
|
465
479
|
except Exception as e:
|
|
466
480
|
raise
|
|
467
481
|
|
|
@@ -568,20 +582,21 @@ class BinanceQVUSDM(cxp.binanceusdm, BinanceQV):
|
|
|
568
582
|
async def un_watch_funding_rates(self):
|
|
569
583
|
"""Unwatch funding rates to ensure fresh connections"""
|
|
570
584
|
from qubx import logger
|
|
585
|
+
|
|
571
586
|
logger.debug("un_watch_funding_rates called - resetting connection")
|
|
572
|
-
|
|
587
|
+
|
|
573
588
|
# Try to unwatch mark prices if possible
|
|
574
|
-
if hasattr(self,
|
|
589
|
+
if hasattr(self, "un_watch_mark_prices"):
|
|
575
590
|
try:
|
|
576
591
|
await self.un_watch_mark_prices()
|
|
577
592
|
except Exception as e:
|
|
578
593
|
logger.debug(f"Error unwatching mark prices: {e}")
|
|
579
|
-
|
|
594
|
+
|
|
580
595
|
# Clear any internal caches that might exist
|
|
581
|
-
if hasattr(self,
|
|
596
|
+
if hasattr(self, "markPrices") and self.markPrices:
|
|
582
597
|
self.markPrices.clear()
|
|
583
598
|
logger.debug("Cleared mark prices cache")
|
|
584
|
-
|
|
599
|
+
|
|
585
600
|
return None
|
|
586
601
|
|
|
587
602
|
|