Qubx 1.2.3.dev1__tar.gz → 1.2.3.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/PKG-INFO +1 -1
  2. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/_version.py +2 -2
  3. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchange_manager.py +38 -3
  4. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/rate_limits.py +18 -0
  5. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/storages/ccxt.py +212 -10
  6. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/transformers.py +1 -1
  7. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/pandaz/ta.py +1 -1
  8. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/trackers/riskctrl.py +4 -2
  9. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/trackers/sizers.py +18 -2
  10. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/runner.py +26 -0
  11. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/.gitignore +0 -0
  12. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/LICENSE +0 -0
  13. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/README.md +0 -0
  14. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/pyproject.toml +0 -0
  15. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/scripts/build.py +0 -0
  16. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/__init__.py +0 -0
  17. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/_nb_magic.py +0 -0
  18. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/__init__.py +0 -0
  19. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/account.py +0 -0
  20. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/broker.py +0 -0
  21. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/data.py +0 -0
  22. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/iteratedstream.py +0 -0
  23. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/management.py +0 -0
  24. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/ome.py +0 -0
  25. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/optimization.py +0 -0
  26. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/runner.py +0 -0
  27. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/sentinels.py +0 -0
  28. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/simulated_data.py +0 -0
  29. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/simulated_exchange.py +0 -0
  30. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/simulator.py +0 -0
  31. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/transfers.py +0 -0
  32. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/backtester/utils.py +0 -0
  33. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/__init__.py +0 -0
  34. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/commands.py +0 -0
  35. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/deploy.py +0 -0
  36. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/misc.py +0 -0
  37. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/release.py +0 -0
  38. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/resolver.py +0 -0
  39. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/s3.py +0 -0
  40. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/theme.py +0 -0
  41. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/tui.py +0 -0
  42. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/cli/user_config.py +0 -0
  43. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/config.py +0 -0
  44. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/__init__.py +0 -0
  45. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/__init__.py +0 -0
  46. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/account.py +0 -0
  47. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  48. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  49. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/broker.py +0 -0
  50. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  51. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/data.py +0 -0
  52. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  53. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  54. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  55. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  56. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  57. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  58. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  59. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  60. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  61. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  62. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  63. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  64. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  65. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  66. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  67. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/okx/account.py +0 -0
  68. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
  69. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  70. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/factory.py +0 -0
  71. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  72. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  73. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  74. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  75. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  76. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  77. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  78. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  79. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  80. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  81. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  82. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  83. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  84. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/utils.py +0 -0
  85. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  86. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/registry.py +0 -0
  87. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/tardis/data.py +0 -0
  88. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/connectors/tardis/utils.py +0 -0
  89. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/control/__init__.py +0 -0
  90. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/control/builtin.py +0 -0
  91. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/control/decorator.py +0 -0
  92. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/control/executor.py +0 -0
  93. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/control/interfaces.py +0 -0
  94. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/control/server.py +0 -0
  95. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/control/types.py +0 -0
  96. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/__init__.py +0 -0
  97. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/account.py +0 -0
  98. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/basics.py +0 -0
  99. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/context.py +0 -0
  100. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/detectors/__init__.py +0 -0
  101. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/detectors/delisting.py +0 -0
  102. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/detectors/stale.py +0 -0
  103. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/errors.py +0 -0
  104. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/exceptions.py +0 -0
  105. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/helpers.py +0 -0
  106. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/initializer.py +0 -0
  107. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/interfaces.py +0 -0
  108. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/loggers.py +0 -0
  109. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/lookups.py +0 -0
  110. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/metrics.py +0 -0
  111. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/mixins/__init__.py +0 -0
  112. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/mixins/market.py +0 -0
  113. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/mixins/processing.py +0 -0
  114. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/mixins/subscription.py +0 -0
  115. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/mixins/trading.py +0 -0
  116. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/mixins/universe.py +0 -0
  117. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/mixins/utils.py +0 -0
  118. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/series.pxd +0 -0
  119. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/series.pyi +0 -0
  120. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/series.pyx +0 -0
  121. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/utils.pyi +0 -0
  122. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/core/utils.pyx +0 -0
  123. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/__init__.py +0 -0
  124. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/cache.py +0 -0
  125. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/containers.py +0 -0
  126. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/guards.py +0 -0
  127. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/registry.py +0 -0
  128. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/storage.py +0 -0
  129. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/storages/csv.py +0 -0
  130. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/storages/handy.py +0 -0
  131. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/storages/multi.py +0 -0
  132. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/storages/questdb.py +0 -0
  133. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/storages/stub.py +0 -0
  134. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/data/storages/utils.py +0 -0
  135. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/emitters/__init__.py +0 -0
  136. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/emitters/base.py +0 -0
  137. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/emitters/composite.py +0 -0
  138. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/emitters/csv.py +0 -0
  139. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/emitters/indicator.py +0 -0
  140. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/emitters/inmemory.py +0 -0
  141. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/emitters/prometheus.py +0 -0
  142. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/emitters/questdb.py +0 -0
  143. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/__init__.py +0 -0
  144. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/composite.py +0 -0
  145. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/formatters/__init__.py +0 -0
  146. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/formatters/base.py +0 -0
  147. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/formatters/incremental.py +0 -0
  148. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/formatters/slack.py +0 -0
  149. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/formatters/target_position.py +0 -0
  150. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/redis_streams.py +0 -0
  151. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/exporters/slack.py +0 -0
  152. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/gathering/simplest.py +0 -0
  153. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/health/__init__.py +0 -0
  154. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/health/base.py +0 -0
  155. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/health/dummy.py +0 -0
  156. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/loggers/__init__.py +0 -0
  157. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/loggers/csv.py +0 -0
  158. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/loggers/factory.py +0 -0
  159. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/loggers/inmemory.py +0 -0
  160. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/loggers/mongo.py +0 -0
  161. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/loggers/postgres.py +0 -0
  162. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/notifications/__init__.py +0 -0
  163. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/notifications/composite.py +0 -0
  164. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/notifications/slack.py +0 -0
  165. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/notifications/throttler.py +0 -0
  166. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/pandaz/__init__.py +0 -0
  167. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/pandaz/stats.py +0 -0
  168. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/pandaz/utils.py +0 -0
  169. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/plugins/__init__.py +0 -0
  170. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/plugins/loader.py +0 -0
  171. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/rate_limiting/__init__.py +0 -0
  172. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/rate_limiting/backend.py +0 -0
  173. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/rate_limiting/config.py +0 -0
  174. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/rate_limiting/engine.py +0 -0
  175. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/rate_limiting/ip_resolver.py +0 -0
  176. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/rate_limiting/manager.py +0 -0
  177. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/rate_limiting/pools.py +0 -0
  178. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/rate_limiting/redis_backend.py +0 -0
  179. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/_build.py +0 -0
  180. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/crypto-fees.ini +0 -0
  181. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  182. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  183. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  184. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  185. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  186. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  187. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  188. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  189. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  190. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  191. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  192. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restarts/__init__.py +0 -0
  193. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restarts/state_resolvers.py +0 -0
  194. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restarts/time_finders.py +0 -0
  195. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restorers/__init__.py +0 -0
  196. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restorers/balance.py +0 -0
  197. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restorers/factory.py +0 -0
  198. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restorers/interfaces.py +0 -0
  199. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restorers/position.py +0 -0
  200. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restorers/signal.py +0 -0
  201. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restorers/state.py +0 -0
  202. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/restorers/utils.py +0 -0
  203. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/state/__init__.py +0 -0
  204. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/state/dummy.py +0 -0
  205. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/state/redis.py +0 -0
  206. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/ta/__init__.py +0 -0
  207. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/ta/indicators.pxd +0 -0
  208. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/ta/indicators.pyi +0 -0
  209. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/ta/indicators.pyx +0 -0
  210. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/__init__.py +0 -0
  211. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/base.py +0 -0
  212. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  213. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/project/config.yml.j2 +0 -0
  214. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  215. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  216. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  217. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  218. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  219. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/project/template.yml +0 -0
  220. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  221. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  222. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  223. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  224. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  225. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  226. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.python-version +0 -0
  227. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  228. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  229. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  230. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  231. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/README.md.j2 +0 -0
  232. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  233. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/justfile.j2 +0 -0
  234. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  235. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/research/.gitkeep +0 -0
  236. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  237. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  238. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  239. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/template.yml +0 -0
  240. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  241. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  242. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  243. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/simple/config.yml.j2 +0 -0
  244. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  245. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  246. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  247. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/templates/simple/template.yml +0 -0
  248. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/trackers/__init__.py +0 -0
  249. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/trackers/advanced.py +0 -0
  250. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/trackers/composite.py +0 -0
  251. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/trackers/rebalancers.py +0 -0
  252. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/__init__.py +0 -0
  253. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/_pyxreloader.py +0 -0
  254. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/charting/lookinglass.py +0 -0
  255. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  256. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/charting/orderbook.py +0 -0
  257. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/collections.py +0 -0
  258. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/hft/__init__.py +0 -0
  259. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/hft/numba_utils.py +0 -0
  260. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/hft/orderbook.pyi +0 -0
  261. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/hft/orderbook.pyx +0 -0
  262. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/marketdata/binance.py +0 -0
  263. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/marketdata/ccxt.py +0 -0
  264. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/marketdata/dukas.py +0 -0
  265. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/misc.py +0 -0
  266. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/nonce.py +0 -0
  267. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/ntp.py +0 -0
  268. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/numbers_utils.py +0 -0
  269. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/orderbook.py +0 -0
  270. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/plotting/__init__.py +0 -0
  271. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/plotting/dashboard.py +0 -0
  272. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/plotting/data.py +0 -0
  273. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/plotting/interfaces.py +0 -0
  274. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  275. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  276. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/questdb.py +0 -0
  277. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/rate_limiter.py +0 -0
  278. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/results.py +0 -0
  279. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/ringbuffer.pxd +0 -0
  280. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/ringbuffer.pyi +0 -0
  281. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/ringbuffer.pyx +0 -0
  282. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/__init__.py +0 -0
  283. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  284. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/accounts.py +0 -0
  285. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/configs.py +0 -0
  286. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/factory.py +0 -0
  287. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/kernel_service.py +0 -0
  288. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/__init__.py +0 -0
  289. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/app.py +0 -0
  290. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/handlers.py +0 -0
  291. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/init_code.py +0 -0
  292. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/kernel.py +0 -0
  293. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  294. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  295. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
  296. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  297. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  298. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  299. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  300. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  301. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  302. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/s3.py +0 -0
  303. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/slack.py +0 -0
  304. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/throttler.py +0 -0
  305. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/time.py +0 -0
  306. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/src/qubx/utils/websocket_manager.py +0 -0
  307. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  308. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  309. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  310. {qubx-1.2.3.dev1 → qubx-1.2.3.dev3}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 1.2.3.dev1
3
+ Version: 1.2.3.dev3
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Project-URL: homepage, https://xlydian.com
6
6
  Project-URL: repository, https://github.com/xLydianSoftware/Qubx
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.2.3.dev1'
22
- __version_tuple__ = version_tuple = (1, 2, 3, 'dev1')
21
+ __version__ = version = '1.2.3.dev3'
22
+ __version_tuple__ = version_tuple = (1, 2, 3, 'dev3')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -271,14 +271,32 @@ class ExchangeManager:
271
271
  logger.info(f"Rate limiter attached to {self._exchange_name}")
272
272
 
273
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."""
274
+ """Install our throttle and response-header-sync hooks on *exchange*.
275
+
276
+ Two hooks are installed per exchange:
277
+
278
+ 1. **Throttle (pre-request)** — replaces CCXT's built-in throttle so every
279
+ REST call blocks on our token bucket first. ``acquire`` forwards the
280
+ CCXT-provided cost to the limiter as ``weight_override``.
281
+
282
+ 2. **Response hook (post-request)** — wraps CCXT's ``on_rest_response`` to
283
+ invoke the exchange-specific header parser (OKX ``x-ratelimit-*``,
284
+ Binance ``X-MBX-USED-WEIGHT-1M``, etc.). The parser calls
285
+ ``rate_limiter.sync_from_exchange`` so our modeled budget tracks the
286
+ exchange's reported usage. Unlike ``CcxtStorage``'s best-effort sync,
287
+ this path runs atomically inside CCXT's fetch pipeline — no
288
+ concurrent-fetch race on ``last_response_headers``.
289
+
290
+ Both hooks are reapplied automatically after exchange recreation via
291
+ ``register_recreation_callback`` in :meth:`attach_rate_limiter`.
292
+ """
275
293
  rate_limiter = getattr(self, "_rate_limiter", None)
276
294
  if rate_limiter is None:
277
295
  return
278
296
 
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
297
+ # 1) Throttle override pre-request acquisition.
281
298
  exchange.enableRateLimit = True
299
+
282
300
  async def _rate_limit_throttle(cost=None):
283
301
  if cost is None:
284
302
  cost = 1.0
@@ -286,6 +304,23 @@ class ExchangeManager:
286
304
 
287
305
  exchange.throttle = _rate_limit_throttle
288
306
 
307
+ # 2) Response hook — post-response state sync.
308
+ from qubx.connectors.ccxt.rate_limits import get_header_parser
309
+
310
+ parser = get_header_parser(getattr(exchange, "id", ""))
311
+ if parser is not None:
312
+ _orig_on_rest = exchange.on_rest_response
313
+
314
+ def _header_sync_hook(code, reason, url, method, headers, body, req_headers, req_body):
315
+ try:
316
+ if headers:
317
+ parser(headers, rate_limiter)
318
+ except Exception as e:
319
+ logger.debug(f"[{self._exchange_name}] header sync failed (non-fatal): {e}")
320
+ return _orig_on_rest(code, reason, url, method, headers, body, req_headers, req_body)
321
+
322
+ exchange.on_rest_response = _header_sync_hook
323
+
289
324
  @property
290
325
  def rate_limiter(self):
291
326
  """Access the attached rate limiter (if any)."""
@@ -277,3 +277,21 @@ def get_header_parser(exchange_name: str):
277
277
  """Get the response header parser for an exchange."""
278
278
  name = exchange_name.lower().split(".")[0]
279
279
  return HEADER_PARSERS.get(name)
280
+
281
+
282
+ # NOTE: As of #264 these parsers are invoked from two fetch paths:
283
+ #
284
+ # * ``qubx.data.storages.ccxt._sync_rate_limiter_from_response_headers`` after
285
+ # each successful OHLCV page fetch. Best-effort — ``last_response_headers`` is
286
+ # an exchange-wide attribute so under concurrent fetches the read may be from a
287
+ # neighboring request. Since all concurrent requests drain the same IP-scoped
288
+ # budget, an approximate reading is still strictly better than no sync.
289
+ #
290
+ # * ``qubx.connectors.ccxt.exchange_manager.ExchangeManager._apply_rate_limiter_to_exchange``
291
+ # via CCXT's ``on_rest_response`` hook. This one is atomic per-request (the
292
+ # hook runs inside CCXT's fetch pipeline with the actual response headers), so
293
+ # the live path gets exact sync rather than best-effort.
294
+ #
295
+ # Both code paths call the parser, which in turn calls
296
+ # ``rate_limiter.sync_from_exchange(...)`` to pin the modeled budget to the
297
+ # server-reported remaining.
@@ -35,9 +35,11 @@ Supported data types
35
35
  from __future__ import annotations
36
36
 
37
37
  import asyncio
38
- from collections.abc import Callable, Iterator
38
+ import random
39
+ from collections.abc import Awaitable, Callable, Iterator
39
40
  from typing import Any, cast
40
41
 
42
+ import ccxt
41
43
  import numpy as np
42
44
  import pandas as pd
43
45
  import pyarrow as pa
@@ -272,6 +274,149 @@ class CcxtReader(IReader):
272
274
  pass
273
275
 
274
276
 
277
+ _T = Any # explicit alias keeps the retry helper signature readable
278
+
279
+ # retry defaults — conservative. tuned for warmup OHLCV bursts where transient
280
+ # RateLimitExceeded / NetworkError are the dominant failure modes (see #264).
281
+ _RETRY_MAX_ATTEMPTS = 5
282
+ _RETRY_BASE_DELAY_S = 1.0
283
+ _RETRY_MAX_DELAY_S = 30.0
284
+ _RETRY_JITTER_S = 1.0
285
+
286
+
287
+ def _sync_rate_limiter_from_response_headers(ccxt_ex: Any, rate_limiter: Any) -> None:
288
+ """
289
+ Best-effort sync of the rate limiter's modeled state from the exchange's
290
+ last HTTP response headers.
291
+
292
+ Without this, the client-side token bucket flies blind to:
293
+ * cross-process / cross-bot contention on the same IP or account,
294
+ * drift between our ``PoolConfig`` and the exchange's real limits,
295
+ * pre-existing budget debt at process start.
296
+
297
+ This is a *best-effort* correction because ``ccxt_ex.last_response_headers``
298
+ is exchange-wide, not per-request — under concurrent fetches the attribute
299
+ may have been overwritten between the awaited response and this read.
300
+ That is acceptable in practice: every concurrent request drains the same
301
+ IP-scoped budget, so whatever remaining value we read is approximately
302
+ correct, and the limiter converges on subsequent syncs.
303
+
304
+ Any exception (missing attr, malformed header, parser bug) is swallowed
305
+ at DEBUG — header sync must never break a fetch.
306
+ """
307
+ if rate_limiter is None:
308
+ return
309
+ try:
310
+ from qubx.connectors.ccxt.rate_limits import get_header_parser
311
+
312
+ parser = get_header_parser(getattr(ccxt_ex, "id", ""))
313
+ if parser is None:
314
+ return
315
+ headers = getattr(ccxt_ex, "last_response_headers", None)
316
+ if not headers:
317
+ return
318
+ parser(headers, rate_limiter)
319
+ except Exception as e:
320
+ logger.debug(f"[CCXT] header sync failed (non-fatal): {e}")
321
+
322
+
323
+ class CcxtFetchExhausted(RuntimeError):
324
+ """
325
+ Raised when one or more symbols exhaust all retry attempts during a
326
+ multi-symbol fetch. The caller cannot silently proceed with partial data,
327
+ because downstream indicators may compute nonsense on zero bars and
328
+ propagate into corrupt live state (see #264).
329
+
330
+ The ``failures`` attribute maps ``qubx_sym -> last_exception`` so callers
331
+ can log or dispatch based on specific failures.
332
+ """
333
+
334
+ def __init__(self, failures: dict[str, BaseException], total_requested: int) -> None:
335
+ self.failures = failures
336
+ self.total_requested = total_requested
337
+ failed_symbols = sorted(failures.keys())
338
+ preview = ", ".join(
339
+ f"{s}={type(failures[s]).__name__}" for s in failed_symbols[:5]
340
+ )
341
+ more = f" (+{len(failed_symbols) - 5} more)" if len(failed_symbols) > 5 else ""
342
+ super().__init__(
343
+ f"OHLCV fetch exhausted retries for {len(failures)}/{total_requested} "
344
+ f"symbols: {preview}{more}"
345
+ )
346
+
347
+
348
+ async def _retryable_fetch(
349
+ call: Callable[[], Awaitable[_T]],
350
+ *,
351
+ rate_limiter: Any = None,
352
+ rate_limiter_endpoint: str = "ccxt_rest",
353
+ max_attempts: int = _RETRY_MAX_ATTEMPTS,
354
+ base_delay: float = _RETRY_BASE_DELAY_S,
355
+ max_delay: float = _RETRY_MAX_DELAY_S,
356
+ jitter: float = _RETRY_JITTER_S,
357
+ context: str = "",
358
+ sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
359
+ ) -> _T:
360
+ """
361
+ Invoke ``call()`` with exponential backoff + jitter on transient CCXT errors.
362
+
363
+ Retryable errors:
364
+ * ``ccxt.RateLimitExceeded`` — exchange-level 429 / OKX 50011.
365
+ * ``ccxt.NetworkError`` (parent; also covers ``ExchangeNotAvailable``
366
+ and ``OnMaintenance``).
367
+ * ``asyncio.TimeoutError``.
368
+
369
+ Everything else (``ccxt.ExchangeError`` and subclasses like ``BadSymbol``,
370
+ ``AuthenticationError``, plus any non-CCXT exception) is re-raised
371
+ immediately — permanent errors should not consume retry budget.
372
+
373
+ The rate-limiter interaction is handled here in two stages per attempt:
374
+ 1. ``acquire(endpoint)`` before each call to block while the gate is closed.
375
+ 2. ``report_limit_hit(endpoint=...)`` on ``RateLimitExceeded`` so the
376
+ gate closes for ``cooldown`` seconds, pausing all concurrent callers.
377
+
378
+ Args:
379
+ call: Zero-arg callable returning the coroutine to invoke.
380
+ rate_limiter: Optional ``ExchangeRateLimiter``; if provided, calls are
381
+ gated and rate-limit hits are reported back to it.
382
+ rate_limiter_endpoint: Endpoint name used for ``acquire`` /
383
+ ``report_limit_hit`` lookups.
384
+ max_attempts: Total attempts including the initial one.
385
+ base_delay: First retry delay in seconds; doubles each attempt.
386
+ max_delay: Cap on the exponential delay.
387
+ jitter: Uniform jitter (0..jitter) added to each delay.
388
+ context: Short human label included in log lines (e.g. ``"OKX BTCUSDT"``).
389
+ sleep: Async sleep function (injectable for tests).
390
+
391
+ Raises:
392
+ The last retryable exception once ``max_attempts`` is exhausted.
393
+ """
394
+ last_exc: BaseException | None = None
395
+ for attempt in range(1, max_attempts + 1):
396
+ if rate_limiter is not None:
397
+ await rate_limiter.acquire(rate_limiter_endpoint)
398
+ try:
399
+ return await call()
400
+ except (ccxt.NetworkError, asyncio.TimeoutError) as e:
401
+ last_exc = e
402
+ if isinstance(e, ccxt.RateLimitExceeded) and rate_limiter is not None:
403
+ rate_limiter.report_limit_hit(
404
+ endpoint=rate_limiter_endpoint,
405
+ reason=(f"{context}: " if context else "") + str(e)[:120],
406
+ )
407
+ if attempt >= max_attempts:
408
+ break
409
+ delay = min(base_delay * (2 ** (attempt - 1)), max_delay) + random.uniform(0, jitter)
410
+ logger.warning(
411
+ f"[CCXT] transient error on {context or 'fetch'} "
412
+ f"(attempt {attempt}/{max_attempts}): {type(e).__name__}: {e}; "
413
+ f"retrying in {delay:.2f}s"
414
+ )
415
+ await sleep(delay)
416
+ assert last_exc is not None # unreachable: the loop always raises or returns
417
+ raise last_exc
418
+
419
+
275
420
  @storage("ccxt")
276
421
  class CcxtStorage(IStorage):
277
422
  """
@@ -315,10 +460,20 @@ class CcxtStorage(IStorage):
315
460
  max_bars: int = 10_000,
316
461
  max_history: str = "3650d",
317
462
  rate_limiters: dict | None = None,
463
+ retry_max_attempts: int = _RETRY_MAX_ATTEMPTS,
464
+ retry_base_delay: float = _RETRY_BASE_DELAY_S,
465
+ retry_max_delay: float = _RETRY_MAX_DELAY_S,
466
+ retry_jitter: float = _RETRY_JITTER_S,
467
+ strict_fetch: bool = True,
318
468
  ) -> None:
319
469
  self._max_bars = max_bars
320
470
  self._max_history = to_timedelta(max_history)
321
471
  self._rate_limiters = rate_limiters or {}
472
+ self._retry_max_attempts = retry_max_attempts
473
+ self._retry_base_delay = retry_base_delay
474
+ self._retry_max_delay = retry_max_delay
475
+ self._retry_jitter = retry_jitter
476
+ self._strict_fetch = strict_fetch
322
477
  # - shared async loop (created from first exchange connection)
323
478
  self._loop = None
324
479
  self._capabilities = None
@@ -484,13 +639,31 @@ class CcxtStorage(IStorage):
484
639
  all_items: list = []
485
640
  current_since = since
486
641
 
487
- # Get rate limiter for this exchange (if available)
642
+ # Get rate limiter for this exchange (if available). Retry + backoff + rate-limit
643
+ # acquisition are all encapsulated inside ``_retryable_fetch`` so this loop stays
644
+ # focused on pagination.
488
645
  _rl = self._get_rate_limiter(ccxt_ex)
646
+ _ex_id = getattr(ccxt_ex, "id", "ccxt")
489
647
 
490
648
  for _ in range(max_pages):
491
- if _rl:
492
- await _rl.acquire("ccxt_rest")
493
- batch = await method(since=current_since, limit=limit, **method_kwargs)
649
+ _since = current_since # capture for the closure below
650
+
651
+ async def _do_page() -> list:
652
+ batch = await method(since=_since, limit=limit, **method_kwargs)
653
+ # Best-effort: keep our modeled budget in sync with what the exchange
654
+ # actually reports — see ``_sync_rate_limiter_from_response_headers``.
655
+ _sync_rate_limiter_from_response_headers(ccxt_ex, _rl)
656
+ return batch
657
+
658
+ batch = await _retryable_fetch(
659
+ _do_page,
660
+ rate_limiter=_rl,
661
+ max_attempts=self._retry_max_attempts,
662
+ base_delay=self._retry_base_delay,
663
+ max_delay=self._retry_max_delay,
664
+ jitter=self._retry_jitter,
665
+ context=f"{_ex_id} {method_kwargs.get('symbol', '?')}",
666
+ )
494
667
  if not batch:
495
668
  break
496
669
  for item in batch:
@@ -543,7 +716,24 @@ class CcxtStorage(IStorage):
543
716
  ) -> dict[str, list]:
544
717
  """
545
718
  Concurrent OHLCV fetch for multiple instruments with bounded concurrency.
546
- Returns ``{qubx_sym: raw_candles}``.
719
+
720
+ Per-symbol results go through ``_async_paginated_fetch`` which already
721
+ retries transient errors with backoff. If retries are exhausted, behavior
722
+ depends on ``self._strict_fetch``:
723
+
724
+ * ``strict_fetch=True`` (default) — logs ERROR per symbol and raises
725
+ :class:`CcxtFetchExhausted` listing all failures. This prevents
726
+ downstream indicators from computing on zero bars (see #264).
727
+ * ``strict_fetch=False`` — logs WARNING per symbol and returns ``[]``
728
+ for that symbol. Kept for backwards compatibility and use cases
729
+ where partial data is acceptable.
730
+
731
+ Returns:
732
+ ``{qubx_sym: raw_candles}`` on full or partial success.
733
+
734
+ Raises:
735
+ CcxtFetchExhausted: when any symbol exhausts retries and
736
+ ``strict_fetch`` is True.
547
737
  """
548
738
  sem = asyncio.Semaphore(5) # max 5 concurrent fetches
549
739
 
@@ -553,14 +743,26 @@ class CcxtStorage(IStorage):
553
743
 
554
744
  coros = [_fetch_with_sem(ccxt_sym, exc_tf, since, until) for ccxt_sym, _ in instruments_info]
555
745
  results = await asyncio.gather(*coros, return_exceptions=True)
746
+
556
747
  out: dict[str, list] = {}
557
- for i, result in enumerate(results):
558
- _, qubx_sym = instruments_info[i]
559
- if isinstance(result, Exception):
560
- logger.warning(f"[CCXT] Failed to fetch OHLCV for {qubx_sym}: {result}")
748
+ failures: dict[str, BaseException] = {}
749
+ for (_, qubx_sym), result in zip(instruments_info, results):
750
+ if isinstance(result, BaseException):
751
+ failures[qubx_sym] = result
561
752
  out[qubx_sym] = []
562
753
  else:
563
754
  out[qubx_sym] = cast(list, result)
755
+
756
+ if failures:
757
+ _ex_id = getattr(ccxt_ex, "id", "ccxt")
758
+ for qubx_sym, exc in failures.items():
759
+ logger.error(
760
+ f"[CCXT] {_ex_id}: OHLCV fetch for {qubx_sym} exhausted retries — "
761
+ f"{type(exc).__name__}: {exc}"
762
+ )
763
+ if self._strict_fetch:
764
+ raise CcxtFetchExhausted(failures, total_requested=len(instruments_info))
765
+
564
766
  return out
565
767
 
566
768
  def _ccxt_sym_to_qubx(self, ccxt_symbol: str) -> str:
@@ -310,7 +310,7 @@ class TypedRecords(IDataTransformer):
310
310
  return FundingPayment(
311
311
  time=int(row["time"]),
312
312
  funding_rate=float(row["funding_rate"]),
313
- funding_interval_hours=int(row["funding_interval_hours"]),
313
+ funding_interval_hours=round(float(row["funding_interval_hours"])),
314
314
  )
315
315
 
316
316
  case DataType.OPEN_INTEREST:
@@ -251,7 +251,7 @@ def running_view(arr, window, axis=-1):
251
251
  """
252
252
  shape = list(arr.shape)
253
253
  shape[axis] -= window - 1
254
- return np.lib.index_tricks.as_strided(arr, shape + [window], arr.strides + (arr.strides[axis],))
254
+ return np.lib.stride_tricks.as_strided(arr, shape + [window], arr.strides + (arr.strides[axis],))
255
255
 
256
256
 
257
257
  def detrend(y, order):
@@ -794,9 +794,11 @@ class AtrRiskTracker(GenericRiskControllerDecorator):
794
794
  def calculate_risks(self, ctx: IStrategyContext, quote: Quote | None, signal: Signal) -> Signal | None:
795
795
  volatility = self._get_volatility(ctx, signal.instrument)
796
796
 
797
- if len(volatility) < 2 or ((last_volatility := volatility[1]) is None or not np.isfinite(last_volatility)):
797
+ last_volatility = volatility[1] if len(volatility) >= 2 else None
798
+ if last_volatility is None or not np.isfinite(last_volatility) or last_volatility <= 0:
798
799
  logger.debug(
799
- f"[<y>{self._full_name}</y>(<g>{signal.instrument}</g>)] :: not enough ATR data, skipping risk calculation"
800
+ f"[<y>{self._full_name}</y>(<g>{signal.instrument}</g>)] :: "
801
+ f"ATR unusable (value={last_volatility!r}), skipping risk calculation"
800
802
  )
801
803
  return None
802
804
 
@@ -113,6 +113,14 @@ class FixedRiskSizer(IPositionSizer):
113
113
  if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
114
114
  continue
115
115
 
116
+ _stop_distance = abs(signal.stop / _entry - 1)
117
+ if _stop_distance <= 0 or not np.isfinite(_stop_distance):
118
+ logger.warning(
119
+ f" >>> {self.__class__.__name__}: degenerate stop/entry for "
120
+ f"{signal.instrument.symbol} (entry={_entry}, stop={signal.stop}) - skipping"
121
+ )
122
+ continue
123
+
116
124
  # - hey, we can't trade using negative balance ;)
117
125
  _cap = max(ctx.get_total_capital() if self.reinvest_profit else ctx.get_capital(), 0)
118
126
  _scale = abs(signal.signal) if self.scale_by_signal else 1
@@ -122,7 +130,7 @@ class FixedRiskSizer(IPositionSizer):
122
130
  _notional_per_contract = _entry * signal.instrument.quantity_multiplier
123
131
  target_position_size = (
124
132
  _direction
125
- *min((_cap * self.max_cap_in_risk) / abs(signal.stop / _entry - 1), self.max_allowed_position_quoted) / _notional_per_contract
133
+ *min((_cap * self.max_cap_in_risk) / _stop_distance, self.max_allowed_position_quoted) / _notional_per_contract
126
134
  / (len(ctx.instruments) if self.divide_by_symbols else 1)
127
135
  * _scale
128
136
  )
@@ -237,6 +245,14 @@ class FixedRiskSizerWithConstantCapital(IPositionSizer):
237
245
  if (_entry := self.get_signal_entry_price(ctx, signal)) is None:
238
246
  continue
239
247
 
248
+ _stop_distance = abs(signal.stop / _entry - 1)
249
+ if _stop_distance <= 0 or not np.isfinite(_stop_distance):
250
+ logger.warning(
251
+ f" >>> {self.__class__.__name__}: degenerate stop/entry for {signal.instrument.symbol} "
252
+ f"(entry={_entry}, stop={signal.stop}) - skipping"
253
+ )
254
+ continue
255
+
240
256
  # - just use same fixed capital
241
257
  _cap = self.capital / (len(ctx.instruments) if self.divide_by_symbols else 1)
242
258
 
@@ -245,7 +261,7 @@ class FixedRiskSizerWithConstantCapital(IPositionSizer):
245
261
  _notional_per_contract = _entry * signal.instrument.quantity_multiplier
246
262
  target_position_size = (
247
263
  _direction * min(
248
- (_cap * self.max_cap_in_risk) / abs(signal.stop / _entry - 1),
264
+ (_cap * self.max_cap_in_risk) / _stop_distance,
249
265
  self.max_allowed_position_quoted
250
266
  ) / _notional_per_contract
251
267
  )
@@ -80,6 +80,27 @@ from .accounts import AccountConfigurationManager
80
80
  INVERSE_EXCHANGE_MAPPINGS = {mapping: exchange for exchange, mapping in EXCHANGE_MAPPINGS.items()}
81
81
 
82
82
 
83
+ def _inject_warmup_rate_limiters(warmup: WarmupConfig | None, rate_limiters: dict | None) -> None:
84
+ """
85
+ Inject rate limiters into warmup storage configs.
86
+
87
+ Mirrors the aux_configs injection done in ``create_strategy_context`` — without this,
88
+ warmup data fetches (e.g. via ``CcxtStorage``) bypass the per-exchange rate limiter
89
+ and can be rate-limited by the exchange with no retry, producing empty bars and
90
+ corrupting downstream indicators (see xLydianSoftware/Qubx#264).
91
+
92
+ Args:
93
+ warmup: Warmup config. If None or no rate_limiters, this is a no-op.
94
+ rate_limiters: Dict of rate limiters keyed by exchange name (from ``RateLimitManager``).
95
+ """
96
+ if warmup is None or not rate_limiters:
97
+ return
98
+ warmup.data.args["rate_limiters"] = rate_limiters
99
+ for typed_cfg in warmup.custom_data or []:
100
+ for sc in typed_cfg.storages:
101
+ sc.args["rate_limiters"] = rate_limiters
102
+
103
+
83
104
  def _cleanup_event_loop(loop: asyncio.AbstractEventLoop | None) -> None:
84
105
  """
85
106
  Cleanup the shared event loop.
@@ -1028,6 +1049,11 @@ def _run_warmup(
1028
1049
 
1029
1050
  logger.info(f"<yellow>Warmup start time: {warmup_start_time}</yellow>")
1030
1051
 
1052
+ # - inject rate limiters into warmup storage configs (for CcxtStorage, LighterStorage, etc.)
1053
+ # mirrors the aux_configs injection done in create_strategy_context.
1054
+ # without this, warmup OHLCV fetches bypass the configured per-exchange rate limiter.
1055
+ _inject_warmup_rate_limiters(warmup, getattr(ctx, "_rate_limiters", None))
1056
+
1031
1057
  # - resolve warmup data sources (mirrors SimulationConfig layout)
1032
1058
  # - warmup.data is required (StorageConfig, not None) so construct_storage always returns IStorage
1033
1059
  _warmup_data_storage = construct_storage(warmup.data)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes