Qubx 0.6.66__tar.gz → 0.6.68__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.66 → qubx-0.6.68}/PKG-INFO +50 -4
- {qubx-0.6.66 → qubx-0.6.68}/README.md +48 -3
- {qubx-0.6.66 → qubx-0.6.68}/pyproject.toml +2 -1
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/data.py +19 -2
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/cli/commands.py +125 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/connection_manager.py +310 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/data.py +241 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +68 -16
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/__init__.py +29 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/base.py +93 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/factory.py +123 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/funding_rate.py +93 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/liquidation.py +91 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/ohlc.py +202 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/open_interest.py +208 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/orderbook.py +186 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/quote.py +98 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/handlers/trade.py +94 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/subscription_config.py +40 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/subscription_manager.py +331 -0
- qubx-0.6.68/src/qubx/connectors/ccxt/subscription_orchestrator.py +215 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/utils.py +88 -1
- qubx-0.6.68/src/qubx/connectors/ccxt/warmup_service.py +113 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/tardis/data.py +6 -6
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/basics.py +15 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/helpers.py +43 -24
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/initializer.py +5 -9
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/metrics.py +252 -22
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/series.pxd +22 -5
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/series.pyi +33 -3
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/series.pyx +116 -59
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/data/readers.py +68 -33
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/pandaz/ta.py +97 -22
- qubx-0.6.68/src/qubx/templates/__init__.py +5 -0
- qubx-0.6.68/src/qubx/templates/base.py +166 -0
- qubx-0.6.68/src/qubx/templates/project/accounts.toml.j2 +22 -0
- qubx-0.6.68/src/qubx/templates/project/config.yml.j2 +33 -0
- qubx-0.6.68/src/qubx/templates/project/jlive.sh.j2 +43 -0
- qubx-0.6.68/src/qubx/templates/project/jpaper.sh.j2 +6 -0
- qubx-0.6.68/src/qubx/templates/project/pyproject.toml.j2 +18 -0
- qubx-0.6.68/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +5 -0
- qubx-0.6.68/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +170 -0
- qubx-0.6.68/src/qubx/templates/project/template.yml +20 -0
- qubx-0.6.68/src/qubx/templates/simple/__init__.py.j2 +5 -0
- qubx-0.6.68/src/qubx/templates/simple/accounts.toml.j2 +22 -0
- qubx-0.6.68/src/qubx/templates/simple/config.yml.j2 +30 -0
- qubx-0.6.68/src/qubx/templates/simple/jlive.sh.j2 +43 -0
- qubx-0.6.68/src/qubx/templates/simple/jpaper.sh.j2 +6 -0
- qubx-0.6.68/src/qubx/templates/simple/strategy.py.j2 +95 -0
- qubx-0.6.68/src/qubx/templates/simple/template.yml +20 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/trackers/sizers.py +9 -2
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/charting/lookinglass.py +93 -15
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/misc.py +9 -2
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/runner/_jupyter_runner.pyt +4 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/runner/configs.py +1 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/runner/runner.py +1 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/time.py +13 -13
- qubx-0.6.66/src/qubx/connectors/ccxt/data.py +0 -823
- {qubx-0.6.66 → qubx-0.6.68}/LICENSE +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/build.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/account.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/context.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/interfaces.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/stale_data_detector.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/features/core.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/features/price.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/health/base.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.66 → qubx-0.6.68}/src/qubx/utils/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.68
|
|
4
4
|
Summary: Qubx - Quantitative Trading Framework
|
|
5
5
|
Author: Dmitry Marienko
|
|
6
6
|
Author-email: dmitry.marienko@xlydian.com
|
|
@@ -19,6 +19,7 @@ Requires-Dist: dash-bootstrap-components (>=1.6.0,<2.0.0)
|
|
|
19
19
|
Requires-Dist: gitpython (>=3.1.44,<4.0.0)
|
|
20
20
|
Requires-Dist: importlib-metadata
|
|
21
21
|
Requires-Dist: ipywidgets (>=8.1.5,<9.0.0)
|
|
22
|
+
Requires-Dist: jinja2 (>=3.1.0,<4.0.0)
|
|
22
23
|
Requires-Dist: jupyter (>=1.1.1,<2.0.0)
|
|
23
24
|
Requires-Dist: jupyter-console (>=6.6.3,<7.0.0)
|
|
24
25
|
Requires-Dist: loguru (>=0.7.2,<0.8.0)
|
|
@@ -69,6 +70,49 @@ Description-Content-Type: text/markdown
|
|
|
69
70
|
|
|
70
71
|
Qubx is a next-generation quantitative trading framework designed for efficient backtesting and live trading. Built with Python, it offers a robust environment for developing, testing, and deploying trading strategies.
|
|
71
72
|
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
### 1. Install Dependencies
|
|
76
|
+
```bash
|
|
77
|
+
poetry install
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2. Create a Strategy
|
|
81
|
+
```bash
|
|
82
|
+
# Create a simple strategy template (default)
|
|
83
|
+
poetry run qubx init
|
|
84
|
+
|
|
85
|
+
# Or specify a name and symbols
|
|
86
|
+
poetry run qubx init --name my_strategy --symbols BTCUSDT,ETHUSDT
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. Run Your Strategy
|
|
90
|
+
```bash
|
|
91
|
+
cd my_strategy
|
|
92
|
+
|
|
93
|
+
# Run in paper trading mode
|
|
94
|
+
poetry run qubx run config.yml --paper
|
|
95
|
+
|
|
96
|
+
# Or run in Jupyter mode for interactive development
|
|
97
|
+
./jpaper.sh
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Available Templates
|
|
101
|
+
```bash
|
|
102
|
+
# List available strategy templates
|
|
103
|
+
poetry run qubx init --list-templates
|
|
104
|
+
|
|
105
|
+
# Create strategy with full project structure and MACD example
|
|
106
|
+
poetry run qubx init --template project --name my_project
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Strategy Development Workflow
|
|
110
|
+
1. **Initialize**: `poetry run qubx init` - Create strategy from template
|
|
111
|
+
2. **Develop**: Edit `strategy.py` to implement your trading logic
|
|
112
|
+
3. **Test**: `poetry run qubx run config.yml --paper` - Run in paper mode
|
|
113
|
+
4. **Debug**: `./jpaper.sh` - Use Jupyter for interactive development
|
|
114
|
+
5. **Deploy**: Configure for live trading when ready
|
|
115
|
+
|
|
72
116
|
## Features
|
|
73
117
|
|
|
74
118
|
- 🚀 High-performance backtesting engine
|
|
@@ -119,11 +163,13 @@ qubx --help # Show all available commands
|
|
|
119
163
|
|
|
120
164
|
Available commands:
|
|
121
165
|
|
|
122
|
-
- `qubx
|
|
123
|
-
- `qubx ls` - List all strategies in a directory
|
|
124
|
-
- `qubx release` - Package a strategy into a zip file
|
|
166
|
+
- `qubx init` - Create a new strategy from template
|
|
125
167
|
- `qubx run` - Start a strategy with given configuration
|
|
126
168
|
- `qubx simulate` - Run strategy simulation
|
|
169
|
+
- `qubx ls` - List all strategies in a directory
|
|
170
|
+
- `qubx release` - Package a strategy into a zip file
|
|
171
|
+
- `qubx deploy` - Deploy a strategy from a zip file
|
|
172
|
+
- `qubx browse` - Browse backtest results using interactive TUI
|
|
127
173
|
|
|
128
174
|
## Development
|
|
129
175
|
|
|
@@ -11,6 +11,49 @@
|
|
|
11
11
|
|
|
12
12
|
Qubx is a next-generation quantitative trading framework designed for efficient backtesting and live trading. Built with Python, it offers a robust environment for developing, testing, and deploying trading strategies.
|
|
13
13
|
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1. Install Dependencies
|
|
17
|
+
```bash
|
|
18
|
+
poetry install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Create a Strategy
|
|
22
|
+
```bash
|
|
23
|
+
# Create a simple strategy template (default)
|
|
24
|
+
poetry run qubx init
|
|
25
|
+
|
|
26
|
+
# Or specify a name and symbols
|
|
27
|
+
poetry run qubx init --name my_strategy --symbols BTCUSDT,ETHUSDT
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 3. Run Your Strategy
|
|
31
|
+
```bash
|
|
32
|
+
cd my_strategy
|
|
33
|
+
|
|
34
|
+
# Run in paper trading mode
|
|
35
|
+
poetry run qubx run config.yml --paper
|
|
36
|
+
|
|
37
|
+
# Or run in Jupyter mode for interactive development
|
|
38
|
+
./jpaper.sh
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Available Templates
|
|
42
|
+
```bash
|
|
43
|
+
# List available strategy templates
|
|
44
|
+
poetry run qubx init --list-templates
|
|
45
|
+
|
|
46
|
+
# Create strategy with full project structure and MACD example
|
|
47
|
+
poetry run qubx init --template project --name my_project
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Strategy Development Workflow
|
|
51
|
+
1. **Initialize**: `poetry run qubx init` - Create strategy from template
|
|
52
|
+
2. **Develop**: Edit `strategy.py` to implement your trading logic
|
|
53
|
+
3. **Test**: `poetry run qubx run config.yml --paper` - Run in paper mode
|
|
54
|
+
4. **Debug**: `./jpaper.sh` - Use Jupyter for interactive development
|
|
55
|
+
5. **Deploy**: Configure for live trading when ready
|
|
56
|
+
|
|
14
57
|
## Features
|
|
15
58
|
|
|
16
59
|
- 🚀 High-performance backtesting engine
|
|
@@ -61,11 +104,13 @@ qubx --help # Show all available commands
|
|
|
61
104
|
|
|
62
105
|
Available commands:
|
|
63
106
|
|
|
64
|
-
- `qubx
|
|
65
|
-
- `qubx ls` - List all strategies in a directory
|
|
66
|
-
- `qubx release` - Package a strategy into a zip file
|
|
107
|
+
- `qubx init` - Create a new strategy from template
|
|
67
108
|
- `qubx run` - Start a strategy with given configuration
|
|
68
109
|
- `qubx simulate` - Run strategy simulation
|
|
110
|
+
- `qubx ls` - List all strategies in a directory
|
|
111
|
+
- `qubx release` - Package a strategy into a zip file
|
|
112
|
+
- `qubx deploy` - Deploy a strategy from a zip file
|
|
113
|
+
- `qubx browse` - Browse backtest results using interactive TUI
|
|
69
114
|
|
|
70
115
|
## Development
|
|
71
116
|
|
|
@@ -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.68"
|
|
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"
|
|
@@ -76,6 +76,7 @@ websockets = "15.0.1"
|
|
|
76
76
|
qubx-bitfinex-api = "^3.0.7"
|
|
77
77
|
textual = "^0.88.0"
|
|
78
78
|
rich = "^13.9.4"
|
|
79
|
+
jinja2 = "^3.1.0"
|
|
79
80
|
|
|
80
81
|
[tool.ruff.lint]
|
|
81
82
|
extend-select = [ "I",]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
+
from typing import Any, TypeVar
|
|
2
3
|
|
|
3
4
|
import pandas as pd
|
|
4
5
|
|
|
@@ -19,6 +20,17 @@ from qubx.utils.time import infer_series_frequency
|
|
|
19
20
|
from .account import SimulatedAccountProcessor
|
|
20
21
|
from .utils import SimulatedTimeProvider
|
|
21
22
|
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_first_existing(data: dict, keys: list, default: T = None) -> T:
|
|
27
|
+
data_get = data.get # Cache method lookup
|
|
28
|
+
sentinel = object()
|
|
29
|
+
for key in keys:
|
|
30
|
+
if (value := data_get(key, sentinel)) is not sentinel:
|
|
31
|
+
return value
|
|
32
|
+
return default
|
|
33
|
+
|
|
22
34
|
|
|
23
35
|
class SimulatedDataProvider(IDataProvider):
|
|
24
36
|
time_provider: SimulatedTimeProvider
|
|
@@ -164,8 +176,13 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
164
176
|
r.data["high"],
|
|
165
177
|
r.data["low"],
|
|
166
178
|
r.data["close"],
|
|
167
|
-
r.data.get("volume", 0),
|
|
168
|
-
r.data
|
|
179
|
+
volume=r.data.get("volume", 0),
|
|
180
|
+
bought_volume=_get_first_existing(r.data, ["taker_buy_volume", "bought_volume"], 0),
|
|
181
|
+
volume_quote=_get_first_existing(r.data, ["quote_volume", "volume_quote"], 0),
|
|
182
|
+
bought_volume_quote=_get_first_existing(
|
|
183
|
+
r.data, ["taker_buy_quote_volume", "bought_volume_quote"], 0
|
|
184
|
+
),
|
|
185
|
+
trade_count=_get_first_existing(r.data, ["count", "trade_count"], 0),
|
|
169
186
|
)
|
|
170
187
|
)
|
|
171
188
|
|
|
@@ -275,5 +275,130 @@ def browse(results_path: str):
|
|
|
275
275
|
run_backtest_browser(results_path)
|
|
276
276
|
|
|
277
277
|
|
|
278
|
+
@main.command()
|
|
279
|
+
@click.option(
|
|
280
|
+
"--template",
|
|
281
|
+
"-t",
|
|
282
|
+
type=str,
|
|
283
|
+
default="simple",
|
|
284
|
+
help="Built-in template to use (default: simple)",
|
|
285
|
+
show_default=True,
|
|
286
|
+
)
|
|
287
|
+
@click.option(
|
|
288
|
+
"--template-path",
|
|
289
|
+
type=click.Path(exists=True, resolve_path=True),
|
|
290
|
+
help="Path to custom template directory",
|
|
291
|
+
)
|
|
292
|
+
@click.option(
|
|
293
|
+
"--name",
|
|
294
|
+
"-n",
|
|
295
|
+
type=str,
|
|
296
|
+
default="my_strategy",
|
|
297
|
+
help="Name of the strategy to create",
|
|
298
|
+
show_default=True,
|
|
299
|
+
)
|
|
300
|
+
@click.option(
|
|
301
|
+
"--exchange",
|
|
302
|
+
"-e",
|
|
303
|
+
type=str,
|
|
304
|
+
default="BINANCE.UM",
|
|
305
|
+
help="Exchange to configure for the strategy",
|
|
306
|
+
show_default=True,
|
|
307
|
+
)
|
|
308
|
+
@click.option(
|
|
309
|
+
"--symbols",
|
|
310
|
+
"-s",
|
|
311
|
+
type=str,
|
|
312
|
+
default="BTCUSDT",
|
|
313
|
+
help="Comma-separated list of symbols to trade",
|
|
314
|
+
show_default=True,
|
|
315
|
+
)
|
|
316
|
+
@click.option(
|
|
317
|
+
"--timeframe",
|
|
318
|
+
type=str,
|
|
319
|
+
default="1h",
|
|
320
|
+
help="Timeframe for market data",
|
|
321
|
+
show_default=True,
|
|
322
|
+
)
|
|
323
|
+
@click.option(
|
|
324
|
+
"--output-dir",
|
|
325
|
+
"-o",
|
|
326
|
+
type=click.Path(resolve_path=True),
|
|
327
|
+
default=".",
|
|
328
|
+
help="Directory to create the strategy in",
|
|
329
|
+
show_default=True,
|
|
330
|
+
)
|
|
331
|
+
@click.option(
|
|
332
|
+
"--list-templates",
|
|
333
|
+
is_flag=True,
|
|
334
|
+
help="List all available built-in templates",
|
|
335
|
+
)
|
|
336
|
+
def init(
|
|
337
|
+
template: str,
|
|
338
|
+
template_path: str | None,
|
|
339
|
+
name: str,
|
|
340
|
+
exchange: str,
|
|
341
|
+
symbols: str,
|
|
342
|
+
timeframe: str,
|
|
343
|
+
output_dir: str,
|
|
344
|
+
list_templates: bool,
|
|
345
|
+
):
|
|
346
|
+
"""
|
|
347
|
+
Create a new strategy from a template.
|
|
348
|
+
|
|
349
|
+
This command generates a complete strategy project structure with:
|
|
350
|
+
- Strategy class implementing IStrategy interface
|
|
351
|
+
- Configuration file for qubx run command
|
|
352
|
+
- Package structure for proper imports
|
|
353
|
+
|
|
354
|
+
The generated strategy can be run immediately with:
|
|
355
|
+
poetry run qubx run --config config.yml --paper
|
|
356
|
+
"""
|
|
357
|
+
from qubx.templates import TemplateManager, TemplateError
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
manager = TemplateManager()
|
|
361
|
+
|
|
362
|
+
if list_templates:
|
|
363
|
+
templates = manager.list_templates()
|
|
364
|
+
if not templates:
|
|
365
|
+
click.echo("No templates available.")
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
click.echo("Available templates:")
|
|
369
|
+
for template_name, metadata in templates.items():
|
|
370
|
+
description = metadata.get("description", "No description")
|
|
371
|
+
click.echo(f" {template_name:<15} - {description}")
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
# Generate strategy
|
|
375
|
+
strategy_path = manager.generate_strategy(
|
|
376
|
+
template_name=template if not template_path else None,
|
|
377
|
+
template_path=template_path,
|
|
378
|
+
output_dir=output_dir,
|
|
379
|
+
name=name,
|
|
380
|
+
exchange=exchange,
|
|
381
|
+
symbols=symbols,
|
|
382
|
+
timeframe=timeframe,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
click.echo(f"✅ Strategy '{name}' created successfully!")
|
|
386
|
+
click.echo(f"📁 Location: {strategy_path}")
|
|
387
|
+
click.echo()
|
|
388
|
+
click.echo("To run your strategy:")
|
|
389
|
+
click.echo(f" cd {strategy_path}")
|
|
390
|
+
click.echo(" poetry run qubx run config.yml --paper")
|
|
391
|
+
click.echo()
|
|
392
|
+
click.echo("To run in Jupyter mode:")
|
|
393
|
+
click.echo(" ./jpaper.sh")
|
|
394
|
+
|
|
395
|
+
except TemplateError as e:
|
|
396
|
+
click.echo(f"❌ Template error: {e}", err=True)
|
|
397
|
+
raise click.Abort()
|
|
398
|
+
except Exception as e:
|
|
399
|
+
click.echo(f"❌ Unexpected error: {e}", err=True)
|
|
400
|
+
raise click.Abort()
|
|
401
|
+
|
|
402
|
+
|
|
278
403
|
if __name__ == "__main__":
|
|
279
404
|
main()
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection management for CCXT data provider.
|
|
3
|
+
|
|
4
|
+
This module handles WebSocket connections, retry logic, and stream lifecycle management,
|
|
5
|
+
separating connection concerns from subscription state and data handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
from asyncio.exceptions import CancelledError
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from typing import Awaitable, Callable, Dict
|
|
13
|
+
|
|
14
|
+
from ccxt import ExchangeClosedByUser, ExchangeError, ExchangeNotAvailable, NetworkError
|
|
15
|
+
from ccxt.pro import Exchange
|
|
16
|
+
from qubx import logger
|
|
17
|
+
from qubx.core.basics import CtrlChannel
|
|
18
|
+
|
|
19
|
+
from .exceptions import CcxtSymbolNotRecognized
|
|
20
|
+
from .subscription_manager import SubscriptionManager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConnectionManager:
|
|
24
|
+
"""
|
|
25
|
+
Manages WebSocket connections and stream lifecycle for CCXT data provider.
|
|
26
|
+
|
|
27
|
+
Responsibilities:
|
|
28
|
+
- Handle WebSocket connection establishment and management
|
|
29
|
+
- Implement retry logic and error handling
|
|
30
|
+
- Manage stream lifecycle (start, stop, cleanup)
|
|
31
|
+
- Coordinate with SubscriptionManager for state updates
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
exchange_id: str,
|
|
37
|
+
max_ws_retries: int = 10,
|
|
38
|
+
subscription_manager: SubscriptionManager | None = None
|
|
39
|
+
):
|
|
40
|
+
self._exchange_id = exchange_id
|
|
41
|
+
self.max_ws_retries = max_ws_retries
|
|
42
|
+
self._subscription_manager = subscription_manager
|
|
43
|
+
|
|
44
|
+
# Stream state management
|
|
45
|
+
self._is_stream_enabled: Dict[str, bool] = defaultdict(lambda: False)
|
|
46
|
+
self._stream_to_unsubscriber: Dict[str, Callable[[], Awaitable[None]]] = {}
|
|
47
|
+
|
|
48
|
+
# Connection tracking
|
|
49
|
+
self._stream_to_coro: Dict[str, concurrent.futures.Future] = {}
|
|
50
|
+
|
|
51
|
+
def set_subscription_manager(self, subscription_manager: SubscriptionManager) -> None:
|
|
52
|
+
"""Set the subscription manager for state coordination."""
|
|
53
|
+
self._subscription_manager = subscription_manager
|
|
54
|
+
|
|
55
|
+
async def listen_to_stream(
|
|
56
|
+
self,
|
|
57
|
+
subscriber: Callable[[], Awaitable[None]],
|
|
58
|
+
exchange: Exchange,
|
|
59
|
+
channel: CtrlChannel,
|
|
60
|
+
stream_name: str,
|
|
61
|
+
unsubscriber: Callable[[], Awaitable[None]] | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Listen to a WebSocket stream with error handling and retry logic.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
subscriber: Async function that handles the stream data
|
|
68
|
+
exchange: CCXT exchange instance
|
|
69
|
+
channel: Control channel for data flow
|
|
70
|
+
stream_name: Unique name for this stream
|
|
71
|
+
unsubscriber: Optional cleanup function for graceful unsubscription
|
|
72
|
+
"""
|
|
73
|
+
logger.info(f"<yellow>{self._exchange_id}</yellow> Listening to {stream_name}")
|
|
74
|
+
|
|
75
|
+
# Register unsubscriber for cleanup
|
|
76
|
+
if unsubscriber is not None:
|
|
77
|
+
self._stream_to_unsubscriber[stream_name] = unsubscriber
|
|
78
|
+
|
|
79
|
+
# Enable the stream
|
|
80
|
+
self._is_stream_enabled[stream_name] = True
|
|
81
|
+
n_retry = 0
|
|
82
|
+
connection_established = False
|
|
83
|
+
|
|
84
|
+
while channel.control.is_set() and self._is_stream_enabled[stream_name]:
|
|
85
|
+
try:
|
|
86
|
+
await subscriber()
|
|
87
|
+
n_retry = 0 # Reset retry counter on success
|
|
88
|
+
|
|
89
|
+
# Mark subscription as active on first successful data reception
|
|
90
|
+
if not connection_established and self._subscription_manager:
|
|
91
|
+
subscription_type = self._subscription_manager.find_subscription_type_by_name(stream_name)
|
|
92
|
+
if subscription_type:
|
|
93
|
+
self._subscription_manager.mark_subscription_active(subscription_type)
|
|
94
|
+
connection_established = True
|
|
95
|
+
|
|
96
|
+
# Check if stream was disabled during subscriber execution
|
|
97
|
+
if not self._is_stream_enabled[stream_name]:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
except CcxtSymbolNotRecognized:
|
|
101
|
+
# Skip unrecognized symbols but continue listening
|
|
102
|
+
continue
|
|
103
|
+
except CancelledError:
|
|
104
|
+
# Graceful cancellation
|
|
105
|
+
break
|
|
106
|
+
except ExchangeClosedByUser:
|
|
107
|
+
# Connection closed by us, stop gracefully
|
|
108
|
+
logger.info(f"<yellow>{self._exchange_id}</yellow> {stream_name} listening has been stopped")
|
|
109
|
+
break
|
|
110
|
+
except (NetworkError, ExchangeError, ExchangeNotAvailable) as e:
|
|
111
|
+
# Network/exchange errors - retry after short delay
|
|
112
|
+
logger.error(f"<yellow>{self._exchange_id}</yellow> {e.__class__.__name__} :: Error in {stream_name} : {e}")
|
|
113
|
+
await asyncio.sleep(1)
|
|
114
|
+
continue
|
|
115
|
+
except Exception as e:
|
|
116
|
+
# Unexpected errors
|
|
117
|
+
if not channel.control.is_set() or not self._is_stream_enabled[stream_name]:
|
|
118
|
+
# Channel closed or stream disabled, exit gracefully
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
logger.error(f"<yellow>{self._exchange_id}</yellow> Exception in {stream_name}: {e}")
|
|
122
|
+
logger.exception(e)
|
|
123
|
+
|
|
124
|
+
n_retry += 1
|
|
125
|
+
if n_retry >= self.max_ws_retries:
|
|
126
|
+
logger.error(
|
|
127
|
+
f"<yellow>{self._exchange_id}</yellow> Max retries reached for {stream_name}. Closing connection."
|
|
128
|
+
)
|
|
129
|
+
# Clean up exchange reference to force reconnection
|
|
130
|
+
del exchange
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
# Exponential backoff with cap at 60 seconds
|
|
134
|
+
await asyncio.sleep(min(2**n_retry, 60))
|
|
135
|
+
|
|
136
|
+
# Stream ended, cleanup
|
|
137
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> Stream {stream_name} ended")
|
|
138
|
+
|
|
139
|
+
async def stop_stream(
|
|
140
|
+
self,
|
|
141
|
+
stream_name: str,
|
|
142
|
+
future: concurrent.futures.Future | None = None,
|
|
143
|
+
is_resubscription: bool = False
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Stop a stream gracefully with proper cleanup.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
stream_name: Name of the stream to stop
|
|
150
|
+
future: Optional future representing the stream task
|
|
151
|
+
is_resubscription: True if this is stopping an old stream during resubscription
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
context = "old stream" if is_resubscription else "stream"
|
|
155
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> Stopping {context} {stream_name}")
|
|
156
|
+
|
|
157
|
+
# Disable the stream to signal it should stop
|
|
158
|
+
self._is_stream_enabled[stream_name] = False
|
|
159
|
+
|
|
160
|
+
# Wait for the stream to stop naturally
|
|
161
|
+
if future:
|
|
162
|
+
total_sleep_time = 0.0
|
|
163
|
+
while future.running() and total_sleep_time < 20.0:
|
|
164
|
+
await asyncio.sleep(1.0)
|
|
165
|
+
total_sleep_time += 1.0
|
|
166
|
+
|
|
167
|
+
if future.running():
|
|
168
|
+
logger.warning(
|
|
169
|
+
f"<yellow>{self._exchange_id}</yellow> {context.title()} {stream_name} is still running. Cancelling it."
|
|
170
|
+
)
|
|
171
|
+
future.cancel()
|
|
172
|
+
else:
|
|
173
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> {context.title()} {stream_name} has been stopped")
|
|
174
|
+
|
|
175
|
+
# Run unsubscriber if available
|
|
176
|
+
if stream_name in self._stream_to_unsubscriber:
|
|
177
|
+
logger.debug(f"<yellow>{self._exchange_id}</yellow> Unsubscribing from {stream_name}")
|
|
178
|
+
await self._stream_to_unsubscriber[stream_name]()
|
|
179
|
+
del self._stream_to_unsubscriber[stream_name]
|
|
180
|
+
|
|
181
|
+
# Clean up stream state
|
|
182
|
+
if is_resubscription:
|
|
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:
|
|
203
|
+
"""
|
|
204
|
+
Register a future for a stream for tracking and cleanup.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
stream_name: Name of the stream
|
|
208
|
+
future: Future representing the stream task
|
|
209
|
+
"""
|
|
210
|
+
self._stream_to_coro[stream_name] = future
|
|
211
|
+
|
|
212
|
+
def is_stream_enabled(self, stream_name: str) -> bool:
|
|
213
|
+
"""
|
|
214
|
+
Check if a stream is enabled.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
stream_name: Name of the stream to check
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
True if stream is enabled, False otherwise
|
|
221
|
+
"""
|
|
222
|
+
return self._is_stream_enabled.get(stream_name, False)
|
|
223
|
+
|
|
224
|
+
def get_stream_future(self, stream_name: str) -> concurrent.futures.Future | None:
|
|
225
|
+
"""
|
|
226
|
+
Get the future for a stream.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
stream_name: Name of the stream
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Future if exists, None otherwise
|
|
233
|
+
"""
|
|
234
|
+
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
|
+
|
|
245
|
+
def enable_stream(self, stream_name: str) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Enable a stream.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
stream_name: Name of the stream to enable
|
|
251
|
+
"""
|
|
252
|
+
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:
|
|
259
|
+
"""
|
|
260
|
+
Set unsubscriber function for a stream.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
stream_name: Name of the stream
|
|
264
|
+
unsubscriber: Async function to call for unsubscription
|
|
265
|
+
"""
|
|
266
|
+
self._stream_to_unsubscriber[stream_name] = unsubscriber
|
|
267
|
+
|
|
268
|
+
def get_stream_unsubscriber(self, stream_name: str) -> Callable[[], Awaitable[None]] | None:
|
|
269
|
+
"""
|
|
270
|
+
Get unsubscriber function for a stream.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
stream_name: Name of the stream
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Unsubscriber function if exists, None otherwise
|
|
277
|
+
"""
|
|
278
|
+
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:
|
|
285
|
+
"""
|
|
286
|
+
Set coroutine/future for a stream.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
stream_name: Name of the stream
|
|
290
|
+
coro: Future representing the stream task
|
|
291
|
+
"""
|
|
292
|
+
self._stream_to_coro[stream_name] = coro
|
|
293
|
+
|
|
294
|
+
def get_stream_coro(self, stream_name: str) -> concurrent.futures.Future | None:
|
|
295
|
+
"""
|
|
296
|
+
Get coroutine/future for a stream.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
stream_name: Name of the stream
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Future if exists, None otherwise
|
|
303
|
+
"""
|
|
304
|
+
return self._stream_to_coro.get(stream_name)
|
|
305
|
+
|
|
306
|
+
def cleanup_all_streams(self) -> None:
|
|
307
|
+
"""Clean up all stream state (for shutdown)."""
|
|
308
|
+
self._is_stream_enabled.clear()
|
|
309
|
+
self._stream_to_unsubscriber.clear()
|
|
310
|
+
self._stream_to_coro.clear()
|