Qubx 1.1.3.dev10__tar.gz → 1.1.3.dev12__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.1.3.dev10 → qubx-1.1.3.dev12}/PKG-INFO +1 -1
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/_version.py +2 -2
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/connection_manager.py +22 -1
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchange_manager.py +39 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/factory.py +5 -0
- qubx-1.1.3.dev12/src/qubx/connectors/ccxt/rate_limits.py +277 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/context.py +32 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/interfaces.py +5 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/mixins/processing.py +41 -0
- qubx-1.1.3.dev12/src/qubx/rate_limiting/__init__.py +32 -0
- qubx-1.1.3.dev12/src/qubx/rate_limiting/backend.py +83 -0
- qubx-1.1.3.dev12/src/qubx/rate_limiting/config.py +69 -0
- qubx-1.1.3.dev12/src/qubx/rate_limiting/engine.py +392 -0
- qubx-1.1.3.dev12/src/qubx/rate_limiting/ip_resolver.py +98 -0
- qubx-1.1.3.dev12/src/qubx/rate_limiting/redis_backend.py +129 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/configs.py +11 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/factory.py +28 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/runner.py +51 -4
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/.gitignore +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/LICENSE +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/README.md +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/pyproject.toml +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/scripts/build.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/_nb_magic.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/account.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/broker.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/data.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/management.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/ome.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/optimization.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/runner.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/simulator.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/transfers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/backtester/utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/commands.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/deploy.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/misc.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/release.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/resolver.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/s3.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/theme.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/tui.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/cli/user_config.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/config.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/okx/account.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/registry.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/control/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/control/builtin.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/control/decorator.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/control/executor.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/control/interfaces.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/control/server.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/control/types.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/account.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/basics.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/errors.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/exceptions.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/helpers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/initializer.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/loggers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/lookups.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/metrics.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/mixins/market.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/series.pxd +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/series.pyi +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/series.pyx +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/utils.pyi +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/core/utils.pyx +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/cache.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/containers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/guards.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/registry.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/storage.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/storages/ccxt.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/storages/csv.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/storages/handy.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/storages/multi.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/storages/stub.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/storages/utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/data/transformers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/emitters/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/emitters/base.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/emitters/composite.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/emitters/csv.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/emitters/indicator.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/emitters/questdb.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/composite.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/exporters/slack.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/gathering/simplest.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/health/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/health/base.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/health/dummy.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/loggers/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/loggers/csv.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/loggers/factory.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/loggers/mongo.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/loggers/postgres.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/notifications/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/notifications/composite.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/notifications/slack.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/notifications/throttler.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/pandaz/stats.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/pandaz/ta.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/pandaz/utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/plugins/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/plugins/loader.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/_build.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restarts/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restorers/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restorers/balance.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restorers/factory.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restorers/position.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restorers/signal.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restorers/state.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/restorers/utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/state/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/state/dummy.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/state/redis.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/ta/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/base.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/project/template.yml +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.gitignore.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.python-version +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.vscode/launch.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.vscode/settings.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/README.md.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/configs/.gitkeep +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/justfile.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/research/.gitkeep +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/template.yml +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/trackers/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/trackers/advanced.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/trackers/composite.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/trackers/sizers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/collections.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/misc.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/nonce.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/ntp.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/orderbook.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/questdb.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/results.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/s3.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/slack.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/throttler.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/time.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-1.1.3.dev10 → qubx-1.1.3.dev12}/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.1.3.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 1, 3, '
|
|
21
|
+
__version__ = version = '1.1.3.dev12'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 1, 3, 'dev12')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -12,7 +12,16 @@ from asyncio.exceptions import CancelledError
|
|
|
12
12
|
from collections import defaultdict
|
|
13
13
|
from typing import Awaitable, Callable
|
|
14
14
|
|
|
15
|
-
from ccxt import
|
|
15
|
+
from ccxt import (
|
|
16
|
+
BadSymbol,
|
|
17
|
+
DDoSProtection,
|
|
18
|
+
ExchangeClosedByUser,
|
|
19
|
+
ExchangeError,
|
|
20
|
+
ExchangeNotAvailable,
|
|
21
|
+
NetworkError,
|
|
22
|
+
RateLimitExceeded,
|
|
23
|
+
UnsubscribeError,
|
|
24
|
+
)
|
|
16
25
|
from ccxt.async_support.base.ws.client import Client as _WsClient
|
|
17
26
|
from ccxt.pro import Exchange
|
|
18
27
|
|
|
@@ -145,6 +154,18 @@ class ConnectionManager:
|
|
|
145
154
|
)
|
|
146
155
|
await asyncio.sleep(1)
|
|
147
156
|
continue
|
|
157
|
+
except (RateLimitExceeded, DDoSProtection) as e:
|
|
158
|
+
# Rate limit hit — report to rate limiter if available and backoff
|
|
159
|
+
logger.warning(
|
|
160
|
+
f"<yellow>{self._exchange_id}</yellow> Rate limited in {stream_name}: {e}"
|
|
161
|
+
)
|
|
162
|
+
if hasattr(self, "_exchange_manager") and self._exchange_manager.rate_limiter:
|
|
163
|
+
self._exchange_manager.rate_limiter.report_limit_hit(
|
|
164
|
+
pool_name="ccxt_rest", reason=f"{e.__class__.__name__}: {e}"
|
|
165
|
+
)
|
|
166
|
+
await asyncio.sleep(min(2 ** (n_retry + 1), 60))
|
|
167
|
+
n_retry += 1
|
|
168
|
+
continue
|
|
148
169
|
except (NetworkError, ExchangeError, ExchangeNotAvailable) as e:
|
|
149
170
|
# Network/exchange errors - retry after short delay
|
|
150
171
|
logger.error(
|
|
@@ -254,6 +254,45 @@ class ExchangeManager:
|
|
|
254
254
|
# === Exchange Property Access ===
|
|
255
255
|
# Explicit property to access underlying CCXT exchange
|
|
256
256
|
|
|
257
|
+
# === Rate Limiting ===
|
|
258
|
+
|
|
259
|
+
def attach_rate_limiter(self, rate_limiter) -> None:
|
|
260
|
+
"""Attach an ExchangeRateLimiter to this exchange manager.
|
|
261
|
+
|
|
262
|
+
Disables CCXT's built-in rate limiting and replaces the throttle
|
|
263
|
+
method with our rate limiter. Survives exchange recreation.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
rate_limiter: ExchangeRateLimiter instance
|
|
267
|
+
"""
|
|
268
|
+
self._rate_limiter = rate_limiter
|
|
269
|
+
self._apply_rate_limiter_to_exchange(self._exchange)
|
|
270
|
+
self.register_recreation_callback(lambda: self._apply_rate_limiter_to_exchange(self._exchange))
|
|
271
|
+
logger.info(f"Rate limiter attached to {self._exchange_name}")
|
|
272
|
+
|
|
273
|
+
def _apply_rate_limiter_to_exchange(self, exchange: cxp.Exchange) -> None:
|
|
274
|
+
"""Disable CCXT's built-in rate limiter and install our throttle override."""
|
|
275
|
+
rate_limiter = getattr(self, "_rate_limiter", None)
|
|
276
|
+
if rate_limiter is None:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
exchange.enableRateLimit = False
|
|
280
|
+
|
|
281
|
+
# Override the throttle method to use our rate limiter
|
|
282
|
+
# CCXT calls self.throttle(cost) in fetch2() before every REST request
|
|
283
|
+
# cost comes from calculateRateLimiterCost() which uses per-endpoint weights
|
|
284
|
+
async def _rate_limit_throttle(cost=None):
|
|
285
|
+
if cost is None:
|
|
286
|
+
cost = 1.0
|
|
287
|
+
await rate_limiter.acquire("ccxt_rest", weight_override=float(cost))
|
|
288
|
+
|
|
289
|
+
exchange.throttle = _rate_limit_throttle
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def rate_limiter(self):
|
|
293
|
+
"""Access the attached rate limiter (if any)."""
|
|
294
|
+
return getattr(self, "_rate_limiter", None)
|
|
295
|
+
|
|
257
296
|
@property
|
|
258
297
|
def exchange(self) -> cxp.Exchange:
|
|
259
298
|
"""Access to the underlying CCXT exchange instance.
|
|
@@ -129,6 +129,11 @@ def get_ccxt_exchange_manager(
|
|
|
129
129
|
check_interval_seconds=check_interval_seconds,
|
|
130
130
|
)
|
|
131
131
|
|
|
132
|
+
# Attach rate limiter if provided in kwargs
|
|
133
|
+
rate_limiter = kwargs.pop("rate_limiter", None)
|
|
134
|
+
if rate_limiter is not None:
|
|
135
|
+
manager.attach_rate_limiter(rate_limiter)
|
|
136
|
+
|
|
132
137
|
_exchange_manager_cache[cache_key] = manager
|
|
133
138
|
return manager
|
|
134
139
|
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCXT exchange rate limit configurations.
|
|
3
|
+
|
|
4
|
+
Provides default rate limit configs per exchange, differentiated by market type
|
|
5
|
+
(spot vs futures). Also includes response header parsers for syncing modeled
|
|
6
|
+
state with exchange-reported usage.
|
|
7
|
+
|
|
8
|
+
References:
|
|
9
|
+
- Binance: https://binance-docs.github.io/apidocs/spot/en/#limits
|
|
10
|
+
- Binance Futures: https://binance-docs.github.io/apidocs/futures/en/#limits
|
|
11
|
+
- OKX: https://www.okx.com/docs-v5/en/#overview-rate-limit
|
|
12
|
+
- Kraken: https://docs.kraken.com/api/docs/guides/rate-limits
|
|
13
|
+
- Bybit: https://bybit-exchange.github.io/docs/v5/rate-limit
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from qubx.rate_limiting import EndpointCosts, ExchangeRateLimitConfig, PoolConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_ccxt_rate_limit_config(exchange_name: str, ccxt_exchange=None) -> ExchangeRateLimitConfig:
|
|
20
|
+
"""Create rate limit config for a CCXT exchange.
|
|
21
|
+
|
|
22
|
+
Handles exchange.market_type differentiation (spot vs futures).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
exchange_name: Exchange name (e.g., "binance", "binance.um", "kraken")
|
|
26
|
+
ccxt_exchange: Optional CCXT exchange instance for auto-detection
|
|
27
|
+
"""
|
|
28
|
+
name = exchange_name.lower()
|
|
29
|
+
base = name.split(".")[0]
|
|
30
|
+
|
|
31
|
+
if base in ("binance", "binanceusdm", "binancecoinm"):
|
|
32
|
+
return _binance_config(name)
|
|
33
|
+
elif base == "okx":
|
|
34
|
+
return _okx_config()
|
|
35
|
+
elif base == "bybit":
|
|
36
|
+
return _bybit_config()
|
|
37
|
+
elif base in ("kraken", "krakenfutures"):
|
|
38
|
+
return _kraken_config(name)
|
|
39
|
+
else:
|
|
40
|
+
return _default_config(exchange_name, ccxt_exchange)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# === Binance ===
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _binance_config(exchange_name: str) -> ExchangeRateLimitConfig:
|
|
47
|
+
"""Binance rate limits differentiated by market type.
|
|
48
|
+
|
|
49
|
+
Spot:
|
|
50
|
+
- IP weight: 6000/min (recently increased from 1200)
|
|
51
|
+
- UID weight: 180,000/min
|
|
52
|
+
- Orders: 10/sec, 200,000/day per account
|
|
53
|
+
|
|
54
|
+
USDM Futures (binance.um):
|
|
55
|
+
- IP weight: 2400/min
|
|
56
|
+
- Orders: 300/10sec per account
|
|
57
|
+
|
|
58
|
+
COIN-M Futures (binance.cm):
|
|
59
|
+
- IP weight: 6000/min
|
|
60
|
+
- Orders: 300/10sec per account
|
|
61
|
+
|
|
62
|
+
Headers: X-MBX-USED-WEIGHT-1M, X-MBX-ORDER-COUNT-1S, X-MBX-ORDER-COUNT-1D
|
|
63
|
+
"""
|
|
64
|
+
if exchange_name in ("binance.um", "binanceusdm"):
|
|
65
|
+
# USDM Futures
|
|
66
|
+
return ExchangeRateLimitConfig(
|
|
67
|
+
pools={
|
|
68
|
+
"ccxt_rest": PoolConfig("ccxt_rest", "ip", 2400, 40.0, cooldown=30.0),
|
|
69
|
+
"orders": PoolConfig("orders", "account", 300, 30.0, cooldown=10.0),
|
|
70
|
+
},
|
|
71
|
+
endpoint_map={"ccxt_rest": EndpointCosts([("ccxt_rest", 1)])},
|
|
72
|
+
default_costs=EndpointCosts([("ccxt_rest", 1)]),
|
|
73
|
+
)
|
|
74
|
+
elif exchange_name in ("binance.cm", "binancecoinm"):
|
|
75
|
+
# COIN-M Futures
|
|
76
|
+
return ExchangeRateLimitConfig(
|
|
77
|
+
pools={
|
|
78
|
+
"ccxt_rest": PoolConfig("ccxt_rest", "ip", 6000, 100.0, cooldown=30.0),
|
|
79
|
+
"orders": PoolConfig("orders", "account", 300, 30.0, cooldown=10.0),
|
|
80
|
+
},
|
|
81
|
+
endpoint_map={"ccxt_rest": EndpointCosts([("ccxt_rest", 1)])},
|
|
82
|
+
default_costs=EndpointCosts([("ccxt_rest", 1)]),
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
# Spot
|
|
86
|
+
return ExchangeRateLimitConfig(
|
|
87
|
+
pools={
|
|
88
|
+
"ccxt_rest": PoolConfig("ccxt_rest", "ip", 6000, 100.0, cooldown=30.0),
|
|
89
|
+
"orders": PoolConfig("orders", "account", 10, 10.0, cooldown=10.0),
|
|
90
|
+
},
|
|
91
|
+
endpoint_map={"ccxt_rest": EndpointCosts([("ccxt_rest", 1)])},
|
|
92
|
+
default_costs=EndpointCosts([("ccxt_rest", 1)]),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# === OKX ===
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _okx_config() -> ExchangeRateLimitConfig:
|
|
100
|
+
"""OKX rate limits (unified across spot/futures).
|
|
101
|
+
|
|
102
|
+
- REST: varies heavily by endpoint (2-60 req/2sec per endpoint)
|
|
103
|
+
- Trade endpoints: 60/2sec per account
|
|
104
|
+
- Market data: 20/2sec per IP
|
|
105
|
+
|
|
106
|
+
Headers: x-ratelimit-remaining, x-ratelimit-limit, x-ratelimit-reset
|
|
107
|
+
"""
|
|
108
|
+
return ExchangeRateLimitConfig(
|
|
109
|
+
pools={
|
|
110
|
+
"ccxt_rest": PoolConfig("ccxt_rest", "ip", 20, 10.0, cooldown=15.0),
|
|
111
|
+
"orders": PoolConfig("orders", "account", 60, 30.0, cooldown=10.0),
|
|
112
|
+
},
|
|
113
|
+
endpoint_map={"ccxt_rest": EndpointCosts([("ccxt_rest", 1)])},
|
|
114
|
+
default_costs=EndpointCosts([("ccxt_rest", 1)]),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# === Bybit ===
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _bybit_config() -> ExchangeRateLimitConfig:
|
|
122
|
+
"""Bybit rate limits (unified v5 API).
|
|
123
|
+
|
|
124
|
+
- REST: 10 req/sec per endpoint per IP (varies by endpoint)
|
|
125
|
+
- Orders: 10/sec per account
|
|
126
|
+
- Rate limit info in response body: rate_limit_status, rate_limit_reset_ms
|
|
127
|
+
"""
|
|
128
|
+
return ExchangeRateLimitConfig(
|
|
129
|
+
pools={
|
|
130
|
+
"ccxt_rest": PoolConfig("ccxt_rest", "ip", 120, 24.0, cooldown=15.0),
|
|
131
|
+
"orders": PoolConfig("orders", "account", 10, 10.0, cooldown=10.0),
|
|
132
|
+
},
|
|
133
|
+
endpoint_map={"ccxt_rest": EndpointCosts([("ccxt_rest", 1)])},
|
|
134
|
+
default_costs=EndpointCosts([("ccxt_rest", 1)]),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# === Kraken ===
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _kraken_config(exchange_name: str) -> ExchangeRateLimitConfig:
|
|
142
|
+
"""Kraken rate limits.
|
|
143
|
+
|
|
144
|
+
Kraken Spot uses a unique "call counter" system:
|
|
145
|
+
- Each API call adds to a counter (1-6 depending on endpoint)
|
|
146
|
+
- Counter decays at a fixed rate based on verification level
|
|
147
|
+
- Intermediate verified: max counter 20, decay 0.33/sec
|
|
148
|
+
- Pro verified: max counter 20, decay 1.0/sec
|
|
149
|
+
|
|
150
|
+
Per-endpoint costs from CCXT:
|
|
151
|
+
- Public: 1.0-1.2 per call
|
|
152
|
+
- Private: 0-6 per call (orders=0, balance=3, ledgers=6)
|
|
153
|
+
- Orders: cost 0 (separate matching engine limit)
|
|
154
|
+
|
|
155
|
+
Kraken Futures:
|
|
156
|
+
- Simple: ~1.67 req/sec (600ms rateLimit)
|
|
157
|
+
- No per-endpoint weights documented
|
|
158
|
+
|
|
159
|
+
Matching engine (order) limits (separate from REST):
|
|
160
|
+
- Spot: based on tier, typically 60/min for intermediate
|
|
161
|
+
- Futures: separate per-instrument limits
|
|
162
|
+
"""
|
|
163
|
+
if exchange_name in ("krakenfutures",):
|
|
164
|
+
return ExchangeRateLimitConfig(
|
|
165
|
+
pools={
|
|
166
|
+
"ccxt_rest": PoolConfig("ccxt_rest", "ip", 10, 1.67, cooldown=15.0),
|
|
167
|
+
},
|
|
168
|
+
endpoint_map={"ccxt_rest": EndpointCosts([("ccxt_rest", 1)])},
|
|
169
|
+
default_costs=EndpointCosts([("ccxt_rest", 1)]),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Kraken Spot — counter-decay system maps to token bucket:
|
|
173
|
+
# capacity=20 (max counter), refill=0.33/sec (intermediate) or 1.0/sec (pro)
|
|
174
|
+
# Using intermediate as default — can be overridden via config
|
|
175
|
+
return ExchangeRateLimitConfig(
|
|
176
|
+
pools={
|
|
177
|
+
# Main REST counter (per IP, decays at 0.33/sec for intermediate tier)
|
|
178
|
+
"ccxt_rest": PoolConfig("ccxt_rest", "ip", 20, 0.33, cooldown=30.0),
|
|
179
|
+
# Order matching engine (separate limit per account)
|
|
180
|
+
"orders": PoolConfig("orders", "account", 60, 1.0, cooldown=15.0),
|
|
181
|
+
},
|
|
182
|
+
endpoint_map={
|
|
183
|
+
# CCXT defines per-endpoint costs for Kraken — these are the weights
|
|
184
|
+
# that get passed through CCXT's calculateRateLimiterCost → our throttle override.
|
|
185
|
+
# Key costs: Balance=3, ClosedOrders=3, TradesHistory=6, Ledgers=6
|
|
186
|
+
# Orders have cost=0 in the REST counter (separate matching engine limit)
|
|
187
|
+
"ccxt_rest": EndpointCosts([("ccxt_rest", 1)]),
|
|
188
|
+
},
|
|
189
|
+
default_costs=EndpointCosts([("ccxt_rest", 1)]),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# === Default ===
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _default_config(exchange_name: str, ccxt_exchange=None) -> ExchangeRateLimitConfig:
|
|
197
|
+
"""Default conservative config for unknown exchanges.
|
|
198
|
+
|
|
199
|
+
Derives from CCXT's rateLimit property if exchange instance provided.
|
|
200
|
+
"""
|
|
201
|
+
rate_limit_ms = 50 # default: 20 req/sec
|
|
202
|
+
if ccxt_exchange and hasattr(ccxt_exchange, "rateLimit"):
|
|
203
|
+
rate_limit_ms = ccxt_exchange.rateLimit or 50
|
|
204
|
+
|
|
205
|
+
rps = 1000.0 / rate_limit_ms
|
|
206
|
+
capacity = rps * 60 # 1 minute capacity
|
|
207
|
+
|
|
208
|
+
return ExchangeRateLimitConfig(
|
|
209
|
+
pools={
|
|
210
|
+
"ccxt_rest": PoolConfig("ccxt_rest", "ip", capacity, rps, cooldown=15.0),
|
|
211
|
+
},
|
|
212
|
+
endpoint_map={"ccxt_rest": EndpointCosts([("ccxt_rest", 1)])},
|
|
213
|
+
default_costs=EndpointCosts([("ccxt_rest", 1)]),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# === Response Header Parsers ===
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def parse_binance_headers(headers: dict, rate_limiter) -> None:
|
|
221
|
+
"""Parse Binance rate limit headers and sync with rate limiter.
|
|
222
|
+
|
|
223
|
+
Binance returns:
|
|
224
|
+
- X-MBX-USED-WEIGHT-1M: weight used in current 1-minute window
|
|
225
|
+
- X-MBX-ORDER-COUNT-1S: orders placed in current 1-second window
|
|
226
|
+
- X-MBX-ORDER-COUNT-1D: orders placed in current day
|
|
227
|
+
"""
|
|
228
|
+
if rate_limiter is None:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
# Try both casing conventions (CCXT normalizes to lowercase)
|
|
232
|
+
used_weight = headers.get("x-mbx-used-weight-1m") or headers.get("X-MBX-USED-WEIGHT-1M")
|
|
233
|
+
if used_weight:
|
|
234
|
+
try:
|
|
235
|
+
rate_limiter.sync_from_exchange("ccxt_rest", used=int(used_weight))
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
order_count = headers.get("x-mbx-order-count-1s") or headers.get("X-MBX-ORDER-COUNT-1S")
|
|
240
|
+
if order_count and "orders" in rate_limiter.config.pools:
|
|
241
|
+
try:
|
|
242
|
+
rate_limiter.sync_from_exchange("orders", used=int(order_count))
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def parse_okx_headers(headers: dict, rate_limiter) -> None:
|
|
248
|
+
"""Parse OKX rate limit headers and sync with rate limiter.
|
|
249
|
+
|
|
250
|
+
OKX returns:
|
|
251
|
+
- x-ratelimit-remaining: remaining requests for this endpoint
|
|
252
|
+
- x-ratelimit-limit: total limit for this endpoint
|
|
253
|
+
- x-ratelimit-reset: timestamp when limit resets (ms)
|
|
254
|
+
"""
|
|
255
|
+
if rate_limiter is None:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
remaining = headers.get("x-ratelimit-remaining") or headers.get("X-RateLimit-Remaining")
|
|
259
|
+
if remaining:
|
|
260
|
+
try:
|
|
261
|
+
rate_limiter.sync_from_exchange("ccxt_rest", remaining=float(remaining))
|
|
262
|
+
except Exception:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
HEADER_PARSERS = {
|
|
267
|
+
"binance": parse_binance_headers,
|
|
268
|
+
"binanceusdm": parse_binance_headers,
|
|
269
|
+
"binancecoinm": parse_binance_headers,
|
|
270
|
+
"okx": parse_okx_headers,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_header_parser(exchange_name: str):
|
|
275
|
+
"""Get the response header parser for an exchange."""
|
|
276
|
+
name = exchange_name.lower().split(".")[0]
|
|
277
|
+
return HEADER_PARSERS.get(name)
|
|
@@ -159,6 +159,9 @@ class StrategyContext(IStrategyContext):
|
|
|
159
159
|
data_throttler: "InstrumentThrottler | None" = None,
|
|
160
160
|
state_persistence: IStatePersistence | None = None,
|
|
161
161
|
state_snapshot_interval: str | None = None,
|
|
162
|
+
rate_limit_backend: Any | None = None,
|
|
163
|
+
rate_limiting_config: Any | None = None,
|
|
164
|
+
event_loop: "asyncio.AbstractEventLoop | None" = None,
|
|
162
165
|
) -> None:
|
|
163
166
|
self.account = account
|
|
164
167
|
self.strategy = self.__instantiate_strategy(strategy, config)
|
|
@@ -190,6 +193,9 @@ class StrategyContext(IStrategyContext):
|
|
|
190
193
|
self.health = self._health_monitor
|
|
191
194
|
self._state_persistence = state_persistence or DummyStatePersistence()
|
|
192
195
|
self._state_snapshot_interval = state_snapshot_interval
|
|
196
|
+
self._rate_limit_backend = rate_limit_backend
|
|
197
|
+
self._rate_limiting_config = rate_limiting_config
|
|
198
|
+
self.event_loop = event_loop
|
|
193
199
|
|
|
194
200
|
# Initialize shutdown handling
|
|
195
201
|
self._stop_lock = Lock()
|
|
@@ -308,6 +314,16 @@ class StrategyContext(IStrategyContext):
|
|
|
308
314
|
# Configure periodic state snapshot persistence
|
|
309
315
|
self._processing_manager.configure_state_snapshot(self._state_snapshot_interval)
|
|
310
316
|
|
|
317
|
+
# Collect rate limiters from data providers
|
|
318
|
+
for dp in self._data_providers:
|
|
319
|
+
rl = dp.rate_limiter
|
|
320
|
+
if rl is not None:
|
|
321
|
+
self.rate_limiters[rl.exchange] = rl
|
|
322
|
+
|
|
323
|
+
# Configure periodic rate limit metric emission
|
|
324
|
+
_rl_metrics_interval = self._rate_limiting_config.metrics_interval if self._rate_limiting_config else None
|
|
325
|
+
self._processing_manager.configure_rate_limit_metrics(_rl_metrics_interval)
|
|
326
|
+
|
|
311
327
|
if self.is_simulation and isinstance(self.account, CompositeAccountProcessor):
|
|
312
328
|
# Auto-assign simulation transfer manager
|
|
313
329
|
from qubx.backtester.transfers import SimulationTransferManager
|
|
@@ -573,6 +589,22 @@ class StrategyContext(IStrategyContext):
|
|
|
573
589
|
def persistence(self) -> IStatePersistence:
|
|
574
590
|
return self._state_persistence
|
|
575
591
|
|
|
592
|
+
@property
|
|
593
|
+
def rate_limiters(self) -> dict:
|
|
594
|
+
"""Registry of ExchangeRateLimiter instances by exchange name.
|
|
595
|
+
|
|
596
|
+
Connectors register their rate limiters here at startup.
|
|
597
|
+
Used by the processing mixin for periodic metric emission.
|
|
598
|
+
"""
|
|
599
|
+
if not hasattr(self, "_rate_limiters"):
|
|
600
|
+
self._rate_limiters: dict = {}
|
|
601
|
+
return self._rate_limiters
|
|
602
|
+
|
|
603
|
+
@property
|
|
604
|
+
def rate_limit_backend(self):
|
|
605
|
+
"""Rate limit storage backend (InMemoryBackend or RedisBackend)."""
|
|
606
|
+
return self._rate_limit_backend
|
|
607
|
+
|
|
576
608
|
# IAccountViewer delegation
|
|
577
609
|
|
|
578
610
|
# capital information
|
|
@@ -831,6 +831,11 @@ class IDataProvider:
|
|
|
831
831
|
"""
|
|
832
832
|
raise NotImplementedError("exchange() is not implemented")
|
|
833
833
|
|
|
834
|
+
@property
|
|
835
|
+
def rate_limiter(self):
|
|
836
|
+
"""Return the ExchangeRateLimiter for this provider, if any."""
|
|
837
|
+
return None
|
|
838
|
+
|
|
834
839
|
def is_connected(self) -> bool:
|
|
835
840
|
"""
|
|
836
841
|
Check if the data provider is currently connected to the exchange.
|
|
@@ -293,6 +293,47 @@ class ProcessingManager(IProcessingManager):
|
|
|
293
293
|
self._scheduler.schedule_event(cron_schedule, "state_snapshot")
|
|
294
294
|
logger.info(f"State snapshot enabled with interval: {interval}")
|
|
295
295
|
|
|
296
|
+
def configure_rate_limit_metrics(self, interval: str | None) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Configure periodic rate limit metric emission.
|
|
299
|
+
|
|
300
|
+
Emits pool utilization, gate state, hit counts etc. for all registered
|
|
301
|
+
rate limiters via ctx.emitter.emit(). Only active in live mode.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
interval: Emission interval (e.g., "60s", "1m"). None to disable.
|
|
305
|
+
"""
|
|
306
|
+
self._scheduler.unschedule_event("rate_limit_metrics")
|
|
307
|
+
|
|
308
|
+
if interval is not None and not self._is_simulation:
|
|
309
|
+
cron_schedule = interval_to_cron(interval)
|
|
310
|
+
self._scheduler.schedule_event(cron_schedule, "rate_limit_metrics")
|
|
311
|
+
self._custom_scheduled_methods["rate_limit_metrics"] = self._emit_rate_limit_metrics
|
|
312
|
+
logger.info(f"Rate limit metrics emission enabled with interval: {interval}")
|
|
313
|
+
|
|
314
|
+
def _emit_rate_limit_metrics(self, ctx) -> None:
|
|
315
|
+
"""Emit rate limit metrics for all registered rate limiters."""
|
|
316
|
+
import asyncio
|
|
317
|
+
import concurrent.futures
|
|
318
|
+
|
|
319
|
+
if ctx.emitter is None or ctx.event_loop is None:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
for exchange_name, rate_limiter in ctx.rate_limiters.items():
|
|
323
|
+
try:
|
|
324
|
+
future = asyncio.run_coroutine_threadsafe(rate_limiter.collect_metrics(), ctx.event_loop)
|
|
325
|
+
metrics = future.result(timeout=5)
|
|
326
|
+
self._do_emit_metrics(ctx, metrics)
|
|
327
|
+
except concurrent.futures.TimeoutError:
|
|
328
|
+
logger.warning(f"Rate limit metrics collection timed out for {exchange_name}")
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.error(f"Failed to collect rate limit metrics for {exchange_name}: {e}")
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def _do_emit_metrics(ctx, metrics: list[dict]) -> None:
|
|
334
|
+
for m in metrics:
|
|
335
|
+
ctx.emitter.emit(m["name"], m["value"], m["tags"])
|
|
336
|
+
|
|
296
337
|
def process_data(self, instrument: Instrument, d_type: str, data: Any, is_historical: bool) -> bool:
|
|
297
338
|
should_stop = self.__process_data(instrument, d_type, data, is_historical)
|
|
298
339
|
if not is_historical:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exchange-aware rate limiting engine.
|
|
3
|
+
|
|
4
|
+
Provides a shared, multi-pool rate limiting system that connectors use
|
|
5
|
+
to comply with exchange API limits. Supports per-IP and per-account scoping,
|
|
6
|
+
gate mechanism for cooldowns, and optional Redis backend for cross-bot coordination.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
>>> from qubx.rate_limiting import ExchangeRateLimiter, ExchangeRateLimitConfig, PoolConfig, EndpointCosts
|
|
10
|
+
>>>
|
|
11
|
+
>>> config = ExchangeRateLimitConfig(
|
|
12
|
+
... pools={"rest": PoolConfig("rest", "ip", 1200, 20.0)},
|
|
13
|
+
... endpoint_map={"fetch_ohlcv": EndpointCosts([("rest", 5)])},
|
|
14
|
+
... default_costs=EndpointCosts([("rest", 1)]),
|
|
15
|
+
... )
|
|
16
|
+
>>> limiter = ExchangeRateLimiter("binance", config)
|
|
17
|
+
>>> await limiter.acquire("fetch_ohlcv")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from qubx.rate_limiting.backend import IRateLimitBackend, InMemoryBackend
|
|
21
|
+
from qubx.rate_limiting.config import EndpointCosts, ExchangeRateLimitConfig, PoolConfig
|
|
22
|
+
from qubx.rate_limiting.engine import ExchangeRateLimiter, RateLimitGateTimeout
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"EndpointCosts",
|
|
26
|
+
"ExchangeRateLimitConfig",
|
|
27
|
+
"ExchangeRateLimiter",
|
|
28
|
+
"IRateLimitBackend",
|
|
29
|
+
"InMemoryBackend",
|
|
30
|
+
"PoolConfig",
|
|
31
|
+
"RateLimitGateTimeout",
|
|
32
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate limit storage backends.
|
|
3
|
+
|
|
4
|
+
Provides the interface and in-memory implementation for token bucket state.
|
|
5
|
+
Redis backend can be added later for cross-bot coordination.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
|
|
11
|
+
from qubx.utils.rate_limiter import TokenBucketRateLimiter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IRateLimitBackend(ABC):
|
|
15
|
+
"""Interface for rate limit token storage.
|
|
16
|
+
|
|
17
|
+
Backends manage token bucket state — either in-process (InMemoryBackend)
|
|
18
|
+
or distributed (RedisBackend for cross-bot coordination).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
async def acquire(self, key: str, weight: float, capacity: float, refill_rate: float) -> float:
|
|
23
|
+
"""Acquire tokens from a bucket. Blocks until available.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
key: Unique bucket identifier (e.g., "ratelimit:binance:rest:ip_1.2.3.4")
|
|
27
|
+
weight: Tokens to consume
|
|
28
|
+
capacity: Bucket capacity (used to create bucket on first access)
|
|
29
|
+
refill_rate: Tokens per second (used to create bucket on first access)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Time spent waiting in seconds (0.0 if immediate)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def get_remaining(self, key: str) -> float | None:
|
|
37
|
+
"""Get remaining tokens for a key (non-blocking).
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Token count, or None if key doesn't exist
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
async def set_remaining(self, key: str, remaining: float) -> None:
|
|
45
|
+
"""Force-set remaining tokens (for syncing with exchange-reported state).
|
|
46
|
+
|
|
47
|
+
Used when exchange headers/responses tell us the actual remaining budget,
|
|
48
|
+
correcting any drift in our model.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InMemoryBackend(IRateLimitBackend):
|
|
53
|
+
"""In-process token bucket backend.
|
|
54
|
+
|
|
55
|
+
Each key gets its own TokenBucketRateLimiter instance.
|
|
56
|
+
Suitable for single-bot usage without cross-bot coordination.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self):
|
|
60
|
+
self._limiters: dict[str, TokenBucketRateLimiter] = {}
|
|
61
|
+
|
|
62
|
+
def _get_or_create(self, key: str, capacity: float, refill_rate: float) -> TokenBucketRateLimiter:
|
|
63
|
+
if key not in self._limiters:
|
|
64
|
+
self._limiters[key] = TokenBucketRateLimiter(capacity, refill_rate, name=key)
|
|
65
|
+
return self._limiters[key]
|
|
66
|
+
|
|
67
|
+
async def acquire(self, key: str, weight: float, capacity: float, refill_rate: float) -> float:
|
|
68
|
+
limiter = self._get_or_create(key, capacity, refill_rate)
|
|
69
|
+
start = time.monotonic()
|
|
70
|
+
await limiter.acquire(weight)
|
|
71
|
+
return time.monotonic() - start
|
|
72
|
+
|
|
73
|
+
async def get_remaining(self, key: str) -> float | None:
|
|
74
|
+
limiter = self._limiters.get(key)
|
|
75
|
+
if limiter is None:
|
|
76
|
+
return None
|
|
77
|
+
return limiter.get_available_tokens()
|
|
78
|
+
|
|
79
|
+
async def set_remaining(self, key: str, remaining: float) -> None:
|
|
80
|
+
limiter = self._limiters.get(key)
|
|
81
|
+
if limiter is not None:
|
|
82
|
+
limiter._tokens = remaining
|
|
83
|
+
limiter._last_refill = time.monotonic()
|