Qubx 0.6.90__tar.gz → 0.6.93__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.90 → qubx-0.6.93}/PKG-INFO +5 -2
- {qubx-0.6.90 → qubx-0.6.93}/pyproject.toml +9 -2
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/account.py +1 -1
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/management.py +2 -1
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/cli/commands.py +128 -1
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/account.py +3 -2
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/broker.py +19 -12
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/connection_manager.py +14 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchanges/__init__.py +5 -2
- qubx-0.6.93/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +7 -0
- qubx-0.6.93/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +75 -0
- qubx-0.6.93/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +306 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +83 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/factory.py +15 -20
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/base.py +2 -4
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/factory.py +4 -5
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/utils.py +8 -2
- qubx-0.6.93/src/qubx/connectors/xlighter/__init__.py +83 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/account.py +531 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/broker.py +857 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/client.py +378 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/constants.py +126 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/data.py +620 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/extensions.py +248 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/factory.py +330 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/handlers/__init__.py +13 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/handlers/base.py +104 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/handlers/orderbook.py +207 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/handlers/quote.py +158 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/handlers/trades.py +146 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/instruments.py +253 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/orderbook_maintainer.py +314 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/parsers.py +694 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/reader.py +489 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/utils.py +281 -0
- qubx-0.6.93/src/qubx/connectors/xlighter/websocket.py +359 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/account.py +57 -5
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/basics.py +62 -2
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/context.py +2 -2
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/helpers.py +60 -1
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/interfaces.py +201 -2
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/mixins/processing.py +6 -1
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/mixins/trading.py +75 -8
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/series.pxd +8 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/series.pyi +46 -2
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/series.pyx +134 -4
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/data/__init__.py +10 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/data/composite.py +87 -60
- qubx-0.6.93/src/qubx/data/containers.py +234 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/data/readers.py +3 -0
- qubx-0.6.93/src/qubx/data/registry.py +242 -0
- qubx-0.6.93/src/qubx/data/storage.py +74 -0
- qubx-0.6.93/src/qubx/data/storages/csv.py +273 -0
- qubx-0.6.93/src/qubx/data/storages/questdb.py +566 -0
- qubx-0.6.93/src/qubx/data/storages/utils.py +115 -0
- qubx-0.6.93/src/qubx/data/transformers.py +526 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/redis_streams.py +3 -3
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/pandaz/utils.py +1 -1
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/questdb.py +6 -7
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/runner/accounts.py +30 -1
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/runner/configs.py +2 -1
- qubx-0.6.93/src/qubx/utils/runner/kernel_service.py +195 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/runner/runner.py +107 -7
- qubx-0.6.93/src/qubx/utils/runner/textual/__init__.py +177 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/app.py +392 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/handlers.py +90 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/init_code.py +304 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/kernel.py +269 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/styles.tcss +134 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/widgets/__init__.py +10 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/widgets/command_input.py +105 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/widgets/debug_log.py +97 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/widgets/orders_table.py +61 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/widgets/positions_table.py +91 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/widgets/quotes_table.py +90 -0
- qubx-0.6.93/src/qubx/utils/runner/textual/widgets/repl_output.py +97 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/time.py +7 -0
- qubx-0.6.93/src/qubx/utils/websocket_manager.py +442 -0
- qubx-0.6.90/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -6
- qubx-0.6.90/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -77
- qubx-0.6.90/src/qubx/data/registry.py +0 -124
- {qubx-0.6.90 → qubx-0.6.93}/LICENSE +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/README.md +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/build.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/metrics.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/stale_data_detector.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/features/core.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/features/price.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/health/base.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/base.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.90 → qubx-0.6.93}/src/qubx/utils/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.93
|
|
4
4
|
Summary: Qubx - Quantitative Trading Framework
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Dmitry Marienko
|
|
@@ -24,6 +24,7 @@ Requires-Dist: ipywidgets (>=8.1.5,<9.0.0)
|
|
|
24
24
|
Requires-Dist: jinja2 (>=3.1.0,<4.0.0)
|
|
25
25
|
Requires-Dist: jupyter (>=1.1.1,<2.0.0)
|
|
26
26
|
Requires-Dist: jupyter-console (>=6.6.3,<7.0.0)
|
|
27
|
+
Requires-Dist: lighter-sdk (>=0.1.4,<0.2.0)
|
|
27
28
|
Requires-Dist: loguru (>=0.7.2,<0.8.0)
|
|
28
29
|
Requires-Dist: matplotlib (>=3.8.4,<4.0.0)
|
|
29
30
|
Requires-Dist: msgspec (>=0.18.6,<0.19.0)
|
|
@@ -52,7 +53,9 @@ Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
|
|
|
52
53
|
Requires-Dist: stackprinter (>=0.2.10,<0.3.0)
|
|
53
54
|
Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
|
|
54
55
|
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
55
|
-
Requires-Dist: textual (>=0.
|
|
56
|
+
Requires-Dist: textual-autocomplete (>=4.0.0,<5.0.0)
|
|
57
|
+
Requires-Dist: textual-serve (>=1.0.0,<2.0.0)
|
|
58
|
+
Requires-Dist: textual[syntax] (>=6.0.0,<7.0.0)
|
|
56
59
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
57
60
|
Requires-Dist: tqdm
|
|
58
61
|
Requires-Dist: websockets (==15.0.1)
|
|
@@ -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.93"
|
|
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"
|
|
@@ -74,9 +74,11 @@ orjson = "^3.10.15"
|
|
|
74
74
|
aiohttp = "~3.10.11"
|
|
75
75
|
websockets = "15.0.1"
|
|
76
76
|
qubx-bitfinex-api = "^3.0.7"
|
|
77
|
-
textual = "^0.
|
|
77
|
+
textual-autocomplete = "^4.0.0"
|
|
78
|
+
textual-serve = "^1.0.0"
|
|
78
79
|
rich = "^13.9.4"
|
|
79
80
|
jinja2 = "^3.1.0"
|
|
81
|
+
lighter-sdk = "^0.1.4"
|
|
80
82
|
|
|
81
83
|
[tool.ruff.lint]
|
|
82
84
|
extend-select = [ "I",]
|
|
@@ -90,6 +92,10 @@ markers = [ "integration: mark test as requiring external services like Redis",
|
|
|
90
92
|
addopts = "--disable-warnings"
|
|
91
93
|
filterwarnings = [ "ignore:.*Jupyter is migrating.*:DeprecationWarning", "ignore:coroutine.*AsyncMockMixin._execute_mock_call.*was never awaited:RuntimeWarning",]
|
|
92
94
|
|
|
95
|
+
[tool.poetry.dependencies.textual]
|
|
96
|
+
extras = [ "syntax",]
|
|
97
|
+
version = "^6.0.0"
|
|
98
|
+
|
|
93
99
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
94
100
|
"*.ipynb" = [ "F405", "F401", "E701", "E402", "F403", "E401", "E702", "I001",]
|
|
95
101
|
|
|
@@ -120,6 +126,7 @@ pytest-mock = "^3.12.0"
|
|
|
120
126
|
pytest-lazy-fixture = "^0.6.3"
|
|
121
127
|
pytest-cov = "^4.1.0"
|
|
122
128
|
mongomock = "^4.3.0"
|
|
129
|
+
pytest-textual-snapshot = "^1.1.0"
|
|
123
130
|
|
|
124
131
|
[tool.poetry.group.k8.dependencies]
|
|
125
132
|
prometheus-client = "^0.21.1"
|
|
@@ -42,7 +42,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
42
42
|
_pos = self.get_position(instrument)
|
|
43
43
|
_pos.reset_by_position(position)
|
|
44
44
|
|
|
45
|
-
def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
|
|
45
|
+
def get_orders(self, instrument: Instrument | None = None, exchange: str | None = None) -> dict[str, Order]:
|
|
46
46
|
return self._exchange.get_open_orders(instrument)
|
|
47
47
|
|
|
48
48
|
def get_position(self, instrument: Instrument) -> Position:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import zipfile
|
|
3
3
|
from collections import defaultdict
|
|
4
|
+
from os.path import expanduser
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
@@ -39,7 +40,7 @@ class BacktestsResultsManager:
|
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
42
|
def __init__(self, path: str):
|
|
42
|
-
self.path = path
|
|
43
|
+
self.path = expanduser(path)
|
|
43
44
|
self.reload()
|
|
44
45
|
|
|
45
46
|
def reload(self) -> "BacktestsResultsManager":
|
|
@@ -69,11 +69,32 @@ def main(debug: bool, debug_port: int, log_level: str):
|
|
|
69
69
|
@click.option(
|
|
70
70
|
"--jupyter", "-j", is_flag=True, default=False, help="Run strategy in jupyter console.", show_default=True
|
|
71
71
|
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"--textual", "-t", is_flag=True, default=False, help="Run strategy in textual TUI.", show_default=True
|
|
74
|
+
)
|
|
75
|
+
@click.option(
|
|
76
|
+
"--textual-dev", is_flag=True, default=False, help="Enable Textual dev mode (use with 'textual console').", show_default=True
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--textual-web", is_flag=True, default=False, help="Serve Textual app in web browser.", show_default=True
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--textual-port", type=int, default=None, help="Port for Textual (web server: 8000, devtools: 8081).", show_default=False
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--textual-host", type=str, default="0.0.0.0", help="Host for Textual web server.", show_default=True
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--kernel-only", is_flag=True, default=False, help="Start kernel without UI (returns connection file).", show_default=True
|
|
89
|
+
)
|
|
90
|
+
@click.option(
|
|
91
|
+
"--connect", type=Path, default=None, help="Connect to existing kernel via connection file.", show_default=False
|
|
92
|
+
)
|
|
72
93
|
@click.option(
|
|
73
94
|
"--restore", "-r", is_flag=True, default=False, help="Restore strategy state from previous run.", show_default=True
|
|
74
95
|
)
|
|
75
96
|
@click.option("--no-color", is_flag=True, default=False, help="Disable colored logging output.", show_default=True)
|
|
76
|
-
def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool, restore: bool, no_color: bool):
|
|
97
|
+
def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool, textual: bool, textual_dev: bool, textual_web: bool, textual_port: int | None, textual_host: str, kernel_only: bool, connect: Path | None, restore: bool, no_color: bool):
|
|
77
98
|
"""
|
|
78
99
|
Starts the strategy with the given configuration file. If paper mode is enabled, account is not required.
|
|
79
100
|
|
|
@@ -84,12 +105,54 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
|
|
|
84
105
|
"""
|
|
85
106
|
from qubx.utils.misc import add_project_to_system_path, logo
|
|
86
107
|
from qubx.utils.runner.runner import run_strategy_yaml, run_strategy_yaml_in_jupyter
|
|
108
|
+
from qubx.utils.runner.textual import run_strategy_yaml_in_textual
|
|
109
|
+
|
|
110
|
+
# Ensure jupyter and textual are mutually exclusive
|
|
111
|
+
if jupyter and textual:
|
|
112
|
+
click.echo("Error: --jupyter and --textual cannot be used together.", err=True)
|
|
113
|
+
raise click.Abort()
|
|
114
|
+
|
|
115
|
+
# Handle --kernel-only mode
|
|
116
|
+
if kernel_only:
|
|
117
|
+
import asyncio
|
|
118
|
+
|
|
119
|
+
from qubx.utils.runner.kernel_service import KernelService
|
|
120
|
+
|
|
121
|
+
add_project_to_system_path()
|
|
122
|
+
add_project_to_system_path(str(config_file.parent.parent))
|
|
123
|
+
add_project_to_system_path(str(config_file.parent))
|
|
124
|
+
|
|
125
|
+
click.echo("Starting persistent kernel...")
|
|
126
|
+
connection_file = asyncio.run(KernelService.start(config_file, account_file, paper, restore))
|
|
127
|
+
click.echo(click.style("✓ Kernel started successfully!", fg="green", bold=True))
|
|
128
|
+
click.echo(click.style(f"Connection file: {connection_file}", fg="cyan"))
|
|
129
|
+
click.echo()
|
|
130
|
+
click.echo("To connect a UI to this kernel:")
|
|
131
|
+
click.echo(f" qubx run --textual --connect {connection_file}")
|
|
132
|
+
click.echo()
|
|
133
|
+
click.echo("To stop this kernel:")
|
|
134
|
+
click.echo(f" qubx kernel stop {connection_file}")
|
|
135
|
+
click.echo()
|
|
136
|
+
click.echo("Press Ctrl+C to stop the kernel and exit...")
|
|
137
|
+
|
|
138
|
+
# Keep the process alive until interrupted
|
|
139
|
+
try:
|
|
140
|
+
import signal
|
|
141
|
+
signal.pause()
|
|
142
|
+
except KeyboardInterrupt:
|
|
143
|
+
click.echo("\nShutting down kernel...")
|
|
144
|
+
asyncio.run(KernelService.stop(connection_file))
|
|
145
|
+
click.echo("Kernel stopped.")
|
|
146
|
+
return
|
|
87
147
|
|
|
88
148
|
add_project_to_system_path()
|
|
89
149
|
add_project_to_system_path(str(config_file.parent.parent))
|
|
90
150
|
add_project_to_system_path(str(config_file.parent))
|
|
151
|
+
|
|
91
152
|
if jupyter:
|
|
92
153
|
run_strategy_yaml_in_jupyter(config_file, account_file, paper, restore)
|
|
154
|
+
elif textual:
|
|
155
|
+
run_strategy_yaml_in_textual(config_file, account_file, paper, restore, textual_dev, textual_web, textual_port, textual_host, connect)
|
|
93
156
|
else:
|
|
94
157
|
logo()
|
|
95
158
|
run_strategy_yaml(config_file, account_file, paper=paper, restore=restore, blocking=True, no_color=no_color)
|
|
@@ -449,5 +512,69 @@ def init(
|
|
|
449
512
|
raise click.Abort()
|
|
450
513
|
|
|
451
514
|
|
|
515
|
+
@main.group()
|
|
516
|
+
def kernel():
|
|
517
|
+
"""
|
|
518
|
+
Manage persistent Jupyter kernels for strategy execution.
|
|
519
|
+
|
|
520
|
+
Kernels can be started independently of the UI, allowing multiple
|
|
521
|
+
UI instances to connect to the same running strategy.
|
|
522
|
+
"""
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@kernel.command("list")
|
|
527
|
+
def kernel_list():
|
|
528
|
+
"""
|
|
529
|
+
List all active kernel sessions.
|
|
530
|
+
|
|
531
|
+
Shows connection files and associated strategy configurations
|
|
532
|
+
for all currently running kernels.
|
|
533
|
+
"""
|
|
534
|
+
from qubx.utils.runner.kernel_service import KernelService
|
|
535
|
+
|
|
536
|
+
active = KernelService.list_active()
|
|
537
|
+
|
|
538
|
+
if not active:
|
|
539
|
+
click.echo("No active kernels found.")
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
import datetime
|
|
543
|
+
|
|
544
|
+
click.echo(click.style("Active Kernels:", fg="cyan", bold=True))
|
|
545
|
+
click.echo()
|
|
546
|
+
for i, kernel_info in enumerate(active, 1):
|
|
547
|
+
# Format timestamp
|
|
548
|
+
ts = datetime.datetime.fromtimestamp(kernel_info["timestamp"])
|
|
549
|
+
time_str = ts.strftime("%Y-%m-%d %H:%M:%S")
|
|
550
|
+
|
|
551
|
+
click.echo(f"{i}. {click.style('Strategy:', fg='yellow')} {kernel_info['strategy_name']}")
|
|
552
|
+
click.echo(f" {click.style('Started:', fg='yellow')} {time_str}")
|
|
553
|
+
click.echo(f" {click.style('Connection:', fg='yellow')} {kernel_info['connection_file']}")
|
|
554
|
+
click.echo()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@kernel.command("stop")
|
|
558
|
+
@click.argument("connection-file", type=Path, required=True)
|
|
559
|
+
def kernel_stop(connection_file: Path):
|
|
560
|
+
"""
|
|
561
|
+
Stop a running kernel by its connection file.
|
|
562
|
+
|
|
563
|
+
This will gracefully shutdown the kernel and clean up
|
|
564
|
+
the connection file.
|
|
565
|
+
"""
|
|
566
|
+
import asyncio
|
|
567
|
+
|
|
568
|
+
from qubx.utils.runner.kernel_service import KernelService
|
|
569
|
+
|
|
570
|
+
if not connection_file.exists():
|
|
571
|
+
click.echo(click.style(f"✗ Connection file not found: {connection_file}", fg="red"))
|
|
572
|
+
raise click.Abort()
|
|
573
|
+
|
|
574
|
+
click.echo(f"Stopping kernel: {connection_file}")
|
|
575
|
+
asyncio.run(KernelService.stop(str(connection_file)))
|
|
576
|
+
click.echo(click.style("✓ Kernel stopped successfully", fg="green"))
|
|
577
|
+
|
|
578
|
+
|
|
452
579
|
if __name__ == "__main__":
|
|
453
580
|
main()
|
|
@@ -337,7 +337,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
337
337
|
current_pos.change_position_by(timestamp, quantity_diff, _current_price)
|
|
338
338
|
|
|
339
339
|
def _get_start_time_in_ms(self, days_before: int) -> int:
|
|
340
|
-
return (self.time_provider.time() - days_before * pd.Timedelta("1d")).asm8.item() // 1000000
|
|
340
|
+
return (self.time_provider.time() - days_before * pd.Timedelta("1d")).asm8.item() // 1000000 # type: ignore
|
|
341
341
|
|
|
342
342
|
def _is_our_order(self, order: Order) -> bool:
|
|
343
343
|
if order.client_id is None:
|
|
@@ -365,7 +365,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
365
365
|
_fetch_instruments: list[Instrument] = []
|
|
366
366
|
for instr in instruments:
|
|
367
367
|
_dt, _ = self._instrument_to_last_price.get(instr, (None, None))
|
|
368
|
-
if _dt is None or pd.Timedelta(_current_time - _dt) > pd.Timedelta(self.balance_interval):
|
|
368
|
+
if _dt is None or pd.Timedelta(_current_time - _dt) > pd.Timedelta(self.balance_interval): # type: ignore
|
|
369
369
|
_fetch_instruments.append(instr)
|
|
370
370
|
|
|
371
371
|
_symbol_to_instrument = {instr.symbol: instr for instr in instruments}
|
|
@@ -506,6 +506,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
506
506
|
deals: list[Deal] = [ccxt_convert_deal_info(o) for o in deals_data]
|
|
507
507
|
return sorted(deals, key=lambda x: x.time) if deals else []
|
|
508
508
|
|
|
509
|
+
# TODO: this should take the exchange manager instead of cxp.Exchange
|
|
509
510
|
async def _listen_to_stream(
|
|
510
511
|
self,
|
|
511
512
|
subscriber: Callable[[], Awaitable[None]],
|
|
@@ -42,6 +42,7 @@ class CcxtBroker(IBroker):
|
|
|
42
42
|
max_cancel_retries: int = 10,
|
|
43
43
|
enable_create_order_ws: bool = False,
|
|
44
44
|
enable_cancel_order_ws: bool = False,
|
|
45
|
+
enable_edit_order_ws: bool = False,
|
|
45
46
|
):
|
|
46
47
|
self._exchange_manager = exchange_manager
|
|
47
48
|
self.ccxt_exchange_id = str(self._exchange_manager.exchange.name)
|
|
@@ -54,6 +55,7 @@ class CcxtBroker(IBroker):
|
|
|
54
55
|
self.max_cancel_retries = max_cancel_retries
|
|
55
56
|
self.enable_create_order_ws = enable_create_order_ws
|
|
56
57
|
self.enable_cancel_order_ws = enable_cancel_order_ws
|
|
58
|
+
self.enable_edit_order_ws = enable_edit_order_ws
|
|
57
59
|
|
|
58
60
|
@property
|
|
59
61
|
def _loop(self) -> AsyncThreadLoop:
|
|
@@ -476,8 +478,6 @@ class CcxtBroker(IBroker):
|
|
|
476
478
|
BadRequest: If the order is not a limit order
|
|
477
479
|
ExchangeError: If the exchange operation fails
|
|
478
480
|
"""
|
|
479
|
-
logger.debug(f"Updating order {order_id} with price={price}, amount={amount}")
|
|
480
|
-
|
|
481
481
|
active_orders = self.account.get_orders()
|
|
482
482
|
if order_id not in active_orders:
|
|
483
483
|
raise OrderNotFound(f"Order {order_id} not found in active orders")
|
|
@@ -496,7 +496,7 @@ class CcxtBroker(IBroker):
|
|
|
496
496
|
|
|
497
497
|
logger.debug(
|
|
498
498
|
f"[<g>{instrument.symbol}</g>] :: Updating order {order_id}: "
|
|
499
|
-
f"{amount} @ {price} (was: {existing_order.quantity} @ {existing_order.price})"
|
|
499
|
+
f"{amount} @ {price} (was: {existing_order.quantity} @ {existing_order.price} ({existing_order.time_in_force}))"
|
|
500
500
|
)
|
|
501
501
|
|
|
502
502
|
try:
|
|
@@ -511,8 +511,6 @@ class CcxtBroker(IBroker):
|
|
|
511
511
|
|
|
512
512
|
def _update_order_direct(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
|
|
513
513
|
"""Update order using exchange's native edit functionality."""
|
|
514
|
-
logger.debug(f"Using direct order update for {order_id}")
|
|
515
|
-
|
|
516
514
|
future_result = self._loop.submit(self._edit_order_async(order_id, existing_order, price, amount))
|
|
517
515
|
updated_order, error = future_result.result()
|
|
518
516
|
|
|
@@ -521,15 +519,13 @@ class CcxtBroker(IBroker):
|
|
|
521
519
|
|
|
522
520
|
if updated_order is not None:
|
|
523
521
|
self.account.process_order(updated_order)
|
|
524
|
-
logger.debug(f"
|
|
522
|
+
logger.debug(f"[<g>{existing_order.instrument.symbol}</g>] :: Successfully updated order {order_id}")
|
|
525
523
|
return updated_order
|
|
526
524
|
else:
|
|
527
525
|
raise Exception("Order update returned None without error")
|
|
528
526
|
|
|
529
527
|
def _update_order_fallback(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
|
|
530
528
|
"""Update order using cancel+recreate strategy for exchanges without editOrder support."""
|
|
531
|
-
logger.debug(f"Using fallback (cancel+recreate) strategy for order {order_id}")
|
|
532
|
-
|
|
533
529
|
success = self.cancel_order(order_id)
|
|
534
530
|
if not success:
|
|
535
531
|
raise Exception(f"Failed to cancel order {order_id} during update")
|
|
@@ -544,7 +540,9 @@ class CcxtBroker(IBroker):
|
|
|
544
540
|
time_in_force=existing_order.time_in_force or "gtc",
|
|
545
541
|
)
|
|
546
542
|
|
|
547
|
-
logger.debug(
|
|
543
|
+
logger.debug(
|
|
544
|
+
f"[<g>{existing_order.instrument.symbol}</g>] :: Successfully updated order {order_id} -> new order {updated_order.id}"
|
|
545
|
+
)
|
|
548
546
|
return updated_order
|
|
549
547
|
|
|
550
548
|
async def _edit_order_async(
|
|
@@ -555,9 +553,18 @@ class CcxtBroker(IBroker):
|
|
|
555
553
|
ccxt_symbol = instrument_to_ccxt_symbol(existing_order.instrument)
|
|
556
554
|
ccxt_side = "buy" if existing_order.side == "BUY" else "sell"
|
|
557
555
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
556
|
+
# CCXT requires positive amount (side determines direction)
|
|
557
|
+
abs_amount = abs(amount)
|
|
558
|
+
|
|
559
|
+
# Use WebSocket if enabled, otherwise use REST API
|
|
560
|
+
if self.enable_edit_order_ws:
|
|
561
|
+
result = await self._exchange_manager.exchange.edit_order_ws(
|
|
562
|
+
id=order_id, symbol=ccxt_symbol, type="limit", side=ccxt_side, amount=abs_amount, price=price, params={}
|
|
563
|
+
)
|
|
564
|
+
else:
|
|
565
|
+
result = await self._exchange_manager.exchange.edit_order(
|
|
566
|
+
id=order_id, symbol=ccxt_symbol, type="limit", side=ccxt_side, amount=abs_amount, price=price, params={}
|
|
567
|
+
)
|
|
561
568
|
|
|
562
569
|
# Convert the result back to our Order format
|
|
563
570
|
updated_order = ccxt_convert_order_info(existing_order.instrument, result)
|
|
@@ -13,6 +13,7 @@ from collections import defaultdict
|
|
|
13
13
|
from typing import Awaitable, Callable
|
|
14
14
|
|
|
15
15
|
from ccxt import ExchangeClosedByUser, ExchangeError, ExchangeNotAvailable, NetworkError
|
|
16
|
+
from ccxt.async_support.base.ws.client import Client as _WsClient
|
|
16
17
|
from ccxt.pro import Exchange
|
|
17
18
|
from qubx import logger
|
|
18
19
|
from qubx.core.basics import CtrlChannel
|
|
@@ -23,6 +24,19 @@ from .exchange_manager import ExchangeManager
|
|
|
23
24
|
from .subscription_manager import SubscriptionManager
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def _safe_buffer(self):
|
|
28
|
+
conn = getattr(self.connection, "_conn", None)
|
|
29
|
+
if not conn or not getattr(conn, "protocol", None):
|
|
30
|
+
return b""
|
|
31
|
+
payload = getattr(conn.protocol, "_payload", None)
|
|
32
|
+
buf = getattr(payload, "_buffer", None)
|
|
33
|
+
return buf if buf is not None else b""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# SAFETY PATCH: make ccxt WS buffer access resilient to closed connections
|
|
37
|
+
_WsClient.buffer = property(_safe_buffer) # type: ignore
|
|
38
|
+
|
|
39
|
+
|
|
26
40
|
class ConnectionManager:
|
|
27
41
|
"""
|
|
28
42
|
Manages WebSocket connections and stream lifecycle for CCXT data provider.
|
|
@@ -12,6 +12,7 @@ from .binance.broker import BinanceCcxtBroker
|
|
|
12
12
|
from .binance.exchange import BINANCE_UM_MM, BinancePortfolioMargin, BinanceQV, BinanceQVUSDM
|
|
13
13
|
from .bitfinex.bitfinex import BitfinexF
|
|
14
14
|
from .bitfinex.bitfinex_account import BitfinexAccountProcessor
|
|
15
|
+
from .hyperliquid.account import HyperliquidAccountProcessor
|
|
15
16
|
from .hyperliquid.broker import HyperliquidCcxtBroker
|
|
16
17
|
from .hyperliquid.hyperliquid import Hyperliquid, HyperliquidF
|
|
17
18
|
from .kraken.kraken import CustomKrakenFutures
|
|
@@ -45,12 +46,14 @@ CUSTOM_BROKERS = {
|
|
|
45
46
|
"binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
46
47
|
"binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
|
|
47
48
|
"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),
|
|
49
|
+
"hyperliquid": partial(HyperliquidCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False, enable_edit_order_ws=True),
|
|
50
|
+
"hyperliquid.f": partial(HyperliquidCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False, enable_edit_order_ws=True),
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
CUSTOM_ACCOUNTS = {
|
|
53
54
|
"bitfinex.f": BitfinexAccountProcessor,
|
|
55
|
+
"hyperliquid": HyperliquidAccountProcessor,
|
|
56
|
+
"hyperliquid.f": HyperliquidAccountProcessor,
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
READER_CAPABILITIES = {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Hyperliquid exchange overrides
|
|
2
|
+
|
|
3
|
+
from .account import HyperliquidAccountProcessor
|
|
4
|
+
from .broker import HyperliquidCcxtBroker
|
|
5
|
+
from .hyperliquid import Hyperliquid, HyperliquidF
|
|
6
|
+
|
|
7
|
+
__all__ = ["HyperliquidAccountProcessor", "HyperliquidCcxtBroker", "Hyperliquid", "HyperliquidF"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from qubx import logger
|
|
4
|
+
from qubx.connectors.ccxt.account import CcxtAccountProcessor
|
|
5
|
+
from qubx.connectors.ccxt.utils import (
|
|
6
|
+
ccxt_convert_order_info,
|
|
7
|
+
ccxt_extract_deals_from_exec,
|
|
8
|
+
ccxt_find_instrument,
|
|
9
|
+
)
|
|
10
|
+
from qubx.core.basics import CtrlChannel, Instrument
|
|
11
|
+
from qubx.utils.time import now_utc, timestamp_to_ms
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HyperliquidAccountProcessor(CcxtAccountProcessor):
|
|
15
|
+
"""
|
|
16
|
+
Hyperliquid-specific account processor.
|
|
17
|
+
|
|
18
|
+
Hyperliquid uses separate WebSocket channels:
|
|
19
|
+
- orderUpdates: for order status (watch_orders)
|
|
20
|
+
- userFills: for trade/fill updates (watch_my_trades)
|
|
21
|
+
|
|
22
|
+
Unlike Binance, Hyperliquid's watch_orders does NOT include trades,
|
|
23
|
+
so we must subscribe to both channels separately.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
async def _subscribe_instruments(self, instruments: list[Instrument]):
|
|
27
|
+
"""Override to filter out instruments from other exchanges (e.g., spot vs futures)."""
|
|
28
|
+
# Filter instruments to only those belonging to this exchange
|
|
29
|
+
exchange_name = self.exchange_manager.exchange.name
|
|
30
|
+
matching_instruments = [instr for instr in instruments if instr.exchange == exchange_name]
|
|
31
|
+
|
|
32
|
+
if len(matching_instruments) < len(instruments):
|
|
33
|
+
skipped = [instr for instr in instruments if instr.exchange != exchange_name]
|
|
34
|
+
logger.debug(f"Skipping subscription for {len(skipped)} instruments from other exchanges: {skipped}")
|
|
35
|
+
|
|
36
|
+
# Call parent with filtered instruments
|
|
37
|
+
if matching_instruments:
|
|
38
|
+
await super()._subscribe_instruments(matching_instruments)
|
|
39
|
+
|
|
40
|
+
async def _subscribe_executions(self, name: str, channel: CtrlChannel):
|
|
41
|
+
logger.info("<yellow>[Hyperliquid]</yellow> Subscribing to executions")
|
|
42
|
+
_symbol_to_instrument = {}
|
|
43
|
+
|
|
44
|
+
async def _watch_orders():
|
|
45
|
+
orders = await self.exchange_manager.exchange.watch_orders()
|
|
46
|
+
for order in orders:
|
|
47
|
+
instrument = ccxt_find_instrument(
|
|
48
|
+
order["symbol"], self.exchange_manager.exchange, _symbol_to_instrument
|
|
49
|
+
)
|
|
50
|
+
order = ccxt_convert_order_info(instrument, order)
|
|
51
|
+
channel.send((instrument, "order", order, False))
|
|
52
|
+
|
|
53
|
+
async def _watch_my_trades():
|
|
54
|
+
trades = await self.exchange_manager.exchange.watch_my_trades(since=timestamp_to_ms(now_utc()))
|
|
55
|
+
for trade in trades: # type: ignore
|
|
56
|
+
instrument = ccxt_find_instrument(
|
|
57
|
+
trade["symbol"], self.exchange_manager.exchange, _symbol_to_instrument
|
|
58
|
+
)
|
|
59
|
+
deals = ccxt_extract_deals_from_exec({"trades": [trade]})
|
|
60
|
+
channel.send((instrument, "deals", deals, False))
|
|
61
|
+
|
|
62
|
+
await asyncio.gather(
|
|
63
|
+
self._listen_to_stream(
|
|
64
|
+
subscriber=_watch_orders,
|
|
65
|
+
exchange=self.exchange_manager.exchange,
|
|
66
|
+
channel=channel,
|
|
67
|
+
name=f"{name}_orders",
|
|
68
|
+
),
|
|
69
|
+
self._listen_to_stream(
|
|
70
|
+
subscriber=_watch_my_trades,
|
|
71
|
+
exchange=self.exchange_manager.exchange,
|
|
72
|
+
channel=channel,
|
|
73
|
+
name=f"{name}_trades",
|
|
74
|
+
),
|
|
75
|
+
)
|