Qubx 1.2.1.dev1__tar.gz → 1.2.2.dev2__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.
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/PKG-INFO +1 -1
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/_version.py +2 -2
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/release.py +5 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/account.py +27 -1
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +13 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/okx/account.py +18 -3
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/account.py +4 -1
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/engine.py +47 -175
- qubx-1.2.2.dev2/src/qubx/rate_limiting/pools.py +201 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/.gitignore +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/LICENSE +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/README.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/pyproject.toml +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/scripts/build.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/_nb_magic.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/account.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/management.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/ome.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/optimization.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/runner.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/simulator.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/transfers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/commands.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/deploy.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/misc.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/resolver.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/s3.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/theme.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/tui.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/user_config.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/config.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/registry.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/builtin.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/decorator.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/executor.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/interfaces.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/server.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/types.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/basics.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/context.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/errors.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/exceptions.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/helpers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/initializer.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/interfaces.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/loggers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/lookups.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/metrics.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/market.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/series.pxd +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/series.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/series.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/utils.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/utils.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/cache.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/containers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/guards.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/registry.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storage.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/ccxt.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/csv.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/handy.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/multi.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/stub.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/transformers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/composite.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/csv.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/indicator.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/questdb.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/composite.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/slack.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/gathering/simplest.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/health/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/health/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/health/dummy.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/csv.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/mongo.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/postgres.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/notifications/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/notifications/composite.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/notifications/slack.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/notifications/throttler.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/pandaz/stats.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/pandaz/ta.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/pandaz/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/plugins/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/plugins/loader.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/backend.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/config.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/ip_resolver.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/redis_backend.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/_build.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restarts/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/balance.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/position.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/signal.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/state.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/state/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/state/dummy.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/state/redis.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/ta/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/template.yml +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.gitignore.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.python-version +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.vscode/launch.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.vscode/settings.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/README.md.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/configs/.gitkeep +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/justfile.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/research/.gitkeep +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/template.yml +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/advanced.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/composite.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/sizers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/collections.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/misc.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/nonce.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/ntp.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/orderbook.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/questdb.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/results.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/runner.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/s3.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/slack.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/throttler.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/time.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 2,
|
|
21
|
+
__version__ = version = '1.2.2.dev2'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 2, 'dev2')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1432,12 +1432,14 @@ def _bundle_source_overrides(
|
|
|
1432
1432
|
continue
|
|
1433
1433
|
|
|
1434
1434
|
checkout_dir = _find_uv_git_checkout(commit_sha)
|
|
1435
|
+
cloned_locally = False
|
|
1435
1436
|
if not checkout_dir:
|
|
1436
1437
|
# Cache miss — clone the repo at the exact commit
|
|
1437
1438
|
git_url = source["git"]
|
|
1438
1439
|
git_ref = source.get("tag") or source.get("branch") or commit_sha
|
|
1439
1440
|
logger.info(f" {pkg_name}: cache miss, cloning {git_url} at {git_ref} ...")
|
|
1440
1441
|
checkout_dir = os.path.join(os.path.dirname(wheels_dir), f".git-clone-{pkg_norm}")
|
|
1442
|
+
cloned_locally = True
|
|
1441
1443
|
try:
|
|
1442
1444
|
subprocess.run(
|
|
1443
1445
|
["git", "clone", "--depth", "1", "--branch", git_ref, git_url, checkout_dir],
|
|
@@ -1473,6 +1475,9 @@ def _bundle_source_overrides(
|
|
|
1473
1475
|
logger.info(f" Bundled {pkg_name}=={pkg_ver} from git")
|
|
1474
1476
|
except subprocess.CalledProcessError as e:
|
|
1475
1477
|
logger.opt(colors=False).warning(f" Failed to build wheel for {pkg_name}: {e.stderr or e}")
|
|
1478
|
+
finally:
|
|
1479
|
+
if cloned_locally and os.path.isdir(checkout_dir):
|
|
1480
|
+
shutil.rmtree(checkout_dir, ignore_errors=True)
|
|
1476
1481
|
|
|
1477
1482
|
return bundled
|
|
1478
1483
|
|
|
@@ -222,7 +222,10 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
222
222
|
except Exception as e:
|
|
223
223
|
logger.warning(f"Failed to fetch balance data before polling started: {e}")
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
if self._exchange_total_capital is not None:
|
|
226
|
+
return self._exchange_total_capital
|
|
227
|
+
|
|
228
|
+
# fallback: sum of balances + market value of all positions on non spot/margin
|
|
226
229
|
_currency_to_value = {c: self._get_currency_value(b.total, c) for c, b in self._balances.items()}
|
|
227
230
|
_positions_value = sum([p.market_value_funds for p in self._positions.values() if p.instrument.is_futures()])
|
|
228
231
|
return sum(_currency_to_value.values()) + _positions_value
|
|
@@ -316,9 +319,32 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
316
319
|
await self._subscribe_instruments(list(self._required_instruments))
|
|
317
320
|
self._latest_instruments.update(self._required_instruments)
|
|
318
321
|
|
|
322
|
+
def _extract_portfolio_value(self, balances_raw: dict) -> float | None:
|
|
323
|
+
"""Extract exchange-reported portfolio value (equity) from raw balance response.
|
|
324
|
+
|
|
325
|
+
Override in exchange-specific processors for non-standard responses.
|
|
326
|
+
Base implementation handles Binance futures (totalMarginBalance).
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Total equity as float, or None to fall back to computed value.
|
|
330
|
+
"""
|
|
331
|
+
info = balances_raw.get("info", {})
|
|
332
|
+
val = info.get("totalMarginBalance")
|
|
333
|
+
if val is not None:
|
|
334
|
+
try:
|
|
335
|
+
return float(val)
|
|
336
|
+
except (ValueError, TypeError):
|
|
337
|
+
pass
|
|
338
|
+
return None
|
|
339
|
+
|
|
319
340
|
async def _update_balance(self) -> None:
|
|
320
341
|
"""Fetch and update balances from exchange"""
|
|
321
342
|
balances_raw = await self.exchange_manager.exchange.fetch_balance()
|
|
343
|
+
|
|
344
|
+
equity = self._extract_portfolio_value(balances_raw)
|
|
345
|
+
if equity is not None:
|
|
346
|
+
self._exchange_total_capital = equity
|
|
347
|
+
|
|
322
348
|
balances = ccxt_convert_balance(balances_raw, self.exchange)
|
|
323
349
|
current_balances = self.get_balances()
|
|
324
350
|
|
{qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py
RENAMED
|
@@ -23,6 +23,19 @@ class HyperliquidAccountProcessor(CcxtAccountProcessor):
|
|
|
23
23
|
so we must subscribe to both channels separately.
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
+
def _extract_portfolio_value(self, balances_raw: dict) -> float | None:
|
|
27
|
+
"""Extract Hyperliquid account value from raw balance response."""
|
|
28
|
+
info = balances_raw.get("info", {})
|
|
29
|
+
for key in ("marginSummary", "crossMarginSummary"):
|
|
30
|
+
summary = info.get(key, {})
|
|
31
|
+
val = summary.get("accountValue")
|
|
32
|
+
if val is not None:
|
|
33
|
+
try:
|
|
34
|
+
return float(val)
|
|
35
|
+
except (ValueError, TypeError):
|
|
36
|
+
pass
|
|
37
|
+
return None
|
|
38
|
+
|
|
26
39
|
async def _subscribe_instruments(self, instruments: list[Instrument]):
|
|
27
40
|
"""Override to filter out instruments from other exchanges (e.g., spot vs futures)."""
|
|
28
41
|
# Filter instruments to only those belonging to this exchange
|
|
@@ -57,15 +57,30 @@ class OkxAccountProcessor(CcxtAccountProcessor):
|
|
|
57
57
|
),
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
+
def _extract_portfolio_value(self, balances_raw: dict) -> float | None:
|
|
61
|
+
"""Extract OKX total equity from raw balance response."""
|
|
62
|
+
data = balances_raw.get("info", {}).get("data", [{}])[0]
|
|
63
|
+
total_eq = data.get("totalEq")
|
|
64
|
+
if total_eq is not None:
|
|
65
|
+
try:
|
|
66
|
+
return float(total_eq)
|
|
67
|
+
except (ValueError, TypeError):
|
|
68
|
+
pass
|
|
69
|
+
return None
|
|
70
|
+
|
|
60
71
|
async def _update_balance(self) -> None:
|
|
61
|
-
"""OKX balance override: use cashBal
|
|
72
|
+
"""OKX balance override: use cashBal for per-currency balances.
|
|
62
73
|
|
|
63
74
|
CCXT maps OKX's `eq` (equity = cashBal + unrealizedPnL) to balance `total`.
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
We use `cashBal` for individual currency balances, and extract `totalEq`
|
|
76
|
+
as the authoritative portfolio value via _extract_portfolio_value().
|
|
66
77
|
"""
|
|
67
78
|
balances_raw = await self.exchange_manager.exchange.fetch_balance()
|
|
68
79
|
|
|
80
|
+
equity = self._extract_portfolio_value(balances_raw)
|
|
81
|
+
if equity is not None:
|
|
82
|
+
self._exchange_total_capital = equity
|
|
83
|
+
|
|
69
84
|
# Extract cashBal from raw OKX response instead of CCXT-parsed eq
|
|
70
85
|
balances: list[AssetBalance] = []
|
|
71
86
|
raw_details = balances_raw.get("info", {}).get("data", [{}])[0].get("details", [])
|
|
@@ -65,6 +65,7 @@ class BasicAccountProcessor(IAccountProcessor):
|
|
|
65
65
|
self._locked_capital_by_order = dict()
|
|
66
66
|
self._pending_order_requests = {}
|
|
67
67
|
self._balances = {}
|
|
68
|
+
self._exchange_total_capital: float | None = None
|
|
68
69
|
# Initialize with base currency balance
|
|
69
70
|
self._balances[self.base_currency] = AssetBalance(
|
|
70
71
|
exchange=self.exchange, currency=self.base_currency, free=initial_capital, locked=0.0, total=initial_capital
|
|
@@ -90,7 +91,9 @@ class BasicAccountProcessor(IAccountProcessor):
|
|
|
90
91
|
return self.get_available_margin(exchange)
|
|
91
92
|
|
|
92
93
|
def get_total_capital(self, exchange: str | None = None) -> float:
|
|
93
|
-
|
|
94
|
+
if self._exchange_total_capital is not None:
|
|
95
|
+
return self._exchange_total_capital
|
|
96
|
+
# fallback: sum of cash + market value of all positions
|
|
94
97
|
_cash_amount = self._balances[self.base_currency].total
|
|
95
98
|
_positions_value = sum([p.market_value_funds for p in self._positions.values()])
|
|
96
99
|
return _cash_amount + _positions_value
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Exchange rate limiting engine.
|
|
1
|
+
"""Exchange rate limiting engine.
|
|
3
2
|
|
|
4
|
-
The central component that connectors interact with.
|
|
5
|
-
|
|
6
|
-
and metric collection.
|
|
3
|
+
The central component that connectors interact with. Delegates pool-specific
|
|
4
|
+
behavior (gate management, acquisition, sync) to polymorphic pool objects.
|
|
7
5
|
"""
|
|
8
6
|
|
|
9
7
|
import asyncio
|
|
@@ -13,11 +11,9 @@ from typing import Any
|
|
|
13
11
|
from qubx import logger
|
|
14
12
|
|
|
15
13
|
from .backend import InMemoryBackend, IRateLimitBackend
|
|
16
|
-
from .config import ExchangeRateLimitConfig
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class RateLimitGateTimeout(Exception):
|
|
20
|
-
"""Raised when acquire() times out waiting for a gate to reopen."""
|
|
14
|
+
from .config import ExchangeRateLimitConfig
|
|
15
|
+
from .pools import BasePool, QuotaPool, RatePool
|
|
16
|
+
from .pools import RateLimitGateTimeout as RateLimitGateTimeout # re-export
|
|
21
17
|
|
|
22
18
|
|
|
23
19
|
class ExchangeRateLimiter:
|
|
@@ -59,30 +55,19 @@ class ExchangeRateLimiter:
|
|
|
59
55
|
"""
|
|
60
56
|
self._exchange = exchange
|
|
61
57
|
self._config = config
|
|
62
|
-
self._backend = backend or InMemoryBackend()
|
|
63
58
|
self._scope_ids = scope_ids or {}
|
|
64
59
|
self._event_loop = event_loop
|
|
65
|
-
|
|
66
|
-
# Per-pool gates (asyncio.Event: set = open, clear = closed)
|
|
67
|
-
self._gates: dict[str, asyncio.Event] = {}
|
|
68
|
-
self._gate_tasks: dict[str, asyncio.Task] = {}
|
|
69
|
-
for pool_name in config.pools:
|
|
70
|
-
gate = asyncio.Event()
|
|
71
|
-
gate.set()
|
|
72
|
-
self._gates[pool_name] = gate
|
|
73
|
-
|
|
74
|
-
# Quota pools: externally-managed remaining count
|
|
75
|
-
self._quota_remaining: dict[str, float] = {}
|
|
76
|
-
for pool_name, pool in config.pools.items():
|
|
77
|
-
if pool.pool_type == "quota":
|
|
78
|
-
self._quota_remaining[pool_name] = pool.capacity
|
|
79
|
-
|
|
80
|
-
# Stats tracking
|
|
81
|
-
self._hits: dict[str, int] = {}
|
|
82
|
-
self._total_wait: dict[str, float] = {}
|
|
83
|
-
self._consumed: dict[str, float] = {}
|
|
84
60
|
self._last_metrics_time = time.monotonic()
|
|
85
61
|
|
|
62
|
+
backend = backend or InMemoryBackend()
|
|
63
|
+
self._pools: dict[str, BasePool] = {}
|
|
64
|
+
for pool_name, pool_config in config.pools.items():
|
|
65
|
+
scope_id = self._scope_ids.get(pool_config.scope, "local")
|
|
66
|
+
if pool_config.pool_type == "quota":
|
|
67
|
+
self._pools[pool_name] = QuotaPool(pool_config, exchange, scope_id)
|
|
68
|
+
else:
|
|
69
|
+
self._pools[pool_name] = RatePool(pool_config, exchange, scope_id, backend)
|
|
70
|
+
|
|
86
71
|
@property
|
|
87
72
|
def exchange(self) -> str:
|
|
88
73
|
return self._exchange
|
|
@@ -100,11 +85,6 @@ class ExchangeRateLimiter:
|
|
|
100
85
|
def config(self) -> ExchangeRateLimitConfig:
|
|
101
86
|
return self._config
|
|
102
87
|
|
|
103
|
-
def _key_for(self, pool: PoolConfig) -> str:
|
|
104
|
-
"""Construct Redis/backend key for a pool based on its scope."""
|
|
105
|
-
scope_id = self._scope_ids.get(pool.scope, "local")
|
|
106
|
-
return f"ratelimit:{self._exchange}:{pool.name}:{scope_id}"
|
|
107
|
-
|
|
108
88
|
def update_scope_id(self, scope: str, new_id: str) -> None:
|
|
109
89
|
"""Update scope identifier (e.g., when egress IP changes).
|
|
110
90
|
|
|
@@ -114,6 +94,9 @@ class ExchangeRateLimiter:
|
|
|
114
94
|
if old_id != new_id:
|
|
115
95
|
logger.info(f"Rate limiter {self._exchange}: {scope} scope changed {old_id} → {new_id}")
|
|
116
96
|
self._scope_ids[scope] = new_id
|
|
97
|
+
for pool in self._pools.values():
|
|
98
|
+
if pool.config.scope == scope:
|
|
99
|
+
pool.update_scope_id(new_id)
|
|
117
100
|
|
|
118
101
|
async def acquire(self, endpoint: str, weight_override: float | None = None) -> None:
|
|
119
102
|
"""Wait until all pools for this endpoint have sufficient budget.
|
|
@@ -134,50 +117,11 @@ class ExchangeRateLimiter:
|
|
|
134
117
|
return
|
|
135
118
|
|
|
136
119
|
for i, (pool_name, weight) in enumerate(endpoint_costs.costs):
|
|
137
|
-
pool = self.
|
|
120
|
+
pool = self._pools.get(pool_name)
|
|
138
121
|
if pool is None:
|
|
139
122
|
continue
|
|
140
|
-
|
|
141
123
|
actual_weight = weight_override if (weight_override is not None and i == 0) else weight
|
|
142
|
-
|
|
143
|
-
# Wait for gate to be open
|
|
144
|
-
gate = self._gates.get(pool_name)
|
|
145
|
-
if gate is not None and not gate.is_set():
|
|
146
|
-
try:
|
|
147
|
-
await asyncio.wait_for(gate.wait(), timeout=self._config.gate_max_wait)
|
|
148
|
-
except asyncio.TimeoutError:
|
|
149
|
-
raise RateLimitGateTimeout(
|
|
150
|
-
f"{self._exchange}: gate for pool '{pool_name}' did not reopen "
|
|
151
|
-
f"within {self._config.gate_max_wait:.0f}s"
|
|
152
|
-
) from None
|
|
153
|
-
|
|
154
|
-
# For quota pools, check remaining (no time-based refill)
|
|
155
|
-
if pool.pool_type == "quota":
|
|
156
|
-
remaining = self._quota_remaining.get(pool_name, 0)
|
|
157
|
-
if remaining <= 0:
|
|
158
|
-
# Gate should already be closed, but double-check
|
|
159
|
-
self._close_gate(pool_name, pool.cooldown, "quota depleted")
|
|
160
|
-
gate = self._gates.get(pool_name)
|
|
161
|
-
if gate is not None:
|
|
162
|
-
try:
|
|
163
|
-
await asyncio.wait_for(gate.wait(), timeout=self._config.gate_max_wait)
|
|
164
|
-
except asyncio.TimeoutError:
|
|
165
|
-
raise RateLimitGateTimeout(
|
|
166
|
-
f"{self._exchange}: quota pool '{pool_name}' depleted, "
|
|
167
|
-
f"gate did not reopen within {self._config.gate_max_wait:.0f}s"
|
|
168
|
-
) from None
|
|
169
|
-
# Decrement quota locally
|
|
170
|
-
if pool_name in self._quota_remaining:
|
|
171
|
-
self._quota_remaining[pool_name] = max(0, self._quota_remaining[pool_name] - actual_weight)
|
|
172
|
-
continue
|
|
173
|
-
|
|
174
|
-
# For rate pools, acquire from backend
|
|
175
|
-
key = self._key_for(pool)
|
|
176
|
-
wait_time = await self._backend.acquire(key, actual_weight, pool.capacity, pool.refill_rate)
|
|
177
|
-
|
|
178
|
-
# Track stats
|
|
179
|
-
self._total_wait[pool_name] = self._total_wait.get(pool_name, 0) + wait_time
|
|
180
|
-
self._consumed[pool_name] = self._consumed.get(pool_name, 0) + actual_weight
|
|
124
|
+
await pool.acquire(actual_weight, self._config.gate_max_wait)
|
|
181
125
|
|
|
182
126
|
def report_limit_hit(
|
|
183
127
|
self,
|
|
@@ -197,7 +141,7 @@ class ExchangeRateLimiter:
|
|
|
197
141
|
retry_after: Seconds to wait (from Retry-After header)
|
|
198
142
|
reason: Human-readable reason for logging
|
|
199
143
|
"""
|
|
200
|
-
pools_to_close = []
|
|
144
|
+
pools_to_close: list[str] = []
|
|
201
145
|
|
|
202
146
|
if pool_name:
|
|
203
147
|
pools_to_close.append(pool_name)
|
|
@@ -206,15 +150,17 @@ class ExchangeRateLimiter:
|
|
|
206
150
|
pools_to_close = [p for p, _ in costs.costs]
|
|
207
151
|
else:
|
|
208
152
|
# Close all rate pools
|
|
209
|
-
pools_to_close = [
|
|
153
|
+
pools_to_close = [
|
|
154
|
+
name for name, pool in self._pools.items() if pool.config.pool_type == "rate"
|
|
155
|
+
]
|
|
210
156
|
|
|
211
157
|
for pname in pools_to_close:
|
|
212
|
-
pool = self.
|
|
158
|
+
pool = self._pools.get(pname)
|
|
213
159
|
if pool is None:
|
|
214
160
|
continue
|
|
215
|
-
cooldown = retry_after if retry_after is not None else pool.cooldown
|
|
216
|
-
|
|
217
|
-
|
|
161
|
+
cooldown = retry_after if retry_after is not None else pool.config.cooldown
|
|
162
|
+
pool.close_gate(cooldown, reason or f"rate limit hit on {pname}")
|
|
163
|
+
pool.hits += 1
|
|
218
164
|
|
|
219
165
|
def sync_from_exchange(
|
|
220
166
|
self,
|
|
@@ -235,11 +181,11 @@ class ExchangeRateLimiter:
|
|
|
235
181
|
used: Used tokens reported by exchange (remaining = capacity - used)
|
|
236
182
|
capacity: Total capacity reported by exchange (overrides config if provided)
|
|
237
183
|
"""
|
|
238
|
-
pool = self.
|
|
184
|
+
pool = self._pools.get(pool_name)
|
|
239
185
|
if pool is None:
|
|
240
186
|
return
|
|
241
187
|
|
|
242
|
-
actual_capacity = capacity or pool.capacity
|
|
188
|
+
actual_capacity = capacity or pool.config.capacity
|
|
243
189
|
|
|
244
190
|
if remaining is not None:
|
|
245
191
|
actual_remaining = remaining
|
|
@@ -248,77 +194,25 @@ class ExchangeRateLimiter:
|
|
|
248
194
|
else:
|
|
249
195
|
return
|
|
250
196
|
|
|
251
|
-
|
|
252
|
-
self._quota_remaining[pool_name] = actual_remaining
|
|
253
|
-
if actual_remaining <= 0:
|
|
254
|
-
self._close_gate(pool_name, pool.cooldown, f"exchange reports {pool_name} depleted")
|
|
255
|
-
return
|
|
256
|
-
|
|
257
|
-
# For rate pools, update backend
|
|
258
|
-
key = self._key_for(pool)
|
|
259
|
-
# Fire-and-forget since set_remaining may be async but we're in sync context
|
|
260
|
-
# Use a helper to run it
|
|
261
|
-
try:
|
|
262
|
-
loop = asyncio.get_running_loop()
|
|
263
|
-
loop.create_task(self._backend.set_remaining(key, actual_remaining))
|
|
264
|
-
except RuntimeError:
|
|
265
|
-
pass # No running loop — skip sync (e.g., during shutdown)
|
|
266
|
-
|
|
267
|
-
def sync_quota(self, pool_name: str, remaining: float) -> None:
|
|
268
|
-
"""Update externally-managed quota pool from exchange response.
|
|
197
|
+
pool.sync(actual_remaining, capacity)
|
|
269
198
|
|
|
270
|
-
|
|
271
|
-
|
|
199
|
+
def get_quota_remaining(self, pool_name: str) -> float:
|
|
200
|
+
"""Get remaining budget for a quota pool.
|
|
272
201
|
|
|
273
|
-
|
|
274
|
-
pool_name: Quota pool name
|
|
275
|
-
remaining: Remaining quota reported by exchange
|
|
202
|
+
Returns 0 if pool doesn't exist or isn't a quota pool.
|
|
276
203
|
"""
|
|
277
|
-
self.
|
|
278
|
-
if
|
|
279
|
-
pool
|
|
280
|
-
|
|
281
|
-
self._close_gate(pool_name, cooldown, f"quota {pool_name} depleted (remaining={remaining})")
|
|
282
|
-
|
|
283
|
-
def _close_gate(self, pool_name: str, cooldown: float, reason: str) -> None:
|
|
284
|
-
"""Close a pool's gate for the given cooldown period."""
|
|
285
|
-
gate = self._gates.get(pool_name)
|
|
286
|
-
if gate is None:
|
|
287
|
-
return
|
|
288
|
-
|
|
289
|
-
verb = "extended" if not gate.is_set() else "closed"
|
|
290
|
-
logger.warning(f"Rate limit gate {verb} for {self._exchange}:{pool_name} ({cooldown:.1f}s): {reason}")
|
|
291
|
-
gate.clear()
|
|
292
|
-
|
|
293
|
-
# Cancel existing reopen task
|
|
294
|
-
old_task = self._gate_tasks.get(pool_name)
|
|
295
|
-
if old_task is not None and not old_task.done():
|
|
296
|
-
old_task.cancel()
|
|
297
|
-
|
|
298
|
-
self._gate_tasks[pool_name] = asyncio.ensure_future(self._reopen_gate_after(pool_name, cooldown))
|
|
299
|
-
|
|
300
|
-
async def _reopen_gate_after(self, pool_name: str, delay: float) -> None:
|
|
301
|
-
"""Reopen a pool's gate after a cooldown delay."""
|
|
302
|
-
try:
|
|
303
|
-
await asyncio.sleep(delay)
|
|
304
|
-
gate = self._gates.get(pool_name)
|
|
305
|
-
if gate is not None:
|
|
306
|
-
gate.set()
|
|
307
|
-
logger.info(f"Rate limit gate reopened for {self._exchange}:{pool_name} after {delay:.1f}s")
|
|
308
|
-
except asyncio.CancelledError:
|
|
309
|
-
pass
|
|
204
|
+
pool = self._pools.get(pool_name)
|
|
205
|
+
if isinstance(pool, QuotaPool):
|
|
206
|
+
return pool.remaining
|
|
207
|
+
return 0
|
|
310
208
|
|
|
311
209
|
def reset_gates(self) -> None:
|
|
312
210
|
"""Reopen all gates and cancel pending reopen tasks.
|
|
313
211
|
|
|
314
212
|
Call on connection reset / reconnection.
|
|
315
213
|
"""
|
|
316
|
-
for
|
|
317
|
-
|
|
318
|
-
for pool_name, task in self._gate_tasks.items():
|
|
319
|
-
if not task.done():
|
|
320
|
-
task.cancel()
|
|
321
|
-
self._gate_tasks.clear()
|
|
214
|
+
for pool in self._pools.values():
|
|
215
|
+
pool.reset_gate()
|
|
322
216
|
|
|
323
217
|
def is_gate_closed(self, pool_name: str | None = None) -> bool:
|
|
324
218
|
"""Check if a gate is closed.
|
|
@@ -327,9 +221,9 @@ class ExchangeRateLimiter:
|
|
|
327
221
|
pool_name: Specific pool, or None to check if ANY gate is closed
|
|
328
222
|
"""
|
|
329
223
|
if pool_name:
|
|
330
|
-
|
|
331
|
-
return
|
|
332
|
-
return any(
|
|
224
|
+
pool = self._pools.get(pool_name)
|
|
225
|
+
return pool is not None and pool.is_gate_closed
|
|
226
|
+
return any(pool.is_gate_closed for pool in self._pools.values())
|
|
333
227
|
|
|
334
228
|
async def get_pool_state(self, pool_name: str) -> dict[str, Any] | None:
|
|
335
229
|
"""Get current state of a pool for monitoring.
|
|
@@ -337,32 +231,10 @@ class ExchangeRateLimiter:
|
|
|
337
231
|
Returns:
|
|
338
232
|
Dict with remaining, capacity, gate_closed, hits, etc. or None if pool doesn't exist
|
|
339
233
|
"""
|
|
340
|
-
pool = self.
|
|
234
|
+
pool = self._pools.get(pool_name)
|
|
341
235
|
if pool is None:
|
|
342
236
|
return None
|
|
343
|
-
|
|
344
|
-
if pool.pool_type == "quota":
|
|
345
|
-
remaining = self._quota_remaining.get(pool_name, 0)
|
|
346
|
-
else:
|
|
347
|
-
key = self._key_for(pool)
|
|
348
|
-
remaining = await self._backend.get_remaining(key, pool.capacity, pool.refill_rate)
|
|
349
|
-
if remaining is None:
|
|
350
|
-
remaining = pool.capacity
|
|
351
|
-
|
|
352
|
-
return {
|
|
353
|
-
"pool": pool_name,
|
|
354
|
-
"exchange": self._exchange,
|
|
355
|
-
"scope": pool.scope,
|
|
356
|
-
"scope_id": self._scope_ids.get(pool.scope, "local"),
|
|
357
|
-
"pool_type": pool.pool_type,
|
|
358
|
-
"remaining": remaining,
|
|
359
|
-
"capacity": pool.capacity,
|
|
360
|
-
"utilization": 1.0 - (remaining / pool.capacity) if pool.capacity > 0 else 0,
|
|
361
|
-
"gate_closed": self.is_gate_closed(pool_name),
|
|
362
|
-
"hits": self._hits.get(pool_name, 0),
|
|
363
|
-
"total_wait_s": self._total_wait.get(pool_name, 0),
|
|
364
|
-
"consumed": self._consumed.get(pool_name, 0),
|
|
365
|
-
}
|
|
237
|
+
return await pool.get_state()
|
|
366
238
|
|
|
367
239
|
async def collect_metrics(self) -> list[dict[str, Any]]:
|
|
368
240
|
"""Collect current metrics for all pools.
|
|
@@ -374,7 +246,7 @@ class ExchangeRateLimiter:
|
|
|
374
246
|
ctx.emitter.emit(m["name"], m["value"], m["tags"])
|
|
375
247
|
"""
|
|
376
248
|
metrics = []
|
|
377
|
-
for pool_name in self.
|
|
249
|
+
for pool_name in self._pools:
|
|
378
250
|
state = await self.get_pool_state(pool_name)
|
|
379
251
|
if state is None:
|
|
380
252
|
continue
|
|
@@ -399,5 +271,5 @@ class ExchangeRateLimiter:
|
|
|
399
271
|
return metrics
|
|
400
272
|
|
|
401
273
|
def __repr__(self) -> str:
|
|
402
|
-
pools = ", ".join(f"{name}({pool.scope})" for name, pool in self.
|
|
274
|
+
pools = ", ".join(f"{name}({pool.config.scope})" for name, pool in self._pools.items())
|
|
403
275
|
return f"ExchangeRateLimiter({self._exchange}, pools=[{pools}])"
|