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.
Files changed (310) hide show
  1. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/PKG-INFO +1 -1
  2. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/_version.py +2 -2
  3. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/engine.py +47 -175
  4. qubx-1.2.2.dev1/src/qubx/rate_limiting/pools.py +201 -0
  5. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/.gitignore +0 -0
  6. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/LICENSE +0 -0
  7. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/README.md +0 -0
  8. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/pyproject.toml +0 -0
  9. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/scripts/build.py +0 -0
  10. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/__init__.py +0 -0
  11. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/_nb_magic.py +0 -0
  12. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/__init__.py +0 -0
  13. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/account.py +0 -0
  14. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/broker.py +0 -0
  15. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/data.py +0 -0
  16. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/iteratedstream.py +0 -0
  17. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/management.py +0 -0
  18. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/ome.py +0 -0
  19. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/optimization.py +0 -0
  20. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/runner.py +0 -0
  21. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/sentinels.py +0 -0
  22. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/simulated_data.py +0 -0
  23. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/simulated_exchange.py +0 -0
  24. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/simulator.py +0 -0
  25. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/transfers.py +0 -0
  26. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/backtester/utils.py +0 -0
  27. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/__init__.py +0 -0
  28. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/commands.py +0 -0
  29. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/deploy.py +0 -0
  30. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/misc.py +0 -0
  31. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/release.py +0 -0
  32. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/resolver.py +0 -0
  33. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/s3.py +0 -0
  34. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/theme.py +0 -0
  35. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/tui.py +0 -0
  36. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/cli/user_config.py +0 -0
  37. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/config.py +0 -0
  38. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/__init__.py +0 -0
  39. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/__init__.py +0 -0
  40. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/account.py +0 -0
  41. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  42. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  43. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/broker.py +0 -0
  44. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  45. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/data.py +0 -0
  46. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  47. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  48. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  49. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  50. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  51. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  52. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  53. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  54. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  55. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  56. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  57. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  58. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  59. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  60. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  61. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  62. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/account.py +0 -0
  63. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
  64. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  65. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/factory.py +0 -0
  66. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  67. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  68. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  69. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  70. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  71. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  72. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  73. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  74. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  75. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  76. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
  77. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  78. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  79. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  80. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/utils.py +0 -0
  81. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  82. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/registry.py +0 -0
  83. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/tardis/data.py +0 -0
  84. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/connectors/tardis/utils.py +0 -0
  85. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/__init__.py +0 -0
  86. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/builtin.py +0 -0
  87. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/decorator.py +0 -0
  88. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/executor.py +0 -0
  89. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/interfaces.py +0 -0
  90. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/server.py +0 -0
  91. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/control/types.py +0 -0
  92. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/__init__.py +0 -0
  93. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/account.py +0 -0
  94. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/basics.py +0 -0
  95. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/context.py +0 -0
  96. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/detectors/__init__.py +0 -0
  97. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/detectors/delisting.py +0 -0
  98. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/detectors/stale.py +0 -0
  99. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/errors.py +0 -0
  100. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/exceptions.py +0 -0
  101. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/helpers.py +0 -0
  102. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/initializer.py +0 -0
  103. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/interfaces.py +0 -0
  104. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/loggers.py +0 -0
  105. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/lookups.py +0 -0
  106. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/metrics.py +0 -0
  107. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/__init__.py +0 -0
  108. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/market.py +0 -0
  109. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/processing.py +0 -0
  110. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/subscription.py +0 -0
  111. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/trading.py +0 -0
  112. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/universe.py +0 -0
  113. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/mixins/utils.py +0 -0
  114. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/series.pxd +0 -0
  115. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/series.pyi +0 -0
  116. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/series.pyx +0 -0
  117. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/utils.pyi +0 -0
  118. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/core/utils.pyx +0 -0
  119. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/__init__.py +0 -0
  120. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/cache.py +0 -0
  121. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/containers.py +0 -0
  122. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/guards.py +0 -0
  123. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/registry.py +0 -0
  124. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storage.py +0 -0
  125. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/ccxt.py +0 -0
  126. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/csv.py +0 -0
  127. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/handy.py +0 -0
  128. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/multi.py +0 -0
  129. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/questdb.py +0 -0
  130. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/stub.py +0 -0
  131. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/storages/utils.py +0 -0
  132. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/data/transformers.py +0 -0
  133. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/__init__.py +0 -0
  134. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/base.py +0 -0
  135. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/composite.py +0 -0
  136. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/csv.py +0 -0
  137. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/indicator.py +0 -0
  138. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/inmemory.py +0 -0
  139. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/prometheus.py +0 -0
  140. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/emitters/questdb.py +0 -0
  141. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/__init__.py +0 -0
  142. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/composite.py +0 -0
  143. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/__init__.py +0 -0
  144. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/base.py +0 -0
  145. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/incremental.py +0 -0
  146. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/slack.py +0 -0
  147. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/formatters/target_position.py +0 -0
  148. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/redis_streams.py +0 -0
  149. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/exporters/slack.py +0 -0
  150. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/gathering/simplest.py +0 -0
  151. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/health/__init__.py +0 -0
  152. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/health/base.py +0 -0
  153. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/health/dummy.py +0 -0
  154. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/__init__.py +0 -0
  155. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/csv.py +0 -0
  156. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/factory.py +0 -0
  157. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/inmemory.py +0 -0
  158. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/mongo.py +0 -0
  159. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/loggers/postgres.py +0 -0
  160. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/notifications/__init__.py +0 -0
  161. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/notifications/composite.py +0 -0
  162. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/notifications/slack.py +0 -0
  163. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/notifications/throttler.py +0 -0
  164. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/pandaz/__init__.py +0 -0
  165. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/pandaz/stats.py +0 -0
  166. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/pandaz/ta.py +0 -0
  167. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/pandaz/utils.py +0 -0
  168. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/plugins/__init__.py +0 -0
  169. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/plugins/loader.py +0 -0
  170. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/__init__.py +0 -0
  171. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/backend.py +0 -0
  172. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/config.py +0 -0
  173. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/ip_resolver.py +0 -0
  174. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/manager.py +0 -0
  175. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/rate_limiting/redis_backend.py +0 -0
  176. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/_build.py +0 -0
  177. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/crypto-fees.ini +0 -0
  178. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  179. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  180. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  181. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  182. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  183. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  184. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  185. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  186. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  187. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  188. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  189. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restarts/__init__.py +0 -0
  190. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restarts/state_resolvers.py +0 -0
  191. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restarts/time_finders.py +0 -0
  192. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/__init__.py +0 -0
  193. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/balance.py +0 -0
  194. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/factory.py +0 -0
  195. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/interfaces.py +0 -0
  196. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/position.py +0 -0
  197. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/signal.py +0 -0
  198. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/state.py +0 -0
  199. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/restorers/utils.py +0 -0
  200. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/state/__init__.py +0 -0
  201. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/state/dummy.py +0 -0
  202. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/state/redis.py +0 -0
  203. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/ta/__init__.py +0 -0
  204. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/ta/indicators.pxd +0 -0
  205. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/ta/indicators.pyi +0 -0
  206. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/ta/indicators.pyx +0 -0
  207. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/__init__.py +0 -0
  208. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/base.py +0 -0
  209. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  210. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/config.yml.j2 +0 -0
  211. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  212. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  213. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  214. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  215. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  216. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/project/template.yml +0 -0
  217. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  218. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  219. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  220. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  221. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  222. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  223. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.python-version +0 -0
  224. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  225. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  226. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  227. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  228. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/README.md.j2 +0 -0
  229. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  230. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/justfile.j2 +0 -0
  231. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  232. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/research/.gitkeep +0 -0
  233. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  234. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  235. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  236. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/template.yml +0 -0
  237. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  238. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  239. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  240. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/config.yml.j2 +0 -0
  241. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  242. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  243. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  244. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/templates/simple/template.yml +0 -0
  245. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/__init__.py +0 -0
  246. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/advanced.py +0 -0
  247. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/composite.py +0 -0
  248. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/rebalancers.py +0 -0
  249. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/riskctrl.py +0 -0
  250. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/trackers/sizers.py +0 -0
  251. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/__init__.py +0 -0
  252. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/_pyxreloader.py +0 -0
  253. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/charting/lookinglass.py +0 -0
  254. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  255. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/charting/orderbook.py +0 -0
  256. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/collections.py +0 -0
  257. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/hft/__init__.py +0 -0
  258. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/hft/numba_utils.py +0 -0
  259. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/hft/orderbook.pyi +0 -0
  260. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/hft/orderbook.pyx +0 -0
  261. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/marketdata/binance.py +0 -0
  262. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/marketdata/ccxt.py +0 -0
  263. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/marketdata/dukas.py +0 -0
  264. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/misc.py +0 -0
  265. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/nonce.py +0 -0
  266. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/ntp.py +0 -0
  267. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/numbers_utils.py +0 -0
  268. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/orderbook.py +0 -0
  269. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/__init__.py +0 -0
  270. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/dashboard.py +0 -0
  271. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/data.py +0 -0
  272. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/interfaces.py +0 -0
  273. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  274. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  275. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/questdb.py +0 -0
  276. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/rate_limiter.py +0 -0
  277. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/results.py +0 -0
  278. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/ringbuffer.pxd +0 -0
  279. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/ringbuffer.pyi +0 -0
  280. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/ringbuffer.pyx +0 -0
  281. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/__init__.py +0 -0
  282. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  283. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/accounts.py +0 -0
  284. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/configs.py +0 -0
  285. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/factory.py +0 -0
  286. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/kernel_service.py +0 -0
  287. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/runner.py +0 -0
  288. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/__init__.py +0 -0
  289. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/app.py +0 -0
  290. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/handlers.py +0 -0
  291. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/init_code.py +0 -0
  292. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/kernel.py +0 -0
  293. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  294. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  295. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
  296. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  297. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  298. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  299. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  300. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  301. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  302. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/s3.py +0 -0
  303. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/slack.py +0 -0
  304. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/throttler.py +0 -0
  305. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/time.py +0 -0
  306. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/src/qubx/utils/websocket_manager.py +0 -0
  307. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  308. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  309. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  310. {qubx-1.2.1.dev1 → qubx-1.2.2.dev1}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 1.2.1.dev1
3
+ Version: 1.2.2.dev1
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Project-URL: homepage, https://xlydian.com
6
6
  Project-URL: repository, https://github.com/xLydianSoftware/Qubx
@@ -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.1.dev1'
22
- __version_tuple__ = version_tuple = (1, 2, 1, 'dev1')
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. Handles multi-pool
5
- token acquisition, gate mechanism for cooldowns, exchange state sync,
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, PoolConfig
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._config.pools.get(pool_name)
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 = [name for name, pool in self._config.pools.items() if pool.pool_type == "rate"]
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._config.pools.get(pname)
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
- self._close_gate(pname, cooldown, reason or f"rate limit hit on {pname}")
217
- self._hits[pname] = self._hits.get(pname, 0) + 1
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._config.pools.get(pool_name)
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
- if pool.pool_type == "quota":
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
- Convenience wrapper for quota pools (e.g., Lighter volume quota
271
- reported in sendTx responses).
199
+ def get_quota_remaining(self, pool_name: str) -> float:
200
+ """Get remaining budget for a quota pool.
272
201
 
273
- Args:
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._quota_remaining[pool_name] = remaining
278
- if remaining <= 0:
279
- pool = self._config.pools.get(pool_name)
280
- cooldown = pool.cooldown if pool else 15.0
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 pool_name, gate in self._gates.items():
317
- gate.set()
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
- gate = self._gates.get(pool_name)
331
- return gate is not None and not gate.is_set()
332
- return any(not gate.is_set() for gate in self._gates.values())
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._config.pools.get(pool_name)
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._config.pools:
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._config.pools.items())
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