Qubx 1.1.3.dev10__tar.gz → 1.1.3.dev12__tar.gz

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