Qubx 1.2.1.dev1__tar.gz → 1.2.2.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/PKG-INFO +1 -1
  2. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/_version.py +2 -2
  3. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/release.py +5 -0
  4. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/account.py +27 -1
  5. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +13 -0
  6. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/okx/account.py +18 -3
  7. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/account.py +4 -1
  8. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/engine.py +47 -175
  9. qubx-1.2.2.dev2/src/qubx/rate_limiting/pools.py +201 -0
  10. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/.gitignore +0 -0
  11. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/LICENSE +0 -0
  12. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/README.md +0 -0
  13. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/pyproject.toml +0 -0
  14. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/scripts/build.py +0 -0
  15. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/__init__.py +0 -0
  16. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/_nb_magic.py +0 -0
  17. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/__init__.py +0 -0
  18. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/account.py +0 -0
  19. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/broker.py +0 -0
  20. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/data.py +0 -0
  21. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/iteratedstream.py +0 -0
  22. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/management.py +0 -0
  23. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/ome.py +0 -0
  24. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/optimization.py +0 -0
  25. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/runner.py +0 -0
  26. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/sentinels.py +0 -0
  27. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/simulated_data.py +0 -0
  28. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/simulated_exchange.py +0 -0
  29. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/simulator.py +0 -0
  30. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/transfers.py +0 -0
  31. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/backtester/utils.py +0 -0
  32. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/__init__.py +0 -0
  33. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/commands.py +0 -0
  34. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/deploy.py +0 -0
  35. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/misc.py +0 -0
  36. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/resolver.py +0 -0
  37. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/s3.py +0 -0
  38. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/theme.py +0 -0
  39. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/tui.py +0 -0
  40. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/cli/user_config.py +0 -0
  41. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/config.py +0 -0
  42. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/__init__.py +0 -0
  43. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/__init__.py +0 -0
  44. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  45. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  46. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/broker.py +0 -0
  47. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  48. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/data.py +0 -0
  49. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  50. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  51. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  52. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  53. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  54. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  55. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  56. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  57. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  58. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  59. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  60. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  61. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  62. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  63. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  64. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
  65. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  66. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/factory.py +0 -0
  67. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  68. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  69. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  70. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  71. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  72. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  73. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  74. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  75. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  76. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  77. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
  78. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  79. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  80. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  81. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/utils.py +0 -0
  82. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  83. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/registry.py +0 -0
  84. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/tardis/data.py +0 -0
  85. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/connectors/tardis/utils.py +0 -0
  86. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/__init__.py +0 -0
  87. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/builtin.py +0 -0
  88. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/decorator.py +0 -0
  89. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/executor.py +0 -0
  90. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/interfaces.py +0 -0
  91. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/server.py +0 -0
  92. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/control/types.py +0 -0
  93. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/__init__.py +0 -0
  94. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/basics.py +0 -0
  95. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/context.py +0 -0
  96. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/detectors/__init__.py +0 -0
  97. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/detectors/delisting.py +0 -0
  98. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/detectors/stale.py +0 -0
  99. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/errors.py +0 -0
  100. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/exceptions.py +0 -0
  101. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/helpers.py +0 -0
  102. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/initializer.py +0 -0
  103. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/interfaces.py +0 -0
  104. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/loggers.py +0 -0
  105. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/lookups.py +0 -0
  106. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/metrics.py +0 -0
  107. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/__init__.py +0 -0
  108. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/market.py +0 -0
  109. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/processing.py +0 -0
  110. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/subscription.py +0 -0
  111. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/trading.py +0 -0
  112. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/universe.py +0 -0
  113. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/mixins/utils.py +0 -0
  114. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/series.pxd +0 -0
  115. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/series.pyi +0 -0
  116. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/series.pyx +0 -0
  117. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/utils.pyi +0 -0
  118. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/core/utils.pyx +0 -0
  119. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/__init__.py +0 -0
  120. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/cache.py +0 -0
  121. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/containers.py +0 -0
  122. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/guards.py +0 -0
  123. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/registry.py +0 -0
  124. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storage.py +0 -0
  125. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/ccxt.py +0 -0
  126. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/csv.py +0 -0
  127. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/handy.py +0 -0
  128. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/multi.py +0 -0
  129. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/questdb.py +0 -0
  130. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/stub.py +0 -0
  131. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/storages/utils.py +0 -0
  132. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/data/transformers.py +0 -0
  133. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/__init__.py +0 -0
  134. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/base.py +0 -0
  135. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/composite.py +0 -0
  136. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/csv.py +0 -0
  137. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/indicator.py +0 -0
  138. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/inmemory.py +0 -0
  139. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/prometheus.py +0 -0
  140. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/emitters/questdb.py +0 -0
  141. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/__init__.py +0 -0
  142. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/composite.py +0 -0
  143. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/__init__.py +0 -0
  144. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/base.py +0 -0
  145. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/incremental.py +0 -0
  146. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/slack.py +0 -0
  147. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/formatters/target_position.py +0 -0
  148. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/redis_streams.py +0 -0
  149. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/exporters/slack.py +0 -0
  150. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/gathering/simplest.py +0 -0
  151. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/health/__init__.py +0 -0
  152. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/health/base.py +0 -0
  153. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/health/dummy.py +0 -0
  154. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/__init__.py +0 -0
  155. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/csv.py +0 -0
  156. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/factory.py +0 -0
  157. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/inmemory.py +0 -0
  158. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/mongo.py +0 -0
  159. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/loggers/postgres.py +0 -0
  160. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/notifications/__init__.py +0 -0
  161. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/notifications/composite.py +0 -0
  162. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/notifications/slack.py +0 -0
  163. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/notifications/throttler.py +0 -0
  164. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/pandaz/__init__.py +0 -0
  165. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/pandaz/stats.py +0 -0
  166. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/pandaz/ta.py +0 -0
  167. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/pandaz/utils.py +0 -0
  168. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/plugins/__init__.py +0 -0
  169. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/plugins/loader.py +0 -0
  170. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/__init__.py +0 -0
  171. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/backend.py +0 -0
  172. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/config.py +0 -0
  173. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/ip_resolver.py +0 -0
  174. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/manager.py +0 -0
  175. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/rate_limiting/redis_backend.py +0 -0
  176. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/_build.py +0 -0
  177. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/crypto-fees.ini +0 -0
  178. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  179. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  180. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  181. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  182. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  183. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  184. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  185. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  186. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  187. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  188. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  189. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restarts/__init__.py +0 -0
  190. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restarts/state_resolvers.py +0 -0
  191. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restarts/time_finders.py +0 -0
  192. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/__init__.py +0 -0
  193. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/balance.py +0 -0
  194. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/factory.py +0 -0
  195. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/interfaces.py +0 -0
  196. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/position.py +0 -0
  197. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/signal.py +0 -0
  198. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/state.py +0 -0
  199. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/restorers/utils.py +0 -0
  200. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/state/__init__.py +0 -0
  201. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/state/dummy.py +0 -0
  202. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/state/redis.py +0 -0
  203. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/ta/__init__.py +0 -0
  204. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/ta/indicators.pxd +0 -0
  205. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/ta/indicators.pyi +0 -0
  206. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/ta/indicators.pyx +0 -0
  207. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/__init__.py +0 -0
  208. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/base.py +0 -0
  209. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  210. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/config.yml.j2 +0 -0
  211. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  212. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  213. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  214. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  215. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  216. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/project/template.yml +0 -0
  217. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  218. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  219. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  220. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  221. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  222. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  223. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.python-version +0 -0
  224. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  225. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  226. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  227. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  228. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/README.md.j2 +0 -0
  229. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  230. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/justfile.j2 +0 -0
  231. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  232. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/research/.gitkeep +0 -0
  233. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  234. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  235. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  236. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/template.yml +0 -0
  237. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  238. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  239. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  240. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/config.yml.j2 +0 -0
  241. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  242. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  243. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  244. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/templates/simple/template.yml +0 -0
  245. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/__init__.py +0 -0
  246. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/advanced.py +0 -0
  247. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/composite.py +0 -0
  248. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/rebalancers.py +0 -0
  249. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/riskctrl.py +0 -0
  250. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/trackers/sizers.py +0 -0
  251. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/__init__.py +0 -0
  252. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/_pyxreloader.py +0 -0
  253. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/charting/lookinglass.py +0 -0
  254. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  255. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/charting/orderbook.py +0 -0
  256. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/collections.py +0 -0
  257. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/hft/__init__.py +0 -0
  258. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/hft/numba_utils.py +0 -0
  259. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/hft/orderbook.pyi +0 -0
  260. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/hft/orderbook.pyx +0 -0
  261. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/marketdata/binance.py +0 -0
  262. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/marketdata/ccxt.py +0 -0
  263. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/marketdata/dukas.py +0 -0
  264. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/misc.py +0 -0
  265. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/nonce.py +0 -0
  266. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/ntp.py +0 -0
  267. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/numbers_utils.py +0 -0
  268. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/orderbook.py +0 -0
  269. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/__init__.py +0 -0
  270. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/dashboard.py +0 -0
  271. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/data.py +0 -0
  272. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/interfaces.py +0 -0
  273. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  274. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  275. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/questdb.py +0 -0
  276. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/rate_limiter.py +0 -0
  277. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/results.py +0 -0
  278. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/ringbuffer.pxd +0 -0
  279. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/ringbuffer.pyi +0 -0
  280. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/ringbuffer.pyx +0 -0
  281. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/__init__.py +0 -0
  282. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  283. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/accounts.py +0 -0
  284. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/configs.py +0 -0
  285. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/factory.py +0 -0
  286. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/kernel_service.py +0 -0
  287. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/runner.py +0 -0
  288. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/__init__.py +0 -0
  289. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/app.py +0 -0
  290. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/handlers.py +0 -0
  291. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/init_code.py +0 -0
  292. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/kernel.py +0 -0
  293. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  294. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  295. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
  296. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  297. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  298. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  299. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  300. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  301. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  302. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/s3.py +0 -0
  303. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/slack.py +0 -0
  304. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/throttler.py +0 -0
  305. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/time.py +0 -0
  306. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/src/qubx/utils/websocket_manager.py +0 -0
  307. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  308. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  309. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  310. {qubx-1.2.1.dev1 → qubx-1.2.2.dev2}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 1.2.1.dev1
3
+ Version: 1.2.2.dev2
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Project-URL: homepage, https://xlydian.com
6
6
  Project-URL: repository, https://github.com/xLydianSoftware/Qubx
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.2.1.dev1'
22
- __version_tuple__ = version_tuple = (1, 2, 1, 'dev1')
21
+ __version__ = version = '1.2.2.dev2'
22
+ __version_tuple__ = version_tuple = (1, 2, 2, 'dev2')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1432,12 +1432,14 @@ def _bundle_source_overrides(
1432
1432
  continue
1433
1433
 
1434
1434
  checkout_dir = _find_uv_git_checkout(commit_sha)
1435
+ cloned_locally = False
1435
1436
  if not checkout_dir:
1436
1437
  # Cache miss — clone the repo at the exact commit
1437
1438
  git_url = source["git"]
1438
1439
  git_ref = source.get("tag") or source.get("branch") or commit_sha
1439
1440
  logger.info(f" {pkg_name}: cache miss, cloning {git_url} at {git_ref} ...")
1440
1441
  checkout_dir = os.path.join(os.path.dirname(wheels_dir), f".git-clone-{pkg_norm}")
1442
+ cloned_locally = True
1441
1443
  try:
1442
1444
  subprocess.run(
1443
1445
  ["git", "clone", "--depth", "1", "--branch", git_ref, git_url, checkout_dir],
@@ -1473,6 +1475,9 @@ def _bundle_source_overrides(
1473
1475
  logger.info(f" Bundled {pkg_name}=={pkg_ver} from git")
1474
1476
  except subprocess.CalledProcessError as e:
1475
1477
  logger.opt(colors=False).warning(f" Failed to build wheel for {pkg_name}: {e.stderr or e}")
1478
+ finally:
1479
+ if cloned_locally and os.path.isdir(checkout_dir):
1480
+ shutil.rmtree(checkout_dir, ignore_errors=True)
1476
1481
 
1477
1482
  return bundled
1478
1483
 
@@ -222,7 +222,10 @@ class CcxtAccountProcessor(BasicAccountProcessor):
222
222
  except Exception as e:
223
223
  logger.warning(f"Failed to fetch balance data before polling started: {e}")
224
224
 
225
- # sum of balances + market value of all positions on non spot/margin
225
+ if self._exchange_total_capital is not None:
226
+ return self._exchange_total_capital
227
+
228
+ # fallback: sum of balances + market value of all positions on non spot/margin
226
229
  _currency_to_value = {c: self._get_currency_value(b.total, c) for c, b in self._balances.items()}
227
230
  _positions_value = sum([p.market_value_funds for p in self._positions.values() if p.instrument.is_futures()])
228
231
  return sum(_currency_to_value.values()) + _positions_value
@@ -316,9 +319,32 @@ class CcxtAccountProcessor(BasicAccountProcessor):
316
319
  await self._subscribe_instruments(list(self._required_instruments))
317
320
  self._latest_instruments.update(self._required_instruments)
318
321
 
322
+ def _extract_portfolio_value(self, balances_raw: dict) -> float | None:
323
+ """Extract exchange-reported portfolio value (equity) from raw balance response.
324
+
325
+ Override in exchange-specific processors for non-standard responses.
326
+ Base implementation handles Binance futures (totalMarginBalance).
327
+
328
+ Returns:
329
+ Total equity as float, or None to fall back to computed value.
330
+ """
331
+ info = balances_raw.get("info", {})
332
+ val = info.get("totalMarginBalance")
333
+ if val is not None:
334
+ try:
335
+ return float(val)
336
+ except (ValueError, TypeError):
337
+ pass
338
+ return None
339
+
319
340
  async def _update_balance(self) -> None:
320
341
  """Fetch and update balances from exchange"""
321
342
  balances_raw = await self.exchange_manager.exchange.fetch_balance()
343
+
344
+ equity = self._extract_portfolio_value(balances_raw)
345
+ if equity is not None:
346
+ self._exchange_total_capital = equity
347
+
322
348
  balances = ccxt_convert_balance(balances_raw, self.exchange)
323
349
  current_balances = self.get_balances()
324
350
 
@@ -23,6 +23,19 @@ class HyperliquidAccountProcessor(CcxtAccountProcessor):
23
23
  so we must subscribe to both channels separately.
24
24
  """
25
25
 
26
+ def _extract_portfolio_value(self, balances_raw: dict) -> float | None:
27
+ """Extract Hyperliquid account value from raw balance response."""
28
+ info = balances_raw.get("info", {})
29
+ for key in ("marginSummary", "crossMarginSummary"):
30
+ summary = info.get(key, {})
31
+ val = summary.get("accountValue")
32
+ if val is not None:
33
+ try:
34
+ return float(val)
35
+ except (ValueError, TypeError):
36
+ pass
37
+ return None
38
+
26
39
  async def _subscribe_instruments(self, instruments: list[Instrument]):
27
40
  """Override to filter out instruments from other exchanges (e.g., spot vs futures)."""
28
41
  # Filter instruments to only those belonging to this exchange
@@ -57,15 +57,30 @@ class OkxAccountProcessor(CcxtAccountProcessor):
57
57
  ),
58
58
  )
59
59
 
60
+ def _extract_portfolio_value(self, balances_raw: dict) -> float | None:
61
+ """Extract OKX total equity from raw balance response."""
62
+ data = balances_raw.get("info", {}).get("data", [{}])[0]
63
+ total_eq = data.get("totalEq")
64
+ if total_eq is not None:
65
+ try:
66
+ return float(total_eq)
67
+ except (ValueError, TypeError):
68
+ pass
69
+ return None
70
+
60
71
  async def _update_balance(self) -> None:
61
- """OKX balance override: use cashBal instead of eq (equity) to avoid double-counting unrealized PnL.
72
+ """OKX balance override: use cashBal for per-currency balances.
62
73
 
63
74
  CCXT maps OKX's `eq` (equity = cashBal + unrealizedPnL) to balance `total`.
64
- Since get_total_capital() adds position market values (= unrealized PnL) on top,
65
- using `eq` would double-count unrealized PnL. We use `cashBal` instead.
75
+ We use `cashBal` for individual currency balances, and extract `totalEq`
76
+ as the authoritative portfolio value via _extract_portfolio_value().
66
77
  """
67
78
  balances_raw = await self.exchange_manager.exchange.fetch_balance()
68
79
 
80
+ equity = self._extract_portfolio_value(balances_raw)
81
+ if equity is not None:
82
+ self._exchange_total_capital = equity
83
+
69
84
  # Extract cashBal from raw OKX response instead of CCXT-parsed eq
70
85
  balances: list[AssetBalance] = []
71
86
  raw_details = balances_raw.get("info", {}).get("data", [{}])[0].get("details", [])
@@ -65,6 +65,7 @@ class BasicAccountProcessor(IAccountProcessor):
65
65
  self._locked_capital_by_order = dict()
66
66
  self._pending_order_requests = {}
67
67
  self._balances = {}
68
+ self._exchange_total_capital: float | None = None
68
69
  # Initialize with base currency balance
69
70
  self._balances[self.base_currency] = AssetBalance(
70
71
  exchange=self.exchange, currency=self.base_currency, free=initial_capital, locked=0.0, total=initial_capital
@@ -90,7 +91,9 @@ class BasicAccountProcessor(IAccountProcessor):
90
91
  return self.get_available_margin(exchange)
91
92
 
92
93
  def get_total_capital(self, exchange: str | None = None) -> float:
93
- # sum of cash + market value of all positions
94
+ if self._exchange_total_capital is not None:
95
+ return self._exchange_total_capital
96
+ # fallback: sum of cash + market value of all positions
94
97
  _cash_amount = self._balances[self.base_currency].total
95
98
  _positions_value = sum([p.market_value_funds for p in self._positions.values()])
96
99
  return _cash_amount + _positions_value
@@ -1,9 +1,7 @@
1
- """
2
- Exchange rate limiting engine.
1
+ """Exchange rate limiting engine.
3
2
 
4
- The central component that connectors interact with. Handles multi-pool
5
- token acquisition, gate mechanism for cooldowns, exchange state sync,
6
- and metric collection.
3
+ The central component that connectors interact with. Delegates pool-specific
4
+ behavior (gate management, acquisition, sync) to polymorphic pool objects.
7
5
  """
8
6
 
9
7
  import asyncio
@@ -13,11 +11,9 @@ from typing import Any
13
11
  from qubx import logger
14
12
 
15
13
  from .backend import InMemoryBackend, IRateLimitBackend
16
- from .config import ExchangeRateLimitConfig, PoolConfig
17
-
18
-
19
- class RateLimitGateTimeout(Exception):
20
- """Raised when acquire() times out waiting for a gate to reopen."""
14
+ from .config import ExchangeRateLimitConfig
15
+ from .pools import BasePool, QuotaPool, RatePool
16
+ from .pools import RateLimitGateTimeout as RateLimitGateTimeout # re-export
21
17
 
22
18
 
23
19
  class ExchangeRateLimiter:
@@ -59,30 +55,19 @@ class ExchangeRateLimiter:
59
55
  """
60
56
  self._exchange = exchange
61
57
  self._config = config
62
- self._backend = backend or InMemoryBackend()
63
58
  self._scope_ids = scope_ids or {}
64
59
  self._event_loop = event_loop
65
-
66
- # Per-pool gates (asyncio.Event: set = open, clear = closed)
67
- self._gates: dict[str, asyncio.Event] = {}
68
- self._gate_tasks: dict[str, asyncio.Task] = {}
69
- for pool_name in config.pools:
70
- gate = asyncio.Event()
71
- gate.set()
72
- self._gates[pool_name] = gate
73
-
74
- # Quota pools: externally-managed remaining count
75
- self._quota_remaining: dict[str, float] = {}
76
- for pool_name, pool in config.pools.items():
77
- if pool.pool_type == "quota":
78
- self._quota_remaining[pool_name] = pool.capacity
79
-
80
- # Stats tracking
81
- self._hits: dict[str, int] = {}
82
- self._total_wait: dict[str, float] = {}
83
- self._consumed: dict[str, float] = {}
84
60
  self._last_metrics_time = time.monotonic()
85
61
 
62
+ backend = backend or InMemoryBackend()
63
+ self._pools: dict[str, BasePool] = {}
64
+ for pool_name, pool_config in config.pools.items():
65
+ scope_id = self._scope_ids.get(pool_config.scope, "local")
66
+ if pool_config.pool_type == "quota":
67
+ self._pools[pool_name] = QuotaPool(pool_config, exchange, scope_id)
68
+ else:
69
+ self._pools[pool_name] = RatePool(pool_config, exchange, scope_id, backend)
70
+
86
71
  @property
87
72
  def exchange(self) -> str:
88
73
  return self._exchange
@@ -100,11 +85,6 @@ class ExchangeRateLimiter:
100
85
  def config(self) -> ExchangeRateLimitConfig:
101
86
  return self._config
102
87
 
103
- def _key_for(self, pool: PoolConfig) -> str:
104
- """Construct Redis/backend key for a pool based on its scope."""
105
- scope_id = self._scope_ids.get(pool.scope, "local")
106
- return f"ratelimit:{self._exchange}:{pool.name}:{scope_id}"
107
-
108
88
  def update_scope_id(self, scope: str, new_id: str) -> None:
109
89
  """Update scope identifier (e.g., when egress IP changes).
110
90
 
@@ -114,6 +94,9 @@ class ExchangeRateLimiter:
114
94
  if old_id != new_id:
115
95
  logger.info(f"Rate limiter {self._exchange}: {scope} scope changed {old_id} → {new_id}")
116
96
  self._scope_ids[scope] = new_id
97
+ for pool in self._pools.values():
98
+ if pool.config.scope == scope:
99
+ pool.update_scope_id(new_id)
117
100
 
118
101
  async def acquire(self, endpoint: str, weight_override: float | None = None) -> None:
119
102
  """Wait until all pools for this endpoint have sufficient budget.
@@ -134,50 +117,11 @@ class ExchangeRateLimiter:
134
117
  return
135
118
 
136
119
  for i, (pool_name, weight) in enumerate(endpoint_costs.costs):
137
- pool = self._config.pools.get(pool_name)
120
+ pool = self._pools.get(pool_name)
138
121
  if pool is None:
139
122
  continue
140
-
141
123
  actual_weight = weight_override if (weight_override is not None and i == 0) else weight
142
-
143
- # Wait for gate to be open
144
- gate = self._gates.get(pool_name)
145
- if gate is not None and not gate.is_set():
146
- try:
147
- await asyncio.wait_for(gate.wait(), timeout=self._config.gate_max_wait)
148
- except asyncio.TimeoutError:
149
- raise RateLimitGateTimeout(
150
- f"{self._exchange}: gate for pool '{pool_name}' did not reopen "
151
- f"within {self._config.gate_max_wait:.0f}s"
152
- ) from None
153
-
154
- # For quota pools, check remaining (no time-based refill)
155
- if pool.pool_type == "quota":
156
- remaining = self._quota_remaining.get(pool_name, 0)
157
- if remaining <= 0:
158
- # Gate should already be closed, but double-check
159
- self._close_gate(pool_name, pool.cooldown, "quota depleted")
160
- gate = self._gates.get(pool_name)
161
- if gate is not None:
162
- try:
163
- await asyncio.wait_for(gate.wait(), timeout=self._config.gate_max_wait)
164
- except asyncio.TimeoutError:
165
- raise RateLimitGateTimeout(
166
- f"{self._exchange}: quota pool '{pool_name}' depleted, "
167
- f"gate did not reopen within {self._config.gate_max_wait:.0f}s"
168
- ) from None
169
- # Decrement quota locally
170
- if pool_name in self._quota_remaining:
171
- self._quota_remaining[pool_name] = max(0, self._quota_remaining[pool_name] - actual_weight)
172
- continue
173
-
174
- # For rate pools, acquire from backend
175
- key = self._key_for(pool)
176
- wait_time = await self._backend.acquire(key, actual_weight, pool.capacity, pool.refill_rate)
177
-
178
- # Track stats
179
- self._total_wait[pool_name] = self._total_wait.get(pool_name, 0) + wait_time
180
- self._consumed[pool_name] = self._consumed.get(pool_name, 0) + actual_weight
124
+ await pool.acquire(actual_weight, self._config.gate_max_wait)
181
125
 
182
126
  def report_limit_hit(
183
127
  self,
@@ -197,7 +141,7 @@ class ExchangeRateLimiter:
197
141
  retry_after: Seconds to wait (from Retry-After header)
198
142
  reason: Human-readable reason for logging
199
143
  """
200
- pools_to_close = []
144
+ pools_to_close: list[str] = []
201
145
 
202
146
  if pool_name:
203
147
  pools_to_close.append(pool_name)
@@ -206,15 +150,17 @@ class ExchangeRateLimiter:
206
150
  pools_to_close = [p for p, _ in costs.costs]
207
151
  else:
208
152
  # Close all rate pools
209
- pools_to_close = [name for name, pool in self._config.pools.items() if pool.pool_type == "rate"]
153
+ pools_to_close = [
154
+ name for name, pool in self._pools.items() if pool.config.pool_type == "rate"
155
+ ]
210
156
 
211
157
  for pname in pools_to_close:
212
- pool = self._config.pools.get(pname)
158
+ pool = self._pools.get(pname)
213
159
  if pool is None:
214
160
  continue
215
- cooldown = retry_after if retry_after is not None else pool.cooldown
216
- self._close_gate(pname, cooldown, reason or f"rate limit hit on {pname}")
217
- self._hits[pname] = self._hits.get(pname, 0) + 1
161
+ cooldown = retry_after if retry_after is not None else pool.config.cooldown
162
+ pool.close_gate(cooldown, reason or f"rate limit hit on {pname}")
163
+ pool.hits += 1
218
164
 
219
165
  def sync_from_exchange(
220
166
  self,
@@ -235,11 +181,11 @@ class ExchangeRateLimiter:
235
181
  used: Used tokens reported by exchange (remaining = capacity - used)
236
182
  capacity: Total capacity reported by exchange (overrides config if provided)
237
183
  """
238
- pool = self._config.pools.get(pool_name)
184
+ pool = self._pools.get(pool_name)
239
185
  if pool is None:
240
186
  return
241
187
 
242
- actual_capacity = capacity or pool.capacity
188
+ actual_capacity = capacity or pool.config.capacity
243
189
 
244
190
  if remaining is not None:
245
191
  actual_remaining = remaining
@@ -248,77 +194,25 @@ class ExchangeRateLimiter:
248
194
  else:
249
195
  return
250
196
 
251
- if pool.pool_type == "quota":
252
- self._quota_remaining[pool_name] = actual_remaining
253
- if actual_remaining <= 0:
254
- self._close_gate(pool_name, pool.cooldown, f"exchange reports {pool_name} depleted")
255
- return
256
-
257
- # For rate pools, update backend
258
- key = self._key_for(pool)
259
- # Fire-and-forget since set_remaining may be async but we're in sync context
260
- # Use a helper to run it
261
- try:
262
- loop = asyncio.get_running_loop()
263
- loop.create_task(self._backend.set_remaining(key, actual_remaining))
264
- except RuntimeError:
265
- pass # No running loop — skip sync (e.g., during shutdown)
266
-
267
- def sync_quota(self, pool_name: str, remaining: float) -> None:
268
- """Update externally-managed quota pool from exchange response.
197
+ pool.sync(actual_remaining, capacity)
269
198
 
270
- Convenience wrapper for quota pools (e.g., Lighter volume quota
271
- reported in sendTx responses).
199
+ def get_quota_remaining(self, pool_name: str) -> float:
200
+ """Get remaining budget for a quota pool.
272
201
 
273
- Args:
274
- pool_name: Quota pool name
275
- remaining: Remaining quota reported by exchange
202
+ Returns 0 if pool doesn't exist or isn't a quota pool.
276
203
  """
277
- self._quota_remaining[pool_name] = remaining
278
- if remaining <= 0:
279
- pool = self._config.pools.get(pool_name)
280
- cooldown = pool.cooldown if pool else 15.0
281
- self._close_gate(pool_name, cooldown, f"quota {pool_name} depleted (remaining={remaining})")
282
-
283
- def _close_gate(self, pool_name: str, cooldown: float, reason: str) -> None:
284
- """Close a pool's gate for the given cooldown period."""
285
- gate = self._gates.get(pool_name)
286
- if gate is None:
287
- return
288
-
289
- verb = "extended" if not gate.is_set() else "closed"
290
- logger.warning(f"Rate limit gate {verb} for {self._exchange}:{pool_name} ({cooldown:.1f}s): {reason}")
291
- gate.clear()
292
-
293
- # Cancel existing reopen task
294
- old_task = self._gate_tasks.get(pool_name)
295
- if old_task is not None and not old_task.done():
296
- old_task.cancel()
297
-
298
- self._gate_tasks[pool_name] = asyncio.ensure_future(self._reopen_gate_after(pool_name, cooldown))
299
-
300
- async def _reopen_gate_after(self, pool_name: str, delay: float) -> None:
301
- """Reopen a pool's gate after a cooldown delay."""
302
- try:
303
- await asyncio.sleep(delay)
304
- gate = self._gates.get(pool_name)
305
- if gate is not None:
306
- gate.set()
307
- logger.info(f"Rate limit gate reopened for {self._exchange}:{pool_name} after {delay:.1f}s")
308
- except asyncio.CancelledError:
309
- pass
204
+ pool = self._pools.get(pool_name)
205
+ if isinstance(pool, QuotaPool):
206
+ return pool.remaining
207
+ return 0
310
208
 
311
209
  def reset_gates(self) -> None:
312
210
  """Reopen all gates and cancel pending reopen tasks.
313
211
 
314
212
  Call on connection reset / reconnection.
315
213
  """
316
- for pool_name, gate in self._gates.items():
317
- gate.set()
318
- for pool_name, task in self._gate_tasks.items():
319
- if not task.done():
320
- task.cancel()
321
- self._gate_tasks.clear()
214
+ for pool in self._pools.values():
215
+ pool.reset_gate()
322
216
 
323
217
  def is_gate_closed(self, pool_name: str | None = None) -> bool:
324
218
  """Check if a gate is closed.
@@ -327,9 +221,9 @@ class ExchangeRateLimiter:
327
221
  pool_name: Specific pool, or None to check if ANY gate is closed
328
222
  """
329
223
  if pool_name:
330
- gate = self._gates.get(pool_name)
331
- return gate is not None and not gate.is_set()
332
- return any(not gate.is_set() for gate in self._gates.values())
224
+ pool = self._pools.get(pool_name)
225
+ return pool is not None and pool.is_gate_closed
226
+ return any(pool.is_gate_closed for pool in self._pools.values())
333
227
 
334
228
  async def get_pool_state(self, pool_name: str) -> dict[str, Any] | None:
335
229
  """Get current state of a pool for monitoring.
@@ -337,32 +231,10 @@ class ExchangeRateLimiter:
337
231
  Returns:
338
232
  Dict with remaining, capacity, gate_closed, hits, etc. or None if pool doesn't exist
339
233
  """
340
- pool = self._config.pools.get(pool_name)
234
+ pool = self._pools.get(pool_name)
341
235
  if pool is None:
342
236
  return None
343
-
344
- if pool.pool_type == "quota":
345
- remaining = self._quota_remaining.get(pool_name, 0)
346
- else:
347
- key = self._key_for(pool)
348
- remaining = await self._backend.get_remaining(key, pool.capacity, pool.refill_rate)
349
- if remaining is None:
350
- remaining = pool.capacity
351
-
352
- return {
353
- "pool": pool_name,
354
- "exchange": self._exchange,
355
- "scope": pool.scope,
356
- "scope_id": self._scope_ids.get(pool.scope, "local"),
357
- "pool_type": pool.pool_type,
358
- "remaining": remaining,
359
- "capacity": pool.capacity,
360
- "utilization": 1.0 - (remaining / pool.capacity) if pool.capacity > 0 else 0,
361
- "gate_closed": self.is_gate_closed(pool_name),
362
- "hits": self._hits.get(pool_name, 0),
363
- "total_wait_s": self._total_wait.get(pool_name, 0),
364
- "consumed": self._consumed.get(pool_name, 0),
365
- }
237
+ return await pool.get_state()
366
238
 
367
239
  async def collect_metrics(self) -> list[dict[str, Any]]:
368
240
  """Collect current metrics for all pools.
@@ -374,7 +246,7 @@ class ExchangeRateLimiter:
374
246
  ctx.emitter.emit(m["name"], m["value"], m["tags"])
375
247
  """
376
248
  metrics = []
377
- for pool_name in self._config.pools:
249
+ for pool_name in self._pools:
378
250
  state = await self.get_pool_state(pool_name)
379
251
  if state is None:
380
252
  continue
@@ -399,5 +271,5 @@ class ExchangeRateLimiter:
399
271
  return metrics
400
272
 
401
273
  def __repr__(self) -> str:
402
- pools = ", ".join(f"{name}({pool.scope})" for name, pool in self._config.pools.items())
274
+ pools = ", ".join(f"{name}({pool.config.scope})" for name, pool in self._pools.items())
403
275
  return f"ExchangeRateLimiter({self._exchange}, pools=[{pools}])"