Qubx 0.6.78__tar.gz → 0.6.85__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.78 → qubx-0.6.85}/PKG-INFO +4 -2
- {qubx-0.6.78 → qubx-0.6.85}/pyproject.toml +1 -1
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/management.py +12 -1
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/runner.py +9 -3
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/account.py +23 -4
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchange_manager.py +66 -63
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +17 -9
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/orderbook.py +8 -6
- qubx-0.6.85/src/qubx/connectors/ccxt/handlers/trade.py +207 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/account.py +2 -1
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/basics.py +4 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/helpers.py +9 -3
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/metrics.py +101 -2
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/subscription.py +7 -1
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/series.pxd +3 -2
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/series.pyi +3 -2
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/series.pyx +30 -5
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/helpers.py +34 -12
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/ta/indicators.pxd +5 -1
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/ta/indicators.pyi +7 -2
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/ta/indicators.pyx +136 -18
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/riskctrl.py +83 -3
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/charting/lookinglass.py +42 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/misc.py +23 -6
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/configs.py +1 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/runner.py +27 -2
- qubx-0.6.78/src/qubx/connectors/ccxt/handlers/trade.py +0 -111
- {qubx-0.6.78 → qubx-0.6.85}/LICENSE +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/README.md +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/build.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/context.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/interfaces.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/stale_data_detector.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/readers.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/indicator.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/core.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/price.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/health/base.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/base.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/version.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.85
|
|
4
4
|
Summary: Qubx - Quantitative Trading Framework
|
|
5
|
+
License-File: LICENSE
|
|
5
6
|
Author: Dmitry Marienko
|
|
6
7
|
Author-email: dmitry.marienko@xlydian.com
|
|
7
8
|
Requires-Python: >=3.10,<4.0
|
|
@@ -10,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
15
|
Requires-Dist: aiohttp (>=3.10.11,<3.11.0)
|
|
14
16
|
Requires-Dist: ccxt (>=4.2.68,<5.0.0)
|
|
15
17
|
Requires-Dist: croniter (>=2.0.5,<3.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.85"
|
|
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"
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
8
|
import yaml
|
|
9
|
+
from tqdm.auto import tqdm
|
|
9
10
|
|
|
10
11
|
from qubx.core.metrics import TradingSessionResult
|
|
11
12
|
from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
|
|
@@ -420,8 +421,10 @@ class BacktestsResultsManager:
|
|
|
420
421
|
Returns:
|
|
421
422
|
plotly.graph_objects.Figure: The plot of the variation.
|
|
422
423
|
"""
|
|
423
|
-
import plotly.express as px
|
|
424
424
|
from itertools import cycle
|
|
425
|
+
|
|
426
|
+
import plotly.express as px
|
|
427
|
+
|
|
425
428
|
from qubx.utils.misc import string_shortener
|
|
426
429
|
|
|
427
430
|
_vars = self.variations.get(variation_idx)
|
|
@@ -507,3 +510,11 @@ class BacktestsResultsManager:
|
|
|
507
510
|
)
|
|
508
511
|
)
|
|
509
512
|
return figure
|
|
513
|
+
|
|
514
|
+
def export_backtests_to_markdown(self, path: str, tags: tuple[str] | None = None):
|
|
515
|
+
"""
|
|
516
|
+
Export backtests to markdown format
|
|
517
|
+
"""
|
|
518
|
+
for n, v in tqdm(self.results.items()):
|
|
519
|
+
r = TradingSessionResult.from_file(v.get("path"))
|
|
520
|
+
r.to_markdown(path, list(tags) if tags else None)
|
|
@@ -15,6 +15,7 @@ from qubx.core.helpers import extract_parameters_from_object, full_qualified_cla
|
|
|
15
15
|
from qubx.core.initializer import BasicStrategyInitializer
|
|
16
16
|
from qubx.core.interfaces import (
|
|
17
17
|
CtrlChannel,
|
|
18
|
+
IDataProvider,
|
|
18
19
|
IMetricEmitter,
|
|
19
20
|
IStrategy,
|
|
20
21
|
IStrategyContext,
|
|
@@ -224,7 +225,7 @@ class SimulationRunner:
|
|
|
224
225
|
cc = self.channel
|
|
225
226
|
t = np.datetime64(data.time, "ns")
|
|
226
227
|
_account = self.account.get_account_processor(instrument.exchange)
|
|
227
|
-
_data_provider = self.
|
|
228
|
+
_data_provider = self._get_data_provider(instrument.exchange)
|
|
228
229
|
assert isinstance(_account, SimulatedAccountProcessor)
|
|
229
230
|
assert isinstance(_data_provider, SimulatedDataProvider)
|
|
230
231
|
|
|
@@ -249,7 +250,7 @@ class SimulationRunner:
|
|
|
249
250
|
cc = self.channel
|
|
250
251
|
t = np.datetime64(data.time, "ns")
|
|
251
252
|
_account = self.account.get_account_processor(instrument.exchange)
|
|
252
|
-
_data_provider = self.
|
|
253
|
+
_data_provider = self._get_data_provider(instrument.exchange)
|
|
253
254
|
assert isinstance(_account, SimulatedAccountProcessor)
|
|
254
255
|
assert isinstance(_data_provider, SimulatedDataProvider)
|
|
255
256
|
|
|
@@ -267,6 +268,11 @@ class SimulationRunner:
|
|
|
267
268
|
|
|
268
269
|
return cc.control.is_set()
|
|
269
270
|
|
|
271
|
+
def _get_data_provider(self, exchange: str) -> IDataProvider:
|
|
272
|
+
if exchange in self._exchange_to_data_provider:
|
|
273
|
+
return self._exchange_to_data_provider[exchange]
|
|
274
|
+
raise ValueError(f"Data provider for exchange {exchange} not found")
|
|
275
|
+
|
|
270
276
|
def _run(self, start: pd.Timestamp, stop: pd.Timestamp, silent: bool = False) -> None:
|
|
271
277
|
logger.info(f"{self.__class__.__name__} ::: Simulation started at {start} :::")
|
|
272
278
|
|
|
@@ -328,7 +334,7 @@ class SimulationRunner:
|
|
|
328
334
|
|
|
329
335
|
if not _run(instrument, data_type, event, is_hist):
|
|
330
336
|
return False
|
|
331
|
-
|
|
337
|
+
|
|
332
338
|
return True
|
|
333
339
|
|
|
334
340
|
def _handle_no_data_scenario(self, stop_time):
|
|
@@ -82,6 +82,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
82
82
|
open_order_backoff: str = "1Min",
|
|
83
83
|
max_position_restore_days: int = 5,
|
|
84
84
|
max_retries: int = 10,
|
|
85
|
+
connection_timeout: int = 30,
|
|
85
86
|
read_only: bool = False,
|
|
86
87
|
):
|
|
87
88
|
super().__init__(
|
|
@@ -109,6 +110,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
109
110
|
self._latest_instruments = set()
|
|
110
111
|
self._subscription_manager = None
|
|
111
112
|
self._read_only = read_only
|
|
113
|
+
self._connection_timeout = connection_timeout
|
|
112
114
|
|
|
113
115
|
def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
|
|
114
116
|
self._subscription_manager = manager
|
|
@@ -128,6 +130,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
128
130
|
|
|
129
131
|
if not self.exchange_manager.exchange.isSandboxModeEnabled:
|
|
130
132
|
# - start polling tasks
|
|
133
|
+
self._loop.submit(self.exchange_manager.exchange.load_markets()).result()
|
|
131
134
|
self._polling_tasks["balance"] = self._loop.submit(
|
|
132
135
|
self._poller("balance", self._update_balance, self.balance_interval)
|
|
133
136
|
)
|
|
@@ -172,6 +175,14 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
172
175
|
super().update_position_price(time, instrument, update)
|
|
173
176
|
|
|
174
177
|
def get_total_capital(self, exchange: str | None = None) -> float:
|
|
178
|
+
# If polling is not running yet, we need to fetch balance data directly
|
|
179
|
+
if not self._is_running and self.exchange_manager.exchange:
|
|
180
|
+
try:
|
|
181
|
+
future = self._loop.submit(self._update_balance())
|
|
182
|
+
future.result(timeout=self._connection_timeout)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning(f"Failed to fetch balance data before polling started: {e}")
|
|
185
|
+
|
|
175
186
|
# sum of balances + market value of all positions on non spot/margin
|
|
176
187
|
_currency_to_value = {c: self._get_currency_value(b.total, c) for c, b in self._balances.items()}
|
|
177
188
|
_positions_value = sum([p.market_value_funds for p in self._positions.values() if p.instrument.is_futures()])
|
|
@@ -294,7 +305,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
294
305
|
async def _update_positions(self) -> None:
|
|
295
306
|
# fetch and update positions from exchange
|
|
296
307
|
ccxt_positions = await self.exchange_manager.exchange.fetch_positions()
|
|
297
|
-
positions = ccxt_convert_positions(
|
|
308
|
+
positions = ccxt_convert_positions(
|
|
309
|
+
ccxt_positions, self.exchange_manager.exchange.name, self.exchange_manager.exchange.markets
|
|
310
|
+
) # type: ignore
|
|
298
311
|
# update required instruments that we need to subscribe to
|
|
299
312
|
self._required_instruments.update([p.instrument for p in positions])
|
|
300
313
|
# update positions
|
|
@@ -458,7 +471,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
458
471
|
|
|
459
472
|
async def _cancel_order(order: Order) -> None:
|
|
460
473
|
try:
|
|
461
|
-
await self.exchange_manager.exchange.cancel_order(
|
|
474
|
+
await self.exchange_manager.exchange.cancel_order(
|
|
475
|
+
order.id, symbol=instrument_to_ccxt_symbol(order.instrument)
|
|
476
|
+
)
|
|
462
477
|
logger.debug(
|
|
463
478
|
f" :: [SYNC] Canceled {order.id} {order.instrument.symbol} {order.side} {order.quantity} @ {order.price} ({order.status})"
|
|
464
479
|
)
|
|
@@ -476,7 +491,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
476
491
|
) -> dict[str, Order]:
|
|
477
492
|
_start_ms = self._get_start_time_in_ms(days_before) if limit is None else None
|
|
478
493
|
_ccxt_symbol = instrument_to_ccxt_symbol(instrument)
|
|
479
|
-
_fetcher =
|
|
494
|
+
_fetcher = (
|
|
495
|
+
self.exchange_manager.exchange.fetch_open_orders if is_open else self.exchange_manager.exchange.fetch_orders
|
|
496
|
+
)
|
|
480
497
|
_raw_orders = await _fetcher(_ccxt_symbol, since=_start_ms, limit=limit)
|
|
481
498
|
_orders = [ccxt_convert_order_info(instrument, o) for o in _raw_orders]
|
|
482
499
|
_id_to_order = {o.id: o for o in _orders}
|
|
@@ -533,7 +550,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
533
550
|
async def _watch_executions():
|
|
534
551
|
exec = await self.exchange_manager.exchange.watch_orders()
|
|
535
552
|
for report in exec:
|
|
536
|
-
instrument = ccxt_find_instrument(
|
|
553
|
+
instrument = ccxt_find_instrument(
|
|
554
|
+
report["symbol"], self.exchange_manager.exchange, _symbol_to_instrument
|
|
555
|
+
)
|
|
537
556
|
order = ccxt_convert_order_info(instrument, report)
|
|
538
557
|
deals = ccxt_extract_deals_from_exec(report)
|
|
539
558
|
channel.send((instrument, "order", order, False))
|
|
@@ -24,12 +24,13 @@ SECONDS_PER_HOUR = 3600
|
|
|
24
24
|
|
|
25
25
|
# Custom stall detection thresholds (in seconds)
|
|
26
26
|
STALL_THRESHOLDS = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
"funding_payment": 12 * SECONDS_PER_HOUR, # 12 hours = 43,200s
|
|
28
|
+
"open_interest": 30 * 60, # 30 minutes = 1,800s
|
|
29
|
+
"orderbook": 5 * 60, # 5 minutes = 300s
|
|
30
|
+
"trade": 60 * 60, # 60 minutes = 3,600s
|
|
31
|
+
"liquidation": 7 * 24 * SECONDS_PER_HOUR, # 7 days = 604,800s
|
|
32
|
+
"ohlc": 5 * 60, # 5 minutes = 300s
|
|
33
|
+
"quote": 5 * 60, # 5 minutes = 300s
|
|
33
34
|
}
|
|
34
35
|
DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
|
|
35
36
|
|
|
@@ -37,10 +38,10 @@ DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
|
|
|
37
38
|
class ExchangeManager(IDataArrivalListener):
|
|
38
39
|
"""
|
|
39
40
|
Wrapper for CCXT Exchange that handles recreation internally with self-monitoring.
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
Exposes the underlying exchange via .exchange property for explicit access.
|
|
42
43
|
Self-monitors for data stalls and triggers recreation automatically.
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
Key Features:
|
|
45
46
|
- Explicit .exchange property for CCXT access
|
|
46
47
|
- Self-contained stall detection and recreation triggering
|
|
@@ -48,7 +49,7 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
48
49
|
- Atomic exchange transitions during recreation
|
|
49
50
|
- Background monitoring thread for stall detection
|
|
50
51
|
"""
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
_exchange: cxp.Exchange # Type hint that this is always a valid exchange
|
|
53
54
|
|
|
54
55
|
def __init__(
|
|
@@ -61,10 +62,10 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
61
62
|
check_interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
|
|
62
63
|
):
|
|
63
64
|
"""Initialize ExchangeManager with underlying CCXT exchange.
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
Args:
|
|
66
67
|
exchange_name: Exchange name for factory (e.g., "binance.um")
|
|
67
|
-
factory_params: Parameters for get_ccxt_exchange()
|
|
68
|
+
factory_params: Parameters for get_ccxt_exchange()
|
|
68
69
|
initial_exchange: Pre-created exchange instance (from factory)
|
|
69
70
|
max_recreations: Maximum recreation attempts before giving up
|
|
70
71
|
reset_interval_hours: Hours between recreation count resets
|
|
@@ -74,24 +75,24 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
74
75
|
self._factory_params = factory_params.copy()
|
|
75
76
|
self._max_recreations = max_recreations
|
|
76
77
|
self._reset_interval_hours = reset_interval_hours
|
|
77
|
-
|
|
78
|
+
|
|
78
79
|
# Recreation state
|
|
79
80
|
self._recreation_count = 0
|
|
80
81
|
self._recreation_lock = threading.RLock()
|
|
81
82
|
self._last_successful_reset = time.time()
|
|
82
|
-
|
|
83
|
+
|
|
83
84
|
# Stall detection state
|
|
84
85
|
self._check_interval = check_interval_seconds
|
|
85
86
|
self._last_data_times: dict[str, float] = {}
|
|
86
87
|
self._data_lock = threading.RLock()
|
|
87
|
-
|
|
88
|
+
|
|
88
89
|
# Monitoring control
|
|
89
90
|
self._monitoring_enabled = False
|
|
90
91
|
self._monitor_thread = None
|
|
91
|
-
|
|
92
|
+
|
|
92
93
|
# Recreation callback management
|
|
93
94
|
self._recreation_callbacks: list[Callable[[], None]] = []
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
# Use provided exchange or create new one
|
|
96
97
|
if initial_exchange:
|
|
97
98
|
self._exchange = initial_exchange
|
|
@@ -105,23 +106,23 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
105
106
|
try:
|
|
106
107
|
# Import here to avoid circular import (factory → broker → exchange_manager)
|
|
107
108
|
from .factory import get_ccxt_exchange
|
|
108
|
-
|
|
109
|
+
|
|
109
110
|
# Create raw exchange using factory logic
|
|
110
111
|
ccxt_exchange = get_ccxt_exchange(**self._factory_params)
|
|
111
|
-
|
|
112
|
+
|
|
112
113
|
# Setup exception handler for the new exchange
|
|
113
114
|
self._setup_ccxt_exception_handler(ccxt_exchange)
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
logger.debug(f"Created new {self._exchange_name} exchange instance")
|
|
116
117
|
return ccxt_exchange
|
|
117
|
-
|
|
118
|
+
|
|
118
119
|
except Exception as e:
|
|
119
120
|
logger.error(f"Failed to create {self._exchange_name} exchange: {e}")
|
|
120
121
|
raise RuntimeError(f"Failed to create {self._exchange_name} exchange: {e}") from e
|
|
121
|
-
|
|
122
|
+
|
|
122
123
|
def register_recreation_callback(self, callback: Callable[[], None]) -> None:
|
|
123
124
|
"""Register callback to be called after successful exchange recreation.
|
|
124
|
-
|
|
125
|
+
|
|
125
126
|
Args:
|
|
126
127
|
callback: Function to call after successful recreation (no parameters)
|
|
127
128
|
"""
|
|
@@ -131,7 +132,7 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
131
132
|
def _call_recreation_callbacks(self) -> None:
|
|
132
133
|
"""Call all registered recreation callbacks after successful exchange recreation."""
|
|
133
134
|
logger.debug(f"Calling {len(self._recreation_callbacks)} recreation callbacks for {self._exchange_name}")
|
|
134
|
-
|
|
135
|
+
|
|
135
136
|
for callback in self._recreation_callbacks:
|
|
136
137
|
try:
|
|
137
138
|
callback()
|
|
@@ -142,66 +143,68 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
142
143
|
def force_recreation(self) -> bool:
|
|
143
144
|
"""
|
|
144
145
|
Force recreation due to data stalls (called by BaseHealthMonitor).
|
|
145
|
-
|
|
146
|
+
|
|
146
147
|
Returns:
|
|
147
148
|
True if recreation successful, False if failed/limit exceeded
|
|
148
149
|
"""
|
|
149
150
|
with self._recreation_lock:
|
|
150
151
|
# Check recreation limit
|
|
151
152
|
if self._recreation_count >= self._max_recreations:
|
|
152
|
-
logger.error(
|
|
153
|
+
logger.error(
|
|
154
|
+
f"Cannot recreate {self._exchange_name}: recreation limit ({self._max_recreations}) exceeded"
|
|
155
|
+
)
|
|
153
156
|
return False
|
|
154
|
-
|
|
157
|
+
|
|
155
158
|
logger.info(f"Stall-triggered recreation for {self._exchange_name}")
|
|
156
159
|
return self._recreate_exchange()
|
|
157
|
-
|
|
160
|
+
|
|
158
161
|
def _recreate_exchange(self) -> bool:
|
|
159
162
|
"""Recreate the underlying exchange (must be called with _recreation_lock held)."""
|
|
160
163
|
self._recreation_count += 1
|
|
161
|
-
logger.warning(
|
|
162
|
-
|
|
164
|
+
logger.warning(
|
|
165
|
+
f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count}/{self._max_recreations})"
|
|
166
|
+
)
|
|
167
|
+
|
|
163
168
|
# Create new exchange
|
|
164
169
|
try:
|
|
165
170
|
new_exchange = self._create_exchange()
|
|
166
171
|
except Exception as e:
|
|
167
172
|
logger.error(f"Failed to recreate {self._exchange_name} exchange: {e}")
|
|
168
173
|
return False
|
|
169
|
-
|
|
174
|
+
|
|
170
175
|
# Atomically replace the exchange
|
|
171
176
|
old_exchange = self._exchange
|
|
172
177
|
self._exchange = new_exchange
|
|
173
|
-
|
|
178
|
+
|
|
174
179
|
# Clean up old exchange
|
|
175
180
|
try:
|
|
176
|
-
if hasattr(old_exchange,
|
|
177
|
-
old_exchange.asyncio_loop.call_soon_threadsafe(
|
|
178
|
-
lambda: asyncio.create_task(old_exchange.close())
|
|
179
|
-
)
|
|
181
|
+
if hasattr(old_exchange, "close") and hasattr(old_exchange, "asyncio_loop"):
|
|
182
|
+
old_exchange.asyncio_loop.call_soon_threadsafe(lambda: asyncio.create_task(old_exchange.close()))
|
|
180
183
|
except Exception as e:
|
|
181
184
|
logger.warning(f"Error closing old {self._exchange_name} exchange: {e}")
|
|
182
|
-
|
|
185
|
+
|
|
183
186
|
logger.info(f"Successfully recreated {self._exchange_name} exchange")
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
# Call recreation callbacks after successful recreation
|
|
186
189
|
self._call_recreation_callbacks()
|
|
187
|
-
|
|
190
|
+
|
|
188
191
|
return True
|
|
189
|
-
|
|
192
|
+
|
|
190
193
|
def reset_recreation_count_if_needed(self) -> None:
|
|
191
194
|
"""Reset recreation count periodically (called by monitoring loop)."""
|
|
192
195
|
reset_interval_seconds = self._reset_interval_hours * SECONDS_PER_HOUR
|
|
193
|
-
|
|
196
|
+
|
|
194
197
|
current_time = time.time()
|
|
195
198
|
time_since_reset = current_time - self._last_successful_reset
|
|
196
|
-
|
|
199
|
+
|
|
197
200
|
if time_since_reset >= reset_interval_seconds and self._recreation_count > 0:
|
|
198
201
|
logger.info(f"Resetting recreation count for {self._exchange_name} (was {self._recreation_count})")
|
|
199
202
|
self._recreation_count = 0
|
|
200
203
|
self._last_successful_reset = current_time
|
|
201
|
-
|
|
204
|
+
|
|
202
205
|
def on_data_arrival(self, event_type: str, event_time: dt_64) -> None:
|
|
203
206
|
"""Record data arrival for stall detection.
|
|
204
|
-
|
|
207
|
+
|
|
205
208
|
Args:
|
|
206
209
|
event_type: Type of data event (e.g., "ohlcv", "trade", "orderbook")
|
|
207
210
|
event_time: Timestamp of the data event (unused for stall detection)
|
|
@@ -209,10 +212,10 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
209
212
|
current_timestamp = time.time()
|
|
210
213
|
with self._data_lock:
|
|
211
214
|
self._last_data_times[event_type] = current_timestamp
|
|
212
|
-
|
|
215
|
+
|
|
213
216
|
def _extract_ohlc_timeframe(self, event_type: str) -> Optional[str]:
|
|
214
217
|
"""Extract timeframe from OHLC event type like 'ohlc(1m)' -> '1m'."""
|
|
215
|
-
if event_type.startswith(
|
|
218
|
+
if event_type.startswith("ohlc(") and event_type.endswith(")"):
|
|
216
219
|
return event_type[5:-1] # Simple slice: ohlc(1m) -> 1m
|
|
217
220
|
return None
|
|
218
221
|
|
|
@@ -222,30 +225,30 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
222
225
|
|
|
223
226
|
def _get_stall_threshold(self, event_type: str) -> float:
|
|
224
227
|
"""Get stall threshold for specific event type.
|
|
225
|
-
|
|
228
|
+
|
|
226
229
|
Extracts base data type from parameterized types like 'ohlc(1m)' -> 'ohlc'.
|
|
227
230
|
"""
|
|
228
231
|
# Extract base data type (everything before first '(' if present)
|
|
229
|
-
base_event_type = event_type.split(
|
|
232
|
+
base_event_type = event_type.split("(")[0]
|
|
230
233
|
return float(STALL_THRESHOLDS.get(base_event_type, DEFAULT_STALL_THRESHOLD_SECONDS))
|
|
231
|
-
|
|
234
|
+
|
|
232
235
|
def start_monitoring(self) -> None:
|
|
233
236
|
"""Start background stall detection monitoring."""
|
|
234
237
|
if self._monitoring_enabled:
|
|
235
238
|
return
|
|
236
|
-
|
|
239
|
+
|
|
237
240
|
self._monitoring_enabled = True
|
|
238
241
|
self._monitor_thread = threading.Thread(target=self._stall_monitor_loop, daemon=True)
|
|
239
242
|
self._monitor_thread.start()
|
|
240
243
|
logger.debug(f"ExchangeManager: Started stall monitoring for {self._exchange_name}")
|
|
241
|
-
|
|
244
|
+
|
|
242
245
|
def stop_monitoring(self) -> None:
|
|
243
246
|
"""Stop background stall detection monitoring."""
|
|
244
247
|
self._monitoring_enabled = False
|
|
245
248
|
if self._monitor_thread:
|
|
246
249
|
self._monitor_thread = None
|
|
247
250
|
logger.debug(f"ExchangeManager: Stopped stall monitoring for {self._exchange_name}")
|
|
248
|
-
|
|
251
|
+
|
|
249
252
|
def _stall_monitor_loop(self) -> None:
|
|
250
253
|
"""Background thread that checks for data stalls and triggers self-recreation."""
|
|
251
254
|
while self._monitoring_enabled:
|
|
@@ -256,26 +259,26 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
256
259
|
except Exception as e:
|
|
257
260
|
logger.error(f"Error in ExchangeManager stall detection: {e}")
|
|
258
261
|
time.sleep(self._check_interval)
|
|
259
|
-
|
|
262
|
+
|
|
260
263
|
def _check_and_handle_stalls(self) -> None:
|
|
261
264
|
"""Check for stalls using custom thresholds per data type."""
|
|
262
265
|
current_time = time.time()
|
|
263
266
|
stalled_types = []
|
|
264
|
-
|
|
267
|
+
|
|
265
268
|
with self._data_lock:
|
|
266
269
|
for event_type, last_data_time in self._last_data_times.items():
|
|
267
270
|
time_since_data = current_time - last_data_time
|
|
268
271
|
threshold = self._get_stall_threshold(event_type)
|
|
269
|
-
|
|
272
|
+
|
|
270
273
|
if time_since_data > threshold:
|
|
271
274
|
stalled_types.append((event_type, time_since_data))
|
|
272
|
-
|
|
275
|
+
|
|
273
276
|
if not stalled_types:
|
|
274
277
|
return # No stalls detected
|
|
275
|
-
|
|
278
|
+
|
|
276
279
|
stall_info = ", ".join([f"{event_type}({int(time_since)}s)" for event_type, time_since in stalled_types])
|
|
277
280
|
logger.error(f"Data stalls detected in {self._exchange_name}: {stall_info}")
|
|
278
|
-
|
|
281
|
+
|
|
279
282
|
try:
|
|
280
283
|
logger.info(f"Self-triggering recreation for {self._exchange_name} due to stalls...")
|
|
281
284
|
if self.force_recreation():
|
|
@@ -288,14 +291,14 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
288
291
|
logger.error(f"Stall-triggered recreation failed for {self._exchange_name}")
|
|
289
292
|
except Exception as e:
|
|
290
293
|
logger.error(f"Error during stall-triggered recreation: {e}")
|
|
291
|
-
|
|
294
|
+
|
|
292
295
|
def _setup_ccxt_exception_handler(self, exchange: cxp.Exchange) -> None:
|
|
293
296
|
"""
|
|
294
297
|
Set up global exception handler for the CCXT async loop to handle unretrieved futures.
|
|
295
298
|
|
|
296
299
|
This prevents 'Future exception was never retrieved' warnings from CCXT's internal
|
|
297
300
|
per-symbol futures that complete with UnsubscribeError during resubscription.
|
|
298
|
-
|
|
301
|
+
|
|
299
302
|
Applied to every newly created exchange (initial and recreated).
|
|
300
303
|
"""
|
|
301
304
|
asyncio_loop = exchange.asyncio_loop
|
|
@@ -324,15 +327,15 @@ class ExchangeManager(IDataArrivalListener):
|
|
|
324
327
|
# Set the custom exception handler on the CCXT loop
|
|
325
328
|
asyncio_loop.set_exception_handler(handle_ccxt_exception)
|
|
326
329
|
|
|
327
|
-
# === Exchange Property Access ===
|
|
330
|
+
# === Exchange Property Access ===
|
|
328
331
|
# Explicit property to access underlying CCXT exchange
|
|
329
|
-
|
|
332
|
+
|
|
330
333
|
@property
|
|
331
334
|
def exchange(self) -> cxp.Exchange:
|
|
332
335
|
"""Access to the underlying CCXT exchange instance.
|
|
333
|
-
|
|
336
|
+
|
|
334
337
|
Use this property to call CCXT methods: exchange_manager.exchange.fetch_ticker(symbol)
|
|
335
|
-
|
|
338
|
+
|
|
336
339
|
Returns:
|
|
337
340
|
The current CCXT exchange instance (may change after recreation)
|
|
338
341
|
"""
|
|
@@ -9,7 +9,7 @@ from qubx.core.exceptions import BadRequest
|
|
|
9
9
|
class HyperliquidCcxtBroker(CcxtBroker):
|
|
10
10
|
"""
|
|
11
11
|
HyperLiquid-specific broker that handles market order slippage requirements.
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
HyperLiquid requires a price even for market orders to calculate max slippage.
|
|
14
14
|
This broker automatically calculates slippage-protected prices for market orders.
|
|
15
15
|
"""
|
|
@@ -17,7 +17,7 @@ class HyperliquidCcxtBroker(CcxtBroker):
|
|
|
17
17
|
def __init__(
|
|
18
18
|
self,
|
|
19
19
|
*args,
|
|
20
|
-
market_order_slippage: float = 0.
|
|
20
|
+
market_order_slippage: float = 0.01, # 5% default slippage
|
|
21
21
|
**kwargs,
|
|
22
22
|
):
|
|
23
23
|
super().__init__(*args, **kwargs)
|
|
@@ -38,21 +38,29 @@ class HyperliquidCcxtBroker(CcxtBroker):
|
|
|
38
38
|
if order_type.lower() == "market" and price is None:
|
|
39
39
|
quote = self.data_provider.get_quote(instrument)
|
|
40
40
|
if quote is None:
|
|
41
|
-
logger.warning(
|
|
42
|
-
|
|
41
|
+
logger.warning(
|
|
42
|
+
f"[<y>{instrument.symbol}</y>] :: Quote is not available for market order slippage calculation."
|
|
43
|
+
)
|
|
44
|
+
raise BadRequest(
|
|
45
|
+
f"Quote is not available for market order slippage calculation for {instrument.symbol}"
|
|
46
|
+
)
|
|
43
47
|
|
|
44
48
|
# Get slippage from options or use default
|
|
45
49
|
slippage = options.get("slippage", self.market_order_slippage)
|
|
46
|
-
|
|
50
|
+
|
|
47
51
|
# Calculate slippage-protected price
|
|
48
52
|
if order_side.upper() == "BUY":
|
|
49
53
|
# For buy orders, add slippage to ask price to ensure execution
|
|
50
54
|
price = quote.ask * (1 + slippage)
|
|
51
|
-
logger.debug(
|
|
55
|
+
logger.debug(
|
|
56
|
+
f"[<y>{instrument.symbol}</y>] :: Market BUY order: using slippage-protected price {price:.6f} (ask: {quote.ask:.6f}, slippage: {slippage:.1%})"
|
|
57
|
+
)
|
|
52
58
|
else: # SELL
|
|
53
59
|
# For sell orders, subtract slippage from bid price to ensure execution
|
|
54
60
|
price = quote.bid * (1 - slippage)
|
|
55
|
-
logger.debug(
|
|
61
|
+
logger.debug(
|
|
62
|
+
f"[<y>{instrument.symbol}</y>] :: Market SELL order: using slippage-protected price {price:.6f} (bid: {quote.bid:.6f}, slippage: {slippage:.1%})"
|
|
63
|
+
)
|
|
56
64
|
|
|
57
65
|
# Call parent implementation with calculated price
|
|
58
66
|
payload = super()._prepare_order_payload(
|
|
@@ -64,6 +72,6 @@ class HyperliquidCcxtBroker(CcxtBroker):
|
|
|
64
72
|
if "slippage" in options:
|
|
65
73
|
# HyperLiquid accepts slippage as a percentage (e.g., 0.05 for 5%)
|
|
66
74
|
params["px"] = price # Explicit price for slippage calculation
|
|
67
|
-
|
|
75
|
+
|
|
68
76
|
payload["params"] = params
|
|
69
|
-
return payload
|
|
77
|
+
return payload
|
|
@@ -65,7 +65,7 @@ class OrderBookDataHandler(BaseDataTypeHandler):
|
|
|
65
65
|
|
|
66
66
|
# Notify all listeners
|
|
67
67
|
self._data_provider.notify_data_arrival(sub_type, dt_64(ob.time, "ns"))
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
channel.send((instrument, sub_type, ob, False))
|
|
70
70
|
return True
|
|
71
71
|
|
|
@@ -150,7 +150,7 @@ class OrderBookDataHandler(BaseDataTypeHandler):
|
|
|
150
150
|
) -> SubscriptionConfiguration:
|
|
151
151
|
"""
|
|
152
152
|
Prepare subscription configuration for individual instruments.
|
|
153
|
-
|
|
153
|
+
|
|
154
154
|
Creates separate subscriber functions for each instrument to enable independent
|
|
155
155
|
WebSocket streams without waiting for all instruments. This follows the same
|
|
156
156
|
pattern as the OHLC handler for proper individual stream management.
|
|
@@ -169,10 +169,10 @@ class OrderBookDataHandler(BaseDataTypeHandler):
|
|
|
169
169
|
try:
|
|
170
170
|
# Watch orderbook for single instrument
|
|
171
171
|
ccxt_ob = await self._exchange_manager.exchange.watch_order_book(symbol)
|
|
172
|
-
|
|
172
|
+
|
|
173
173
|
# Use private processing method to avoid duplication
|
|
174
174
|
self._process_orderbook(ccxt_ob, inst, sub_type, channel, depth, tick_size_pct)
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
except Exception as e:
|
|
177
177
|
logger.error(
|
|
178
178
|
f"<yellow>{exchange_id}</yellow> Error in individual orderbook subscription for {inst.symbol}: {e}"
|
|
@@ -186,13 +186,15 @@ class OrderBookDataHandler(BaseDataTypeHandler):
|
|
|
186
186
|
# Create individual unsubscriber if exchange supports it
|
|
187
187
|
un_watch_method = getattr(self._exchange_manager.exchange, "un_watch_order_book", None)
|
|
188
188
|
if un_watch_method is not None and callable(un_watch_method):
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
def create_individual_unsubscriber(symbol=ccxt_symbol, exchange_id=self._exchange_id):
|
|
191
191
|
async def individual_unsubscriber():
|
|
192
192
|
try:
|
|
193
193
|
await self._exchange_manager.exchange.un_watch_order_book(symbol)
|
|
194
194
|
except Exception as e:
|
|
195
|
-
logger.error(
|
|
195
|
+
logger.error(
|
|
196
|
+
f"<yellow>{exchange_id}</yellow> Error unsubscribing orderbook for {symbol}: {e}"
|
|
197
|
+
)
|
|
196
198
|
|
|
197
199
|
return individual_unsubscriber
|
|
198
200
|
|