Qubx 1.1.3.dev13__tar.gz → 1.1.3.dev14__tar.gz

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