Qubx 1.1.3.dev13__tar.gz → 1.1.3.dev14__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.dev13 → qubx-1.1.3.dev14}/PKG-INFO +1 -1
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/_version.py +2 -2
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/__init__.py +1 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/data.py +6 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchange_manager.py +3 -5
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/factory.py +2 -7
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/rate_limits.py +2 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/registry.py +39 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/control/builtin.py +50 -26
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/context.py +0 -13
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/interfaces.py +0 -5
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/mixins/processing.py +1 -1
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/storages/ccxt.py +33 -4
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/rate_limiting/engine.py +12 -1
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/rate_limiting/ip_resolver.py +1 -1
- qubx-1.1.3.dev14/src/qubx/rate_limiting/manager.py +138 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/rate_limiting/redis_backend.py +10 -4
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/factory.py +0 -26
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/runner.py +15 -45
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/.gitignore +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/LICENSE +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/README.md +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/pyproject.toml +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/scripts/build.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/_nb_magic.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/account.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/broker.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/data.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/management.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/ome.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/optimization.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/runner.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/simulator.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/transfers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/backtester/utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/commands.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/deploy.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/misc.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/release.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/resolver.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/s3.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/theme.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/tui.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/cli/user_config.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/config.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/okx/account.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/control/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/control/decorator.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/control/executor.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/control/interfaces.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/control/server.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/control/types.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/account.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/basics.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/errors.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/exceptions.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/helpers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/initializer.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/loggers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/lookups.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/metrics.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/mixins/market.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/series.pxd +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/series.pyi +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/series.pyx +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/utils.pyi +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/core/utils.pyx +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/cache.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/containers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/guards.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/registry.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/storage.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/storages/csv.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/storages/handy.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/storages/multi.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/storages/stub.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/storages/utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/data/transformers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/emitters/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/emitters/base.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/emitters/composite.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/emitters/csv.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/emitters/indicator.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/emitters/questdb.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/composite.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/exporters/slack.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/gathering/simplest.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/health/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/health/base.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/health/dummy.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/loggers/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/loggers/csv.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/loggers/factory.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/loggers/mongo.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/loggers/postgres.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/notifications/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/notifications/composite.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/notifications/slack.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/notifications/throttler.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/pandaz/stats.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/pandaz/ta.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/pandaz/utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/plugins/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/plugins/loader.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/rate_limiting/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/rate_limiting/backend.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/rate_limiting/config.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/_build.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restarts/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restorers/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restorers/balance.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restorers/factory.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restorers/position.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restorers/signal.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restorers/state.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/restorers/utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/state/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/state/dummy.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/state/redis.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/ta/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/base.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/project/template.yml +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.gitignore.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.python-version +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.vscode/launch.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.vscode/settings.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/README.md.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/configs/.gitkeep +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/justfile.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/research/.gitkeep +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/template.yml +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/trackers/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/trackers/advanced.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/trackers/composite.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/trackers/sizers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/collections.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/misc.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/nonce.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/ntp.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/orderbook.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/questdb.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/results.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/s3.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/slack.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/throttler.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/time.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-1.1.3.dev13 → qubx-1.1.3.dev14}/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.dev14'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 1, 3, 'dev14')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -19,6 +19,7 @@ from qubx.connectors.registry import ( # noqa: F401
|
|
|
19
19
|
from qubx.connectors.ccxt.account import CcxtAccountProcessor # noqa: F401
|
|
20
20
|
from qubx.connectors.ccxt.broker import CcxtBroker # noqa: F401
|
|
21
21
|
from qubx.connectors.ccxt.data import CcxtDataProvider # noqa: F401
|
|
22
|
+
from qubx.connectors.ccxt.rate_limits import create_ccxt_rate_limit_config # noqa: F401
|
|
22
23
|
from qubx.connectors.tardis.data import TardisDataProvider # noqa: F401
|
|
23
24
|
|
|
24
25
|
# Note: Paper trading connectors (SimulatedAccountProcessor, SimulatedBroker) are
|
|
@@ -47,6 +47,8 @@ class CcxtDataProvider(IDataProvider):
|
|
|
47
47
|
):
|
|
48
48
|
from qubx.connectors.ccxt.factory import get_ccxt_exchange_manager
|
|
49
49
|
|
|
50
|
+
rate_limiter = kwargs.pop("rate_limiter", None)
|
|
51
|
+
|
|
50
52
|
settings = account_manager.get_exchange_settings(exchange_name)
|
|
51
53
|
self._exchange_manager = get_ccxt_exchange_manager(
|
|
52
54
|
exchange=exchange_name,
|
|
@@ -58,6 +60,10 @@ class CcxtDataProvider(IDataProvider):
|
|
|
58
60
|
)
|
|
59
61
|
self.orderbook_limit = orderbook_limit
|
|
60
62
|
|
|
63
|
+
# Attach rate limiter to exchange manager (overrides CCXT's built-in throttle)
|
|
64
|
+
if rate_limiter is not None:
|
|
65
|
+
self._exchange_manager.attach_rate_limiter(rate_limiter)
|
|
66
|
+
|
|
61
67
|
self.time_provider = time_provider
|
|
62
68
|
self.channel = channel
|
|
63
69
|
self.max_ws_retries = max_ws_retries
|
|
@@ -276,11 +276,9 @@ class ExchangeManager:
|
|
|
276
276
|
if rate_limiter is None:
|
|
277
277
|
return
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
# CCXT calls self.throttle(cost) in fetch2() before every REST request
|
|
283
|
-
# cost comes from calculateRateLimiterCost() which uses per-endpoint weights
|
|
279
|
+
# Keep enableRateLimit=True so CCXT calls self.throttle(cost) in fetch2()
|
|
280
|
+
# We override the throttle method to use our rate limiter instead of CCXT's built-in
|
|
281
|
+
exchange.enableRateLimit = True
|
|
284
282
|
async def _rate_limit_throttle(cost=None):
|
|
285
283
|
if cost is None:
|
|
286
284
|
cost = 1.0
|
|
@@ -111,13 +111,13 @@ def get_ccxt_exchange_manager(
|
|
|
111
111
|
"exchange": exchange,
|
|
112
112
|
"api_key": api_key,
|
|
113
113
|
"secret": secret,
|
|
114
|
-
"loop":
|
|
114
|
+
"loop": loop,
|
|
115
115
|
"use_testnet": use_testnet,
|
|
116
116
|
**kwargs,
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
ccxt_exchange = get_ccxt_exchange(
|
|
120
|
-
exchange=exchange, api_key=api_key, secret=secret, loop=
|
|
120
|
+
exchange=exchange, api_key=api_key, secret=secret, loop=loop, use_testnet=use_testnet, **kwargs
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
manager = ExchangeManager(
|
|
@@ -129,11 +129,6 @@ 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
|
-
|
|
137
132
|
_exchange_manager_cache[cache_key] = manager
|
|
138
133
|
return manager
|
|
139
134
|
|
|
@@ -13,9 +13,11 @@ References:
|
|
|
13
13
|
- Bybit: https://bybit-exchange.github.io/docs/v5/rate-limit
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
from qubx.connectors.registry import rate_limit_config
|
|
16
17
|
from qubx.rate_limiting import EndpointCosts, ExchangeRateLimitConfig, PoolConfig
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
@rate_limit_config("ccxt")
|
|
19
21
|
def create_ccxt_rate_limit_config(exchange_name: str, ccxt_exchange=None) -> ExchangeRateLimitConfig:
|
|
20
22
|
"""Create rate limit config for a CCXT exchange.
|
|
21
23
|
|
|
@@ -26,6 +26,7 @@ class ConnectorRegistry:
|
|
|
26
26
|
_data_providers: dict[str, Type[IDataProvider]] = {}
|
|
27
27
|
_account_processors: dict[str, Type[IAccountProcessor]] = {}
|
|
28
28
|
_brokers: dict[str, Type[IBroker]] = {}
|
|
29
|
+
_rate_limit_configs: dict[str, Callable] = {}
|
|
29
30
|
|
|
30
31
|
@classmethod
|
|
31
32
|
def register_data_provider(cls, name: str) -> Callable[[Type[T]], Type[T]]:
|
|
@@ -153,6 +154,32 @@ class ConnectorRegistry:
|
|
|
153
154
|
)
|
|
154
155
|
return broker_cls(**kwargs)
|
|
155
156
|
|
|
157
|
+
@classmethod
|
|
158
|
+
def register_rate_limit_config(cls, name: str) -> Callable:
|
|
159
|
+
"""Decorator to register a rate limit config factory for a connector.
|
|
160
|
+
|
|
161
|
+
The factory receives (exchange_name: str) and returns ExchangeRateLimitConfig or None.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def decorator(func: Callable) -> Callable:
|
|
165
|
+
cls._rate_limit_configs[name.lower()] = func
|
|
166
|
+
logger.debug(f"Registered rate limit config: {name}")
|
|
167
|
+
return func
|
|
168
|
+
|
|
169
|
+
return decorator
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def get_rate_limit_config(cls, name: str, exchange_name: str):
|
|
173
|
+
"""Get rate limit config for a connector/exchange pair.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
ExchangeRateLimitConfig or None if connector has no rate limiting registered
|
|
177
|
+
"""
|
|
178
|
+
factory = cls._rate_limit_configs.get(name.lower())
|
|
179
|
+
if factory is None:
|
|
180
|
+
return None
|
|
181
|
+
return factory(exchange_name)
|
|
182
|
+
|
|
156
183
|
@classmethod
|
|
157
184
|
def is_data_provider_registered(cls, name: str) -> bool:
|
|
158
185
|
"""Check if a data provider is registered."""
|
|
@@ -222,3 +249,15 @@ def broker(name: str) -> Callable[[Type[T]], Type[T]]:
|
|
|
222
249
|
...
|
|
223
250
|
"""
|
|
224
251
|
return ConnectorRegistry.register_broker(name)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def rate_limit_config(name: str) -> Callable:
|
|
255
|
+
"""
|
|
256
|
+
Decorator for registering a rate limit config factory.
|
|
257
|
+
|
|
258
|
+
Usage:
|
|
259
|
+
@rate_limit_config("my_exchange")
|
|
260
|
+
def create_my_exchange_rate_limits(exchange_name: str) -> ExchangeRateLimitConfig:
|
|
261
|
+
return ExchangeRateLimitConfig(pools={...}, ...)
|
|
262
|
+
"""
|
|
263
|
+
return ConnectorRegistry.register_rate_limit_config(name)
|
|
@@ -29,6 +29,14 @@ def _resolve(ctx: IStrategyContext, symbol: str, exchange: str | None = None) ->
|
|
|
29
29
|
return None
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def _default_quote(ctx: IStrategyContext) -> str:
|
|
33
|
+
"""Get default quote currency from the first exchange's base currency."""
|
|
34
|
+
try:
|
|
35
|
+
return ctx.account.get_base_currency()
|
|
36
|
+
except Exception:
|
|
37
|
+
return "USDT"
|
|
38
|
+
|
|
39
|
+
|
|
32
40
|
# --- Rounding helpers ---
|
|
33
41
|
|
|
34
42
|
|
|
@@ -121,7 +129,9 @@ def _set_universe(ctx: IStrategyContext, symbols: list[str], exchange: str | Non
|
|
|
121
129
|
# --- Instrument discovery actions ---
|
|
122
130
|
|
|
123
131
|
|
|
124
|
-
def _get_available_instruments(ctx: IStrategyContext, exchange: str, quote: str =
|
|
132
|
+
def _get_available_instruments(ctx: IStrategyContext, exchange: str, quote: str | None = None, market_type: str = "SWAP", **kwargs) -> ActionResult:
|
|
133
|
+
if quote is None:
|
|
134
|
+
quote = _default_quote(ctx)
|
|
125
135
|
mt = MarketType(market_type) if market_type else None
|
|
126
136
|
as_of = to_timestamp(ctx.time())
|
|
127
137
|
instruments = lookup.find_instruments(exchange, quote=quote, market_type=mt, as_of=as_of)
|
|
@@ -173,10 +183,12 @@ def _get_top_instruments(
|
|
|
173
183
|
sort_by: str = "turnover",
|
|
174
184
|
period: str = "3d",
|
|
175
185
|
timeframe: str = "1d",
|
|
176
|
-
quote: str =
|
|
186
|
+
quote: str | None = None,
|
|
177
187
|
market_type: str = "SWAP",
|
|
178
188
|
**kwargs,
|
|
179
189
|
) -> ActionResult:
|
|
190
|
+
if quote is None:
|
|
191
|
+
quote = _default_quote(ctx)
|
|
180
192
|
stop = to_timestamp(ctx.time())
|
|
181
193
|
start = stop - to_timedelta(period)
|
|
182
194
|
|
|
@@ -308,10 +320,10 @@ def _get_balances(ctx: IStrategyContext, **kwargs) -> ActionResult:
|
|
|
308
320
|
return ActionResult(status="ok", data={"balances": balances})
|
|
309
321
|
|
|
310
322
|
|
|
311
|
-
def _get_orders(ctx: IStrategyContext, symbol: str | None = None, **kwargs) -> ActionResult:
|
|
323
|
+
def _get_orders(ctx: IStrategyContext, symbol: str | None = None, exchange: str | None = None, **kwargs) -> ActionResult:
|
|
312
324
|
orders_data = []
|
|
313
325
|
if symbol:
|
|
314
|
-
instr = _resolve(ctx, symbol)
|
|
326
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
315
327
|
if instr is None:
|
|
316
328
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
317
329
|
orders = ctx.get_orders(instrument=instr)
|
|
@@ -333,8 +345,8 @@ def _get_orders(ctx: IStrategyContext, symbol: str | None = None, **kwargs) -> A
|
|
|
333
345
|
return ActionResult(status="ok", data={"orders": orders_data})
|
|
334
346
|
|
|
335
347
|
|
|
336
|
-
def _get_quote(ctx: IStrategyContext, symbol: str, **kwargs) -> ActionResult:
|
|
337
|
-
instr = _resolve(ctx, symbol)
|
|
348
|
+
def _get_quote(ctx: IStrategyContext, symbol: str, exchange: str | None = None, **kwargs) -> ActionResult:
|
|
349
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
338
350
|
if instr is None:
|
|
339
351
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
340
352
|
|
|
@@ -354,8 +366,8 @@ def _get_quote(ctx: IStrategyContext, symbol: str, **kwargs) -> ActionResult:
|
|
|
354
366
|
)
|
|
355
367
|
|
|
356
368
|
|
|
357
|
-
def _get_ohlc(ctx: IStrategyContext, symbol: str, timeframe: str = "1h", length: int = 20, **kwargs) -> ActionResult:
|
|
358
|
-
instr = _resolve(ctx, symbol)
|
|
369
|
+
def _get_ohlc(ctx: IStrategyContext, symbol: str, timeframe: str = "1h", length: int = 20, exchange: str | None = None, **kwargs) -> ActionResult:
|
|
370
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
359
371
|
if instr is None:
|
|
360
372
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
361
373
|
|
|
@@ -520,8 +532,8 @@ def _get_state_schema(ctx: IStrategyContext, **kwargs) -> ActionResult:
|
|
|
520
532
|
# --- Trading actions ---
|
|
521
533
|
|
|
522
534
|
|
|
523
|
-
def _trade(ctx: IStrategyContext, symbol: str, amount: float, price: float | None = None, time_in_force: str = "gtc", **kwargs) -> ActionResult:
|
|
524
|
-
instr = _resolve(ctx, symbol)
|
|
535
|
+
def _trade(ctx: IStrategyContext, symbol: str, amount: float, price: float | None = None, time_in_force: str = "gtc", exchange: str | None = None, **kwargs) -> ActionResult:
|
|
536
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
525
537
|
if instr is None:
|
|
526
538
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
527
539
|
|
|
@@ -535,8 +547,8 @@ def _trade(ctx: IStrategyContext, symbol: str, amount: float, price: float | Non
|
|
|
535
547
|
return ActionResult(status="error", error=str(e))
|
|
536
548
|
|
|
537
549
|
|
|
538
|
-
def _set_target_position(ctx: IStrategyContext, symbol: str, target: float, price: float | None = None, **kwargs) -> ActionResult:
|
|
539
|
-
instr = _resolve(ctx, symbol)
|
|
550
|
+
def _set_target_position(ctx: IStrategyContext, symbol: str, target: float, price: float | None = None, exchange: str | None = None, **kwargs) -> ActionResult:
|
|
551
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
540
552
|
if instr is None:
|
|
541
553
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
542
554
|
|
|
@@ -550,8 +562,8 @@ def _set_target_position(ctx: IStrategyContext, symbol: str, target: float, pric
|
|
|
550
562
|
return ActionResult(status="error", error=str(e))
|
|
551
563
|
|
|
552
564
|
|
|
553
|
-
def _set_target_leverage(ctx: IStrategyContext, symbol: str, leverage: float, price: float | None = None, **kwargs) -> ActionResult:
|
|
554
|
-
instr = _resolve(ctx, symbol)
|
|
565
|
+
def _set_target_leverage(ctx: IStrategyContext, symbol: str, leverage: float, price: float | None = None, exchange: str | None = None, **kwargs) -> ActionResult:
|
|
566
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
555
567
|
if instr is None:
|
|
556
568
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
557
569
|
|
|
@@ -562,8 +574,8 @@ def _set_target_leverage(ctx: IStrategyContext, symbol: str, leverage: float, pr
|
|
|
562
574
|
return ActionResult(status="error", error=str(e))
|
|
563
575
|
|
|
564
576
|
|
|
565
|
-
def _close_position(ctx: IStrategyContext, symbol: str, **kwargs) -> ActionResult:
|
|
566
|
-
instr = _resolve(ctx, symbol)
|
|
577
|
+
def _close_position(ctx: IStrategyContext, symbol: str, exchange: str | None = None, **kwargs) -> ActionResult:
|
|
578
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
567
579
|
if instr is None:
|
|
568
580
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
569
581
|
|
|
@@ -586,10 +598,10 @@ def _close_positions(ctx: IStrategyContext, **kwargs) -> ActionResult:
|
|
|
586
598
|
return ActionResult(status="error", error=str(e))
|
|
587
599
|
|
|
588
600
|
|
|
589
|
-
def _cancel_orders(ctx: IStrategyContext, symbol: str | None = None, **kwargs) -> ActionResult:
|
|
601
|
+
def _cancel_orders(ctx: IStrategyContext, symbol: str | None = None, exchange: str | None = None, **kwargs) -> ActionResult:
|
|
590
602
|
try:
|
|
591
603
|
if symbol:
|
|
592
|
-
instr = _resolve(ctx, symbol)
|
|
604
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
593
605
|
if instr is None:
|
|
594
606
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
595
607
|
ctx.cancel_orders(instr)
|
|
@@ -601,8 +613,8 @@ def _cancel_orders(ctx: IStrategyContext, symbol: str | None = None, **kwargs) -
|
|
|
601
613
|
return ActionResult(status="error", error=str(e))
|
|
602
614
|
|
|
603
615
|
|
|
604
|
-
def _emit_signal(ctx: IStrategyContext, symbol: str, signal_value: float, price: float | None = None, group: str = "", **kwargs) -> ActionResult:
|
|
605
|
-
instr = _resolve(ctx, symbol)
|
|
616
|
+
def _emit_signal(ctx: IStrategyContext, symbol: str, signal_value: float, price: float | None = None, group: str = "", exchange: str | None = None, **kwargs) -> ActionResult:
|
|
617
|
+
instr = _resolve(ctx, symbol, exchange)
|
|
606
618
|
if instr is None:
|
|
607
619
|
return ActionResult(status="error", error=f"Unknown symbol: {symbol}")
|
|
608
620
|
|
|
@@ -623,6 +635,7 @@ def _emit_signal(ctx: IStrategyContext, symbol: str, signal_value: float, price:
|
|
|
623
635
|
# --- Action registry ---
|
|
624
636
|
|
|
625
637
|
_MARKET_TYPE_DOC = "Market type: SPOT, SWAP (perpetual futures), FUTURE (dated futures), OPTION, MARGIN"
|
|
638
|
+
_EXCHANGE_PARAM = ActionParam(name="exchange", type="string", description="Exchange for symbol resolution (required when bot trades on multiple exchanges)", required=False, default=None)
|
|
626
639
|
|
|
627
640
|
BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
628
641
|
# Universe
|
|
@@ -679,7 +692,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
679
692
|
read_only=True,
|
|
680
693
|
params=[
|
|
681
694
|
ActionParam(name="exchange", type="string", description="Exchange name (e.g., BINANCE.UM, KRAKEN, BYBIT)"),
|
|
682
|
-
ActionParam(name="quote", type="string", description="Quote currency filter", required=False, default=
|
|
695
|
+
ActionParam(name="quote", type="string", description="Quote currency filter (defaults to bot's base currency)", required=False, default=None),
|
|
683
696
|
ActionParam(name="market_type", type="string", description=_MARKET_TYPE_DOC, required=False, default="SWAP"),
|
|
684
697
|
],
|
|
685
698
|
),
|
|
@@ -710,7 +723,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
710
723
|
ActionParam(name="sort_by", type="string", description="Ranking metric: turnover, market_cap, or funding", required=False, default="turnover"),
|
|
711
724
|
ActionParam(name="period", type="string", description="Lookback period (e.g., 3d, 7d, 1h)", required=False, default="3d"),
|
|
712
725
|
ActionParam(name="timeframe", type="string", description="Candle timeframe for turnover (e.g., 1d, 1h)", required=False, default="1d"),
|
|
713
|
-
ActionParam(name="quote", type="string", description="Quote currency filter", required=False, default=
|
|
726
|
+
ActionParam(name="quote", type="string", description="Quote currency filter (defaults to bot's base currency)", required=False, default=None),
|
|
714
727
|
ActionParam(name="market_type", type="string", description=_MARKET_TYPE_DOC, required=False, default="SWAP"),
|
|
715
728
|
],
|
|
716
729
|
),
|
|
@@ -731,7 +744,10 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
731
744
|
description="Get open orders",
|
|
732
745
|
category="diagnostics",
|
|
733
746
|
read_only=True,
|
|
734
|
-
params=[
|
|
747
|
+
params=[
|
|
748
|
+
ActionParam(name="symbol", type="string", description="Filter by symbol", required=False, default=None),
|
|
749
|
+
_EXCHANGE_PARAM,
|
|
750
|
+
],
|
|
735
751
|
),
|
|
736
752
|
_get_orders,
|
|
737
753
|
),
|
|
@@ -741,7 +757,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
741
757
|
description="Get latest quote for an instrument",
|
|
742
758
|
category="diagnostics",
|
|
743
759
|
read_only=True,
|
|
744
|
-
params=[ActionParam(name="symbol", type="string", description="Trading instrument symbol")],
|
|
760
|
+
params=[ActionParam(name="symbol", type="string", description="Trading instrument symbol"), _EXCHANGE_PARAM],
|
|
745
761
|
),
|
|
746
762
|
_get_quote,
|
|
747
763
|
),
|
|
@@ -755,6 +771,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
755
771
|
ActionParam(name="symbol", type="string", description="Trading instrument symbol"),
|
|
756
772
|
ActionParam(name="timeframe", type="string", description="Bar timeframe", required=False, default="1h"),
|
|
757
773
|
ActionParam(name="length", type="integer", description="Number of bars", required=False, default=20),
|
|
774
|
+
_EXCHANGE_PARAM,
|
|
758
775
|
],
|
|
759
776
|
),
|
|
760
777
|
_get_ohlc,
|
|
@@ -801,6 +818,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
801
818
|
ActionParam(name="amount", type="number", description="Order amount (positive=buy, negative=sell)"),
|
|
802
819
|
ActionParam(name="price", type="number", description="Limit price", required=False, default=None),
|
|
803
820
|
ActionParam(name="time_in_force", type="string", description="Time in force", required=False, default="gtc"),
|
|
821
|
+
_EXCHANGE_PARAM,
|
|
804
822
|
],
|
|
805
823
|
),
|
|
806
824
|
_trade,
|
|
@@ -815,6 +833,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
815
833
|
ActionParam(name="symbol", type="string", description="Trading instrument symbol"),
|
|
816
834
|
ActionParam(name="target", type="number", description="Target position size"),
|
|
817
835
|
ActionParam(name="price", type="number", description="Limit price", required=False, default=None),
|
|
836
|
+
_EXCHANGE_PARAM,
|
|
818
837
|
],
|
|
819
838
|
),
|
|
820
839
|
_set_target_position,
|
|
@@ -829,6 +848,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
829
848
|
ActionParam(name="symbol", type="string", description="Trading instrument symbol"),
|
|
830
849
|
ActionParam(name="leverage", type="number", description="Target leverage"),
|
|
831
850
|
ActionParam(name="price", type="number", description="Limit price", required=False, default=None),
|
|
851
|
+
_EXCHANGE_PARAM,
|
|
832
852
|
],
|
|
833
853
|
),
|
|
834
854
|
_set_target_leverage,
|
|
@@ -839,7 +859,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
839
859
|
description="Close position for an instrument",
|
|
840
860
|
category="trading",
|
|
841
861
|
dangerous=True,
|
|
842
|
-
params=[ActionParam(name="symbol", type="string", description="Trading instrument symbol")],
|
|
862
|
+
params=[ActionParam(name="symbol", type="string", description="Trading instrument symbol"), _EXCHANGE_PARAM],
|
|
843
863
|
),
|
|
844
864
|
_close_position,
|
|
845
865
|
),
|
|
@@ -852,7 +872,10 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
852
872
|
name="cancel_orders",
|
|
853
873
|
description="Cancel open orders",
|
|
854
874
|
category="trading",
|
|
855
|
-
params=[
|
|
875
|
+
params=[
|
|
876
|
+
ActionParam(name="symbol", type="string", description="Filter by symbol", required=False, default=None),
|
|
877
|
+
_EXCHANGE_PARAM,
|
|
878
|
+
],
|
|
856
879
|
),
|
|
857
880
|
_cancel_orders,
|
|
858
881
|
),
|
|
@@ -867,6 +890,7 @@ BUILTIN_ACTIONS: dict[str, tuple[ActionDef, Callable]] = {
|
|
|
867
890
|
ActionParam(name="signal_value", type="number", description="Signal value (target position)"),
|
|
868
891
|
ActionParam(name="price", type="number", description="Limit price", required=False, default=None),
|
|
869
892
|
ActionParam(name="group", type="string", description="Signal group", required=False, default=""),
|
|
893
|
+
_EXCHANGE_PARAM,
|
|
870
894
|
],
|
|
871
895
|
),
|
|
872
896
|
_emit_signal,
|
|
@@ -159,7 +159,6 @@ 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
162
|
rate_limiting_config: Any | None = None,
|
|
164
163
|
event_loop: "asyncio.AbstractEventLoop | None" = None,
|
|
165
164
|
) -> None:
|
|
@@ -193,7 +192,6 @@ class StrategyContext(IStrategyContext):
|
|
|
193
192
|
self.health = self._health_monitor
|
|
194
193
|
self._state_persistence = state_persistence or DummyStatePersistence()
|
|
195
194
|
self._state_snapshot_interval = state_snapshot_interval
|
|
196
|
-
self._rate_limit_backend = rate_limit_backend
|
|
197
195
|
self._rate_limiting_config = rate_limiting_config
|
|
198
196
|
self.event_loop = event_loop
|
|
199
197
|
|
|
@@ -314,12 +312,6 @@ class StrategyContext(IStrategyContext):
|
|
|
314
312
|
# Configure periodic state snapshot persistence
|
|
315
313
|
self._processing_manager.configure_state_snapshot(self._state_snapshot_interval)
|
|
316
314
|
|
|
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
315
|
# Configure periodic rate limit metric emission
|
|
324
316
|
_rl_metrics_interval = self._rate_limiting_config.metrics_interval if self._rate_limiting_config else None
|
|
325
317
|
self._processing_manager.configure_rate_limit_metrics(_rl_metrics_interval)
|
|
@@ -600,11 +592,6 @@ class StrategyContext(IStrategyContext):
|
|
|
600
592
|
self._rate_limiters: dict = {}
|
|
601
593
|
return self._rate_limiters
|
|
602
594
|
|
|
603
|
-
@property
|
|
604
|
-
def rate_limit_backend(self):
|
|
605
|
-
"""Rate limit storage backend (InMemoryBackend or RedisBackend)."""
|
|
606
|
-
return self._rate_limit_backend
|
|
607
|
-
|
|
608
595
|
# IAccountViewer delegation
|
|
609
596
|
|
|
610
597
|
# capital information
|
|
@@ -831,11 +831,6 @@ 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
|
-
|
|
839
834
|
def is_connected(self) -> bool:
|
|
840
835
|
"""
|
|
841
836
|
Check if the data provider is currently connected to the exchange.
|
|
@@ -327,7 +327,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
327
327
|
except concurrent.futures.TimeoutError:
|
|
328
328
|
logger.warning(f"Rate limit metrics collection timed out for {exchange_name}")
|
|
329
329
|
except Exception as e:
|
|
330
|
-
logger.error(f"Failed to collect rate limit metrics for {exchange_name}: {e}")
|
|
330
|
+
logger.opt(colors=False).error(f"Failed to collect rate limit metrics for {exchange_name}: {e}")
|
|
331
331
|
|
|
332
332
|
@staticmethod
|
|
333
333
|
def _do_emit_metrics(ctx, metrics: list[dict]) -> None:
|
|
@@ -314,9 +314,11 @@ class CcxtStorage(IStorage):
|
|
|
314
314
|
_uri_hint: str = "", # - ignored; absorbed from URI "ccxt::EXCHANGE" via StorageRegistry
|
|
315
315
|
max_bars: int = 10_000,
|
|
316
316
|
max_history: str = "3650d",
|
|
317
|
+
rate_limiters: dict | None = None,
|
|
317
318
|
) -> None:
|
|
318
319
|
self._max_bars = max_bars
|
|
319
320
|
self._max_history = to_timedelta(max_history)
|
|
321
|
+
self._rate_limiters = rate_limiters or {}
|
|
320
322
|
# - shared async loop (created from first exchange connection)
|
|
321
323
|
self._loop = None
|
|
322
324
|
self._capabilities = None
|
|
@@ -448,6 +450,16 @@ class CcxtStorage(IStorage):
|
|
|
448
450
|
_start = _max_time
|
|
449
451
|
return _start, _stop
|
|
450
452
|
|
|
453
|
+
def _get_rate_limiter(self, ccxt_ex: Exchange):
|
|
454
|
+
"""Find the rate limiter for a CCXT exchange instance."""
|
|
455
|
+
if not self._rate_limiters:
|
|
456
|
+
return None
|
|
457
|
+
# Match by exchange key in our cache (e.g., "BINANCE.UM", "OKX.F")
|
|
458
|
+
for exch_key, ex in self._exchanges.items():
|
|
459
|
+
if ex is ccxt_ex:
|
|
460
|
+
return self._rate_limiters.get(exch_key)
|
|
461
|
+
return None
|
|
462
|
+
|
|
451
463
|
async def _async_paginated_fetch(
|
|
452
464
|
self,
|
|
453
465
|
ccxt_ex: Exchange,
|
|
@@ -472,7 +484,12 @@ class CcxtStorage(IStorage):
|
|
|
472
484
|
all_items: list = []
|
|
473
485
|
current_since = since
|
|
474
486
|
|
|
487
|
+
# Get rate limiter for this exchange (if available)
|
|
488
|
+
_rl = self._get_rate_limiter(ccxt_ex)
|
|
489
|
+
|
|
475
490
|
for _ in range(max_pages):
|
|
491
|
+
if _rl:
|
|
492
|
+
await _rl.acquire("ccxt_rest")
|
|
476
493
|
batch = await method(since=current_since, limit=limit, **method_kwargs)
|
|
477
494
|
if not batch:
|
|
478
495
|
break
|
|
@@ -525,10 +542,16 @@ class CcxtStorage(IStorage):
|
|
|
525
542
|
until: int,
|
|
526
543
|
) -> dict[str, list]:
|
|
527
544
|
"""
|
|
528
|
-
Concurrent OHLCV fetch for multiple instruments
|
|
545
|
+
Concurrent OHLCV fetch for multiple instruments with bounded concurrency.
|
|
529
546
|
Returns ``{qubx_sym: raw_candles}``.
|
|
530
547
|
"""
|
|
531
|
-
|
|
548
|
+
sem = asyncio.Semaphore(5) # max 5 concurrent fetches
|
|
549
|
+
|
|
550
|
+
async def _fetch_with_sem(ccxt_sym: str, exc_tf: str, since: int, until: int) -> list:
|
|
551
|
+
async with sem:
|
|
552
|
+
return await self._async_fetch_ohlcv(ccxt_ex, ccxt_sym, exc_tf, since, until)
|
|
553
|
+
|
|
554
|
+
coros = [_fetch_with_sem(ccxt_sym, exc_tf, since, until) for ccxt_sym, _ in instruments_info]
|
|
532
555
|
results = await asyncio.gather(*coros, return_exceptions=True)
|
|
533
556
|
out: dict[str, list] = {}
|
|
534
557
|
for i, result in enumerate(results):
|
|
@@ -600,11 +623,17 @@ class CcxtStorage(IStorage):
|
|
|
600
623
|
default_hours: float,
|
|
601
624
|
) -> dict[str, list[tuple[int, float, float]]]:
|
|
602
625
|
"""
|
|
603
|
-
Concurrent per-instrument funding fetch
|
|
626
|
+
Concurrent per-instrument funding fetch with bounded concurrency.
|
|
604
627
|
Returns ``{qubx_sym: [(ts_ms, rate, hours), ...]}``.
|
|
605
628
|
"""
|
|
629
|
+
sem = asyncio.Semaphore(5)
|
|
630
|
+
|
|
631
|
+
async def _fetch_with_sem(ccxt_ex, instr, ccxt_sym, since, until, default_hours):
|
|
632
|
+
async with sem:
|
|
633
|
+
return await self._async_fetch_funding_one(ccxt_ex, instr, ccxt_sym, since, until, default_hours)
|
|
634
|
+
|
|
606
635
|
coros = [
|
|
607
|
-
|
|
636
|
+
_fetch_with_sem(ccxt_ex, instr, ccxt_sym, since, until, default_hours)
|
|
608
637
|
for ccxt_sym, instr in instruments_info
|
|
609
638
|
]
|
|
610
639
|
results = await asyncio.gather(*coros, return_exceptions=True)
|
|
@@ -47,6 +47,7 @@ class ExchangeRateLimiter:
|
|
|
47
47
|
config: ExchangeRateLimitConfig,
|
|
48
48
|
backend: IRateLimitBackend | None = None,
|
|
49
49
|
scope_ids: dict[str, str] | None = None,
|
|
50
|
+
event_loop: "asyncio.AbstractEventLoop | None" = None,
|
|
50
51
|
):
|
|
51
52
|
"""
|
|
52
53
|
Args:
|
|
@@ -54,11 +55,13 @@ class ExchangeRateLimiter:
|
|
|
54
55
|
config: Rate limit configuration with pools and endpoint mappings
|
|
55
56
|
backend: Storage backend (default: InMemoryBackend)
|
|
56
57
|
scope_ids: Maps scope type to identifier, e.g. {"ip": "1.2.3.4", "account": "abc123"}
|
|
58
|
+
event_loop: Event loop this rate limiter operates on (for metrics collection)
|
|
57
59
|
"""
|
|
58
60
|
self._exchange = exchange
|
|
59
61
|
self._config = config
|
|
60
62
|
self._backend = backend or InMemoryBackend()
|
|
61
63
|
self._scope_ids = scope_ids or {}
|
|
64
|
+
self._event_loop = event_loop
|
|
62
65
|
|
|
63
66
|
# Per-pool gates (asyncio.Event: set = open, clear = closed)
|
|
64
67
|
self._gates: dict[str, asyncio.Event] = {}
|
|
@@ -84,6 +87,15 @@ class ExchangeRateLimiter:
|
|
|
84
87
|
def exchange(self) -> str:
|
|
85
88
|
return self._exchange
|
|
86
89
|
|
|
90
|
+
@property
|
|
91
|
+
def event_loop(self):
|
|
92
|
+
"""Event loop this rate limiter operates on (set by connector)."""
|
|
93
|
+
return getattr(self, "_event_loop", None)
|
|
94
|
+
|
|
95
|
+
@event_loop.setter
|
|
96
|
+
def event_loop(self, loop):
|
|
97
|
+
self._event_loop = loop
|
|
98
|
+
|
|
87
99
|
@property
|
|
88
100
|
def config(self) -> ExchangeRateLimitConfig:
|
|
89
101
|
return self._config
|
|
@@ -356,7 +368,6 @@ class ExchangeRateLimiter:
|
|
|
356
368
|
"""Collect current metrics for all pools.
|
|
357
369
|
|
|
358
370
|
Returns a list of metric dicts suitable for emitting via ctx.emitter.emit().
|
|
359
|
-
Each dict has: name, value, tags.
|
|
360
371
|
|
|
361
372
|
Call this periodically (e.g., once per minute) and emit via:
|
|
362
373
|
for m in await rate_limiter.collect_metrics():
|