Qubx 1.2.1.dev1__tar.gz → 1.2.2.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/PKG-INFO +1 -1
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/_version.py +2 -2
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/engine.py +47 -175
- qubx-1.2.2.dev1/src/qubx/rate_limiting/pools.py +201 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/.gitignore +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/LICENSE +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/README.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/pyproject.toml +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/scripts/build.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/_nb_magic.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/account.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/management.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/ome.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/optimization.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/runner.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/simulator.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/transfers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/commands.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/deploy.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/misc.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/release.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/resolver.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/s3.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/theme.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/tui.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/user_config.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/config.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/account.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/registry.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/builtin.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/decorator.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/executor.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/interfaces.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/server.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/types.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/account.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/basics.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/context.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/errors.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/exceptions.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/helpers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/initializer.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/interfaces.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/loggers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/lookups.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/metrics.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/market.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/series.pxd +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/series.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/series.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/utils.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/utils.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/cache.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/containers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/guards.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/registry.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storage.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/ccxt.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/csv.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/handy.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/multi.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/stub.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/transformers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/composite.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/csv.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/indicator.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/questdb.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/composite.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/slack.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/gathering/simplest.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/health/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/health/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/health/dummy.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/csv.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/mongo.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/postgres.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/notifications/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/notifications/composite.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/notifications/slack.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/notifications/throttler.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/pandaz/stats.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/pandaz/ta.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/pandaz/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/plugins/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/plugins/loader.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/backend.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/config.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/ip_resolver.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/redis_backend.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/_build.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restarts/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/balance.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/position.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/signal.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/state.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/state/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/state/dummy.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/state/redis.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/ta/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/base.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/template.yml +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.gitignore.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.python-version +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.vscode/launch.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.vscode/settings.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/README.md.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/configs/.gitkeep +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/justfile.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/research/.gitkeep +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/template.yml +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/advanced.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/composite.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/sizers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/collections.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/misc.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/nonce.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/ntp.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/orderbook.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/questdb.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/results.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/runner.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/s3.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/slack.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/throttler.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/time.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 2,
|
|
21
|
+
__version__ = version = '1.2.2.dev1'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 2, 'dev1')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Exchange rate limiting engine.
|
|
1
|
+
"""Exchange rate limiting engine.
|
|
3
2
|
|
|
4
|
-
The central component that connectors interact with.
|
|
5
|
-
|
|
6
|
-
and metric collection.
|
|
3
|
+
The central component that connectors interact with. Delegates pool-specific
|
|
4
|
+
behavior (gate management, acquisition, sync) to polymorphic pool objects.
|
|
7
5
|
"""
|
|
8
6
|
|
|
9
7
|
import asyncio
|
|
@@ -13,11 +11,9 @@ from typing import Any
|
|
|
13
11
|
from qubx import logger
|
|
14
12
|
|
|
15
13
|
from .backend import InMemoryBackend, IRateLimitBackend
|
|
16
|
-
from .config import ExchangeRateLimitConfig
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class RateLimitGateTimeout(Exception):
|
|
20
|
-
"""Raised when acquire() times out waiting for a gate to reopen."""
|
|
14
|
+
from .config import ExchangeRateLimitConfig
|
|
15
|
+
from .pools import BasePool, QuotaPool, RatePool
|
|
16
|
+
from .pools import RateLimitGateTimeout as RateLimitGateTimeout # re-export
|
|
21
17
|
|
|
22
18
|
|
|
23
19
|
class ExchangeRateLimiter:
|
|
@@ -59,30 +55,19 @@ class ExchangeRateLimiter:
|
|
|
59
55
|
"""
|
|
60
56
|
self._exchange = exchange
|
|
61
57
|
self._config = config
|
|
62
|
-
self._backend = backend or InMemoryBackend()
|
|
63
58
|
self._scope_ids = scope_ids or {}
|
|
64
59
|
self._event_loop = event_loop
|
|
65
|
-
|
|
66
|
-
# Per-pool gates (asyncio.Event: set = open, clear = closed)
|
|
67
|
-
self._gates: dict[str, asyncio.Event] = {}
|
|
68
|
-
self._gate_tasks: dict[str, asyncio.Task] = {}
|
|
69
|
-
for pool_name in config.pools:
|
|
70
|
-
gate = asyncio.Event()
|
|
71
|
-
gate.set()
|
|
72
|
-
self._gates[pool_name] = gate
|
|
73
|
-
|
|
74
|
-
# Quota pools: externally-managed remaining count
|
|
75
|
-
self._quota_remaining: dict[str, float] = {}
|
|
76
|
-
for pool_name, pool in config.pools.items():
|
|
77
|
-
if pool.pool_type == "quota":
|
|
78
|
-
self._quota_remaining[pool_name] = pool.capacity
|
|
79
|
-
|
|
80
|
-
# Stats tracking
|
|
81
|
-
self._hits: dict[str, int] = {}
|
|
82
|
-
self._total_wait: dict[str, float] = {}
|
|
83
|
-
self._consumed: dict[str, float] = {}
|
|
84
60
|
self._last_metrics_time = time.monotonic()
|
|
85
61
|
|
|
62
|
+
backend = backend or InMemoryBackend()
|
|
63
|
+
self._pools: dict[str, BasePool] = {}
|
|
64
|
+
for pool_name, pool_config in config.pools.items():
|
|
65
|
+
scope_id = self._scope_ids.get(pool_config.scope, "local")
|
|
66
|
+
if pool_config.pool_type == "quota":
|
|
67
|
+
self._pools[pool_name] = QuotaPool(pool_config, exchange, scope_id)
|
|
68
|
+
else:
|
|
69
|
+
self._pools[pool_name] = RatePool(pool_config, exchange, scope_id, backend)
|
|
70
|
+
|
|
86
71
|
@property
|
|
87
72
|
def exchange(self) -> str:
|
|
88
73
|
return self._exchange
|
|
@@ -100,11 +85,6 @@ class ExchangeRateLimiter:
|
|
|
100
85
|
def config(self) -> ExchangeRateLimitConfig:
|
|
101
86
|
return self._config
|
|
102
87
|
|
|
103
|
-
def _key_for(self, pool: PoolConfig) -> str:
|
|
104
|
-
"""Construct Redis/backend key for a pool based on its scope."""
|
|
105
|
-
scope_id = self._scope_ids.get(pool.scope, "local")
|
|
106
|
-
return f"ratelimit:{self._exchange}:{pool.name}:{scope_id}"
|
|
107
|
-
|
|
108
88
|
def update_scope_id(self, scope: str, new_id: str) -> None:
|
|
109
89
|
"""Update scope identifier (e.g., when egress IP changes).
|
|
110
90
|
|
|
@@ -114,6 +94,9 @@ class ExchangeRateLimiter:
|
|
|
114
94
|
if old_id != new_id:
|
|
115
95
|
logger.info(f"Rate limiter {self._exchange}: {scope} scope changed {old_id} → {new_id}")
|
|
116
96
|
self._scope_ids[scope] = new_id
|
|
97
|
+
for pool in self._pools.values():
|
|
98
|
+
if pool.config.scope == scope:
|
|
99
|
+
pool.update_scope_id(new_id)
|
|
117
100
|
|
|
118
101
|
async def acquire(self, endpoint: str, weight_override: float | None = None) -> None:
|
|
119
102
|
"""Wait until all pools for this endpoint have sufficient budget.
|
|
@@ -134,50 +117,11 @@ class ExchangeRateLimiter:
|
|
|
134
117
|
return
|
|
135
118
|
|
|
136
119
|
for i, (pool_name, weight) in enumerate(endpoint_costs.costs):
|
|
137
|
-
pool = self.
|
|
120
|
+
pool = self._pools.get(pool_name)
|
|
138
121
|
if pool is None:
|
|
139
122
|
continue
|
|
140
|
-
|
|
141
123
|
actual_weight = weight_override if (weight_override is not None and i == 0) else weight
|
|
142
|
-
|
|
143
|
-
# Wait for gate to be open
|
|
144
|
-
gate = self._gates.get(pool_name)
|
|
145
|
-
if gate is not None and not gate.is_set():
|
|
146
|
-
try:
|
|
147
|
-
await asyncio.wait_for(gate.wait(), timeout=self._config.gate_max_wait)
|
|
148
|
-
except asyncio.TimeoutError:
|
|
149
|
-
raise RateLimitGateTimeout(
|
|
150
|
-
f"{self._exchange}: gate for pool '{pool_name}' did not reopen "
|
|
151
|
-
f"within {self._config.gate_max_wait:.0f}s"
|
|
152
|
-
) from None
|
|
153
|
-
|
|
154
|
-
# For quota pools, check remaining (no time-based refill)
|
|
155
|
-
if pool.pool_type == "quota":
|
|
156
|
-
remaining = self._quota_remaining.get(pool_name, 0)
|
|
157
|
-
if remaining <= 0:
|
|
158
|
-
# Gate should already be closed, but double-check
|
|
159
|
-
self._close_gate(pool_name, pool.cooldown, "quota depleted")
|
|
160
|
-
gate = self._gates.get(pool_name)
|
|
161
|
-
if gate is not None:
|
|
162
|
-
try:
|
|
163
|
-
await asyncio.wait_for(gate.wait(), timeout=self._config.gate_max_wait)
|
|
164
|
-
except asyncio.TimeoutError:
|
|
165
|
-
raise RateLimitGateTimeout(
|
|
166
|
-
f"{self._exchange}: quota pool '{pool_name}' depleted, "
|
|
167
|
-
f"gate did not reopen within {self._config.gate_max_wait:.0f}s"
|
|
168
|
-
) from None
|
|
169
|
-
# Decrement quota locally
|
|
170
|
-
if pool_name in self._quota_remaining:
|
|
171
|
-
self._quota_remaining[pool_name] = max(0, self._quota_remaining[pool_name] - actual_weight)
|
|
172
|
-
continue
|
|
173
|
-
|
|
174
|
-
# For rate pools, acquire from backend
|
|
175
|
-
key = self._key_for(pool)
|
|
176
|
-
wait_time = await self._backend.acquire(key, actual_weight, pool.capacity, pool.refill_rate)
|
|
177
|
-
|
|
178
|
-
# Track stats
|
|
179
|
-
self._total_wait[pool_name] = self._total_wait.get(pool_name, 0) + wait_time
|
|
180
|
-
self._consumed[pool_name] = self._consumed.get(pool_name, 0) + actual_weight
|
|
124
|
+
await pool.acquire(actual_weight, self._config.gate_max_wait)
|
|
181
125
|
|
|
182
126
|
def report_limit_hit(
|
|
183
127
|
self,
|
|
@@ -197,7 +141,7 @@ class ExchangeRateLimiter:
|
|
|
197
141
|
retry_after: Seconds to wait (from Retry-After header)
|
|
198
142
|
reason: Human-readable reason for logging
|
|
199
143
|
"""
|
|
200
|
-
pools_to_close = []
|
|
144
|
+
pools_to_close: list[str] = []
|
|
201
145
|
|
|
202
146
|
if pool_name:
|
|
203
147
|
pools_to_close.append(pool_name)
|
|
@@ -206,15 +150,17 @@ class ExchangeRateLimiter:
|
|
|
206
150
|
pools_to_close = [p for p, _ in costs.costs]
|
|
207
151
|
else:
|
|
208
152
|
# Close all rate pools
|
|
209
|
-
pools_to_close = [
|
|
153
|
+
pools_to_close = [
|
|
154
|
+
name for name, pool in self._pools.items() if pool.config.pool_type == "rate"
|
|
155
|
+
]
|
|
210
156
|
|
|
211
157
|
for pname in pools_to_close:
|
|
212
|
-
pool = self.
|
|
158
|
+
pool = self._pools.get(pname)
|
|
213
159
|
if pool is None:
|
|
214
160
|
continue
|
|
215
|
-
cooldown = retry_after if retry_after is not None else pool.cooldown
|
|
216
|
-
|
|
217
|
-
|
|
161
|
+
cooldown = retry_after if retry_after is not None else pool.config.cooldown
|
|
162
|
+
pool.close_gate(cooldown, reason or f"rate limit hit on {pname}")
|
|
163
|
+
pool.hits += 1
|
|
218
164
|
|
|
219
165
|
def sync_from_exchange(
|
|
220
166
|
self,
|
|
@@ -235,11 +181,11 @@ class ExchangeRateLimiter:
|
|
|
235
181
|
used: Used tokens reported by exchange (remaining = capacity - used)
|
|
236
182
|
capacity: Total capacity reported by exchange (overrides config if provided)
|
|
237
183
|
"""
|
|
238
|
-
pool = self.
|
|
184
|
+
pool = self._pools.get(pool_name)
|
|
239
185
|
if pool is None:
|
|
240
186
|
return
|
|
241
187
|
|
|
242
|
-
actual_capacity = capacity or pool.capacity
|
|
188
|
+
actual_capacity = capacity or pool.config.capacity
|
|
243
189
|
|
|
244
190
|
if remaining is not None:
|
|
245
191
|
actual_remaining = remaining
|
|
@@ -248,77 +194,25 @@ class ExchangeRateLimiter:
|
|
|
248
194
|
else:
|
|
249
195
|
return
|
|
250
196
|
|
|
251
|
-
|
|
252
|
-
self._quota_remaining[pool_name] = actual_remaining
|
|
253
|
-
if actual_remaining <= 0:
|
|
254
|
-
self._close_gate(pool_name, pool.cooldown, f"exchange reports {pool_name} depleted")
|
|
255
|
-
return
|
|
256
|
-
|
|
257
|
-
# For rate pools, update backend
|
|
258
|
-
key = self._key_for(pool)
|
|
259
|
-
# Fire-and-forget since set_remaining may be async but we're in sync context
|
|
260
|
-
# Use a helper to run it
|
|
261
|
-
try:
|
|
262
|
-
loop = asyncio.get_running_loop()
|
|
263
|
-
loop.create_task(self._backend.set_remaining(key, actual_remaining))
|
|
264
|
-
except RuntimeError:
|
|
265
|
-
pass # No running loop — skip sync (e.g., during shutdown)
|
|
266
|
-
|
|
267
|
-
def sync_quota(self, pool_name: str, remaining: float) -> None:
|
|
268
|
-
"""Update externally-managed quota pool from exchange response.
|
|
197
|
+
pool.sync(actual_remaining, capacity)
|
|
269
198
|
|
|
270
|
-
|
|
271
|
-
|
|
199
|
+
def get_quota_remaining(self, pool_name: str) -> float:
|
|
200
|
+
"""Get remaining budget for a quota pool.
|
|
272
201
|
|
|
273
|
-
|
|
274
|
-
pool_name: Quota pool name
|
|
275
|
-
remaining: Remaining quota reported by exchange
|
|
202
|
+
Returns 0 if pool doesn't exist or isn't a quota pool.
|
|
276
203
|
"""
|
|
277
|
-
self.
|
|
278
|
-
if
|
|
279
|
-
pool
|
|
280
|
-
|
|
281
|
-
self._close_gate(pool_name, cooldown, f"quota {pool_name} depleted (remaining={remaining})")
|
|
282
|
-
|
|
283
|
-
def _close_gate(self, pool_name: str, cooldown: float, reason: str) -> None:
|
|
284
|
-
"""Close a pool's gate for the given cooldown period."""
|
|
285
|
-
gate = self._gates.get(pool_name)
|
|
286
|
-
if gate is None:
|
|
287
|
-
return
|
|
288
|
-
|
|
289
|
-
verb = "extended" if not gate.is_set() else "closed"
|
|
290
|
-
logger.warning(f"Rate limit gate {verb} for {self._exchange}:{pool_name} ({cooldown:.1f}s): {reason}")
|
|
291
|
-
gate.clear()
|
|
292
|
-
|
|
293
|
-
# Cancel existing reopen task
|
|
294
|
-
old_task = self._gate_tasks.get(pool_name)
|
|
295
|
-
if old_task is not None and not old_task.done():
|
|
296
|
-
old_task.cancel()
|
|
297
|
-
|
|
298
|
-
self._gate_tasks[pool_name] = asyncio.ensure_future(self._reopen_gate_after(pool_name, cooldown))
|
|
299
|
-
|
|
300
|
-
async def _reopen_gate_after(self, pool_name: str, delay: float) -> None:
|
|
301
|
-
"""Reopen a pool's gate after a cooldown delay."""
|
|
302
|
-
try:
|
|
303
|
-
await asyncio.sleep(delay)
|
|
304
|
-
gate = self._gates.get(pool_name)
|
|
305
|
-
if gate is not None:
|
|
306
|
-
gate.set()
|
|
307
|
-
logger.info(f"Rate limit gate reopened for {self._exchange}:{pool_name} after {delay:.1f}s")
|
|
308
|
-
except asyncio.CancelledError:
|
|
309
|
-
pass
|
|
204
|
+
pool = self._pools.get(pool_name)
|
|
205
|
+
if isinstance(pool, QuotaPool):
|
|
206
|
+
return pool.remaining
|
|
207
|
+
return 0
|
|
310
208
|
|
|
311
209
|
def reset_gates(self) -> None:
|
|
312
210
|
"""Reopen all gates and cancel pending reopen tasks.
|
|
313
211
|
|
|
314
212
|
Call on connection reset / reconnection.
|
|
315
213
|
"""
|
|
316
|
-
for
|
|
317
|
-
|
|
318
|
-
for pool_name, task in self._gate_tasks.items():
|
|
319
|
-
if not task.done():
|
|
320
|
-
task.cancel()
|
|
321
|
-
self._gate_tasks.clear()
|
|
214
|
+
for pool in self._pools.values():
|
|
215
|
+
pool.reset_gate()
|
|
322
216
|
|
|
323
217
|
def is_gate_closed(self, pool_name: str | None = None) -> bool:
|
|
324
218
|
"""Check if a gate is closed.
|
|
@@ -327,9 +221,9 @@ class ExchangeRateLimiter:
|
|
|
327
221
|
pool_name: Specific pool, or None to check if ANY gate is closed
|
|
328
222
|
"""
|
|
329
223
|
if pool_name:
|
|
330
|
-
|
|
331
|
-
return
|
|
332
|
-
return any(
|
|
224
|
+
pool = self._pools.get(pool_name)
|
|
225
|
+
return pool is not None and pool.is_gate_closed
|
|
226
|
+
return any(pool.is_gate_closed for pool in self._pools.values())
|
|
333
227
|
|
|
334
228
|
async def get_pool_state(self, pool_name: str) -> dict[str, Any] | None:
|
|
335
229
|
"""Get current state of a pool for monitoring.
|
|
@@ -337,32 +231,10 @@ class ExchangeRateLimiter:
|
|
|
337
231
|
Returns:
|
|
338
232
|
Dict with remaining, capacity, gate_closed, hits, etc. or None if pool doesn't exist
|
|
339
233
|
"""
|
|
340
|
-
pool = self.
|
|
234
|
+
pool = self._pools.get(pool_name)
|
|
341
235
|
if pool is None:
|
|
342
236
|
return None
|
|
343
|
-
|
|
344
|
-
if pool.pool_type == "quota":
|
|
345
|
-
remaining = self._quota_remaining.get(pool_name, 0)
|
|
346
|
-
else:
|
|
347
|
-
key = self._key_for(pool)
|
|
348
|
-
remaining = await self._backend.get_remaining(key, pool.capacity, pool.refill_rate)
|
|
349
|
-
if remaining is None:
|
|
350
|
-
remaining = pool.capacity
|
|
351
|
-
|
|
352
|
-
return {
|
|
353
|
-
"pool": pool_name,
|
|
354
|
-
"exchange": self._exchange,
|
|
355
|
-
"scope": pool.scope,
|
|
356
|
-
"scope_id": self._scope_ids.get(pool.scope, "local"),
|
|
357
|
-
"pool_type": pool.pool_type,
|
|
358
|
-
"remaining": remaining,
|
|
359
|
-
"capacity": pool.capacity,
|
|
360
|
-
"utilization": 1.0 - (remaining / pool.capacity) if pool.capacity > 0 else 0,
|
|
361
|
-
"gate_closed": self.is_gate_closed(pool_name),
|
|
362
|
-
"hits": self._hits.get(pool_name, 0),
|
|
363
|
-
"total_wait_s": self._total_wait.get(pool_name, 0),
|
|
364
|
-
"consumed": self._consumed.get(pool_name, 0),
|
|
365
|
-
}
|
|
237
|
+
return await pool.get_state()
|
|
366
238
|
|
|
367
239
|
async def collect_metrics(self) -> list[dict[str, Any]]:
|
|
368
240
|
"""Collect current metrics for all pools.
|
|
@@ -374,7 +246,7 @@ class ExchangeRateLimiter:
|
|
|
374
246
|
ctx.emitter.emit(m["name"], m["value"], m["tags"])
|
|
375
247
|
"""
|
|
376
248
|
metrics = []
|
|
377
|
-
for pool_name in self.
|
|
249
|
+
for pool_name in self._pools:
|
|
378
250
|
state = await self.get_pool_state(pool_name)
|
|
379
251
|
if state is None:
|
|
380
252
|
continue
|
|
@@ -399,5 +271,5 @@ class ExchangeRateLimiter:
|
|
|
399
271
|
return metrics
|
|
400
272
|
|
|
401
273
|
def __repr__(self) -> str:
|
|
402
|
-
pools = ", ".join(f"{name}({pool.scope})" for name, pool in self.
|
|
274
|
+
pools = ", ".join(f"{name}({pool.config.scope})" for name, pool in self._pools.items())
|
|
403
275
|
return f"ExchangeRateLimiter({self._exchange}, pools=[{pools}])"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Pool implementations for the rate limiting engine.
|
|
2
|
+
|
|
3
|
+
Each pool type encapsulates its own gate behavior, acquisition logic,
|
|
4
|
+
sync mechanism, and metrics state. The engine delegates to pools
|
|
5
|
+
polymorphically — no pool_type branching in the engine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from qubx import logger
|
|
12
|
+
|
|
13
|
+
from .backend import IRateLimitBackend
|
|
14
|
+
from .config import PoolConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RateLimitGateTimeout(Exception):
|
|
18
|
+
"""Raised when acquire() times out waiting for a gate to reopen."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, message: str, pool_name: str | None = None):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.pool_name = pool_name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BasePool:
|
|
26
|
+
"""Base pool with gate mechanism and metrics tracking."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, config: PoolConfig, exchange: str, scope_id: str):
|
|
29
|
+
self._config = config
|
|
30
|
+
self._exchange = exchange
|
|
31
|
+
self._scope_id = scope_id
|
|
32
|
+
self._gate = asyncio.Event()
|
|
33
|
+
self._gate.set()
|
|
34
|
+
self._gate_task: asyncio.Task | None = None
|
|
35
|
+
self.hits: int = 0
|
|
36
|
+
self.total_wait: float = 0
|
|
37
|
+
self.consumed: float = 0
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def name(self) -> str:
|
|
41
|
+
return self._config.name
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def config(self) -> PoolConfig:
|
|
45
|
+
return self._config
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def scope_id(self) -> str:
|
|
49
|
+
return self._scope_id
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_gate_closed(self) -> bool:
|
|
53
|
+
return not self._gate.is_set()
|
|
54
|
+
|
|
55
|
+
def update_scope_id(self, scope_id: str) -> None:
|
|
56
|
+
self._scope_id = scope_id
|
|
57
|
+
|
|
58
|
+
async def acquire(self, weight: float, gate_max_wait: float) -> None:
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
def close_gate(self, cooldown: float, reason: str) -> None:
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
def sync(self, remaining: float, capacity: float | None = None) -> None:
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
def reset_gate(self) -> None:
|
|
68
|
+
"""Reopen gate and cancel any pending reopen task."""
|
|
69
|
+
self._gate.set()
|
|
70
|
+
self._cancel_gate_task()
|
|
71
|
+
|
|
72
|
+
async def get_state(self) -> dict[str, Any]:
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
def _cancel_gate_task(self) -> None:
|
|
76
|
+
if self._gate_task is not None and not self._gate_task.done():
|
|
77
|
+
self._gate_task.cancel()
|
|
78
|
+
self._gate_task = None
|
|
79
|
+
|
|
80
|
+
def _base_state(self, remaining: float, capacity: float) -> dict[str, Any]:
|
|
81
|
+
return {
|
|
82
|
+
"pool": self.name,
|
|
83
|
+
"exchange": self._exchange,
|
|
84
|
+
"scope": self._config.scope,
|
|
85
|
+
"scope_id": self._scope_id,
|
|
86
|
+
"pool_type": self._config.pool_type,
|
|
87
|
+
"remaining": remaining,
|
|
88
|
+
"capacity": capacity,
|
|
89
|
+
"utilization": 1.0 - (remaining / capacity) if capacity > 0 else 0,
|
|
90
|
+
"gate_closed": self.is_gate_closed,
|
|
91
|
+
"hits": self.hits,
|
|
92
|
+
"total_wait_s": self.total_wait,
|
|
93
|
+
"consumed": self.consumed,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class RatePool(BasePool):
|
|
98
|
+
"""Time-based token bucket pool. Gate reopens on timer after cooldown."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, config: PoolConfig, exchange: str, scope_id: str, backend: IRateLimitBackend):
|
|
101
|
+
super().__init__(config, exchange, scope_id)
|
|
102
|
+
self._backend = backend
|
|
103
|
+
self._key = self._make_key()
|
|
104
|
+
|
|
105
|
+
def _make_key(self) -> str:
|
|
106
|
+
return f"ratelimit:{self._exchange}:{self._config.name}:{self._scope_id}"
|
|
107
|
+
|
|
108
|
+
def update_scope_id(self, scope_id: str) -> None:
|
|
109
|
+
super().update_scope_id(scope_id)
|
|
110
|
+
self._key = self._make_key()
|
|
111
|
+
|
|
112
|
+
async def acquire(self, weight: float, gate_max_wait: float) -> None:
|
|
113
|
+
if not self._gate.is_set():
|
|
114
|
+
try:
|
|
115
|
+
await asyncio.wait_for(self._gate.wait(), timeout=gate_max_wait)
|
|
116
|
+
except asyncio.TimeoutError:
|
|
117
|
+
raise RateLimitGateTimeout(
|
|
118
|
+
f"{self._exchange}: gate for pool '{self.name}' did not reopen "
|
|
119
|
+
f"within {gate_max_wait:.0f}s",
|
|
120
|
+
pool_name=self.name,
|
|
121
|
+
) from None
|
|
122
|
+
|
|
123
|
+
wait_time = await self._backend.acquire(self._key, weight, self._config.capacity, self._config.refill_rate)
|
|
124
|
+
self.total_wait += wait_time
|
|
125
|
+
self.consumed += weight
|
|
126
|
+
|
|
127
|
+
def close_gate(self, cooldown: float, reason: str) -> None:
|
|
128
|
+
verb = "extended" if not self._gate.is_set() else "closed"
|
|
129
|
+
logger.warning(f"Rate limit gate {verb} for {self._exchange}:{self.name} ({cooldown:.1f}s): {reason}")
|
|
130
|
+
self._gate.clear()
|
|
131
|
+
self._cancel_gate_task()
|
|
132
|
+
self._gate_task = asyncio.ensure_future(self._reopen_after(cooldown))
|
|
133
|
+
|
|
134
|
+
async def _reopen_after(self, delay: float) -> None:
|
|
135
|
+
try:
|
|
136
|
+
await asyncio.sleep(delay)
|
|
137
|
+
self._gate.set()
|
|
138
|
+
logger.info(f"Rate limit gate reopened for {self._exchange}:{self.name} after {delay:.1f}s")
|
|
139
|
+
except asyncio.CancelledError:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def sync(self, remaining: float, capacity: float | None = None) -> None:
|
|
143
|
+
try:
|
|
144
|
+
loop = asyncio.get_running_loop()
|
|
145
|
+
loop.create_task(self._backend.set_remaining(self._key, remaining))
|
|
146
|
+
except RuntimeError:
|
|
147
|
+
pass # No running loop — skip sync (e.g., during shutdown)
|
|
148
|
+
|
|
149
|
+
async def get_state(self) -> dict[str, Any]:
|
|
150
|
+
remaining = await self._backend.get_remaining(self._key, self._config.capacity, self._config.refill_rate)
|
|
151
|
+
if remaining is None:
|
|
152
|
+
remaining = self._config.capacity
|
|
153
|
+
return self._base_state(remaining, self._config.capacity)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class QuotaPool(BasePool):
|
|
157
|
+
"""Externally-managed quota pool. Gate reopens only via sync() or reset_gate()."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, config: PoolConfig, exchange: str, scope_id: str):
|
|
160
|
+
super().__init__(config, exchange, scope_id)
|
|
161
|
+
self._remaining: float = config.capacity
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def remaining(self) -> float:
|
|
165
|
+
return self._remaining
|
|
166
|
+
|
|
167
|
+
async def acquire(self, weight: float, gate_max_wait: float) -> None:
|
|
168
|
+
if not self._gate.is_set() or self._remaining <= 0:
|
|
169
|
+
if self._remaining <= 0:
|
|
170
|
+
self.close_gate(self._config.cooldown, "quota depleted")
|
|
171
|
+
raise RateLimitGateTimeout(
|
|
172
|
+
f"{self._exchange}: quota pool '{self.name}' depleted",
|
|
173
|
+
pool_name=self.name,
|
|
174
|
+
)
|
|
175
|
+
self._remaining = max(0, self._remaining - weight)
|
|
176
|
+
self.consumed += weight
|
|
177
|
+
|
|
178
|
+
def close_gate(self, cooldown: float, reason: str) -> None:
|
|
179
|
+
verb = "extended" if not self._gate.is_set() else "closed"
|
|
180
|
+
logger.warning(f"Rate limit gate {verb} for {self._exchange}:{self.name} ({cooldown:.1f}s): {reason}")
|
|
181
|
+
self._gate.clear()
|
|
182
|
+
self._cancel_gate_task()
|
|
183
|
+
|
|
184
|
+
def sync(self, remaining: float, capacity: float | None = None) -> None:
|
|
185
|
+
self._remaining = remaining
|
|
186
|
+
if remaining <= 0:
|
|
187
|
+
self.close_gate(self._config.cooldown, f"quota {self.name} depleted (remaining={remaining})")
|
|
188
|
+
else:
|
|
189
|
+
# When no explicit capacity provided, grow capacity to track the
|
|
190
|
+
# real account quota (best approximation when exchange only reports remaining)
|
|
191
|
+
if capacity is None:
|
|
192
|
+
self._config.capacity = max(self._config.capacity, remaining)
|
|
193
|
+
if not self._gate.is_set():
|
|
194
|
+
self._cancel_gate_task()
|
|
195
|
+
self._gate.set()
|
|
196
|
+
logger.info(
|
|
197
|
+
f"Rate limit gate reopened for {self._exchange}:{self.name} (remaining={remaining})"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def get_state(self) -> dict[str, Any]:
|
|
201
|
+
return self._base_state(self._remaining, self._config.capacity)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|