Qubx 0.6.71__tar.gz → 0.6.72__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.

Potentially problematic release.


This version of Qubx might be problematic. Click here for more details.

Files changed (214) hide show
  1. {qubx-0.6.71 → qubx-0.6.72}/PKG-INFO +1 -1
  2. {qubx-0.6.71 → qubx-0.6.72}/pyproject.toml +1 -1
  3. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/runner.py +6 -4
  4. qubx-0.6.72/src/qubx/connectors/ccxt/adapters/polling_adapter.py +250 -0
  5. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/__init__.py +26 -0
  6. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +80 -65
  7. qubx-0.6.72/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +3 -0
  8. qubx-0.6.72/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +69 -0
  9. qubx-0.6.72/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +520 -0
  10. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/factory.py +4 -0
  11. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/base.py +2 -1
  12. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/reader.py +181 -69
  13. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/utils.py +50 -26
  14. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/basics.py +6 -0
  15. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/advanced.py +41 -4
  16. qubx-0.6.71/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -439
  17. qubx-0.6.71/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -1
  18. qubx-0.6.71/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -161
  19. {qubx-0.6.71 → qubx-0.6.72}/LICENSE +0 -0
  20. {qubx-0.6.71 → qubx-0.6.72}/README.md +0 -0
  21. {qubx-0.6.71 → qubx-0.6.72}/build.py +0 -0
  22. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/__init__.py +0 -0
  23. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/_nb_magic.py +0 -0
  24. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/__init__.py +0 -0
  25. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/account.py +0 -0
  26. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/broker.py +0 -0
  27. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/data.py +0 -0
  28. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/management.py +0 -0
  29. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/ome.py +0 -0
  30. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/optimization.py +0 -0
  31. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/sentinels.py +0 -0
  32. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/simulated_data.py +0 -0
  33. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/simulated_exchange.py +0 -0
  34. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/simulator.py +0 -0
  35. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/backtester/utils.py +0 -0
  36. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/__init__.py +0 -0
  37. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/commands.py +0 -0
  38. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/deploy.py +0 -0
  39. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/misc.py +0 -0
  40. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/release.py +0 -0
  41. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/cli/tui.py +0 -0
  42. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/__init__.py +0 -0
  43. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/account.py +0 -0
  44. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  45. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/broker.py +0 -0
  46. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  47. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/data.py +0 -0
  48. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  49. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  50. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  51. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  52. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  53. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  54. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  55. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  56. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  57. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  58. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  59. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  60. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  61. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  62. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  63. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  64. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  65. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  66. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/tardis/data.py +0 -0
  67. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/connectors/tardis/utils.py +0 -0
  68. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/__init__.py +0 -0
  69. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/account.py +0 -0
  70. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/context.py +0 -0
  71. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/deque.py +0 -0
  72. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/errors.py +0 -0
  73. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/exceptions.py +0 -0
  74. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/helpers.py +0 -0
  75. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/initializer.py +0 -0
  76. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/interfaces.py +0 -0
  77. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/loggers.py +0 -0
  78. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/lookups.py +0 -0
  79. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/metrics.py +0 -0
  80. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/__init__.py +0 -0
  81. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/market.py +0 -0
  82. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/processing.py +0 -0
  83. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/subscription.py +0 -0
  84. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/trading.py +0 -0
  85. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/mixins/universe.py +0 -0
  86. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/series.pxd +0 -0
  87. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/series.pyi +0 -0
  88. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/series.pyx +0 -0
  89. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/stale_data_detector.py +0 -0
  90. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/utils.pyi +0 -0
  91. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/core/utils.pyx +0 -0
  92. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/__init__.py +0 -0
  93. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/composite.py +0 -0
  94. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/helpers.py +0 -0
  95. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/hft.py +0 -0
  96. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/readers.py +0 -0
  97. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/registry.py +0 -0
  98. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/data/tardis.py +0 -0
  99. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/__init__.py +0 -0
  100. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/base.py +0 -0
  101. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/composite.py +0 -0
  102. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/csv.py +0 -0
  103. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/indicator.py +0 -0
  104. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/inmemory.py +0 -0
  105. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/prometheus.py +0 -0
  106. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/emitters/questdb.py +0 -0
  107. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/__init__.py +0 -0
  108. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/composite.py +0 -0
  109. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/formatters/__init__.py +0 -0
  110. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/formatters/base.py +0 -0
  111. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/formatters/incremental.py +0 -0
  112. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/formatters/slack.py +0 -0
  113. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/redis_streams.py +0 -0
  114. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/exporters/slack.py +0 -0
  115. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/__init__.py +0 -0
  116. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/core.py +0 -0
  117. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/orderbook.py +0 -0
  118. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/price.py +0 -0
  119. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/trades.py +0 -0
  120. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/features/utils.py +0 -0
  121. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/gathering/simplest.py +0 -0
  122. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/health/__init__.py +0 -0
  123. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/health/base.py +0 -0
  124. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/__init__.py +0 -0
  125. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/csv.py +0 -0
  126. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/factory.py +0 -0
  127. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/inmemory.py +0 -0
  128. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/loggers/mongo.py +0 -0
  129. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/math/__init__.py +0 -0
  130. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/math/stats.py +0 -0
  131. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/notifications/__init__.py +0 -0
  132. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/notifications/composite.py +0 -0
  133. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/notifications/slack.py +0 -0
  134. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/notifications/throttler.py +0 -0
  135. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/pandaz/__init__.py +0 -0
  136. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/pandaz/ta.py +0 -0
  137. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/pandaz/utils.py +0 -0
  138. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/_build.py +0 -0
  139. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/crypto-fees.ini +0 -0
  140. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  141. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  142. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  143. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  144. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  145. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  146. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  147. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  148. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  149. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  150. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  151. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restarts/__init__.py +0 -0
  152. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restarts/state_resolvers.py +0 -0
  153. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restarts/time_finders.py +0 -0
  154. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/__init__.py +0 -0
  155. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/balance.py +0 -0
  156. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/factory.py +0 -0
  157. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/interfaces.py +0 -0
  158. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/position.py +0 -0
  159. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/signal.py +0 -0
  160. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/state.py +0 -0
  161. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/restorers/utils.py +0 -0
  162. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/ta/__init__.py +0 -0
  163. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/ta/indicators.pxd +0 -0
  164. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/ta/indicators.pyi +0 -0
  165. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/ta/indicators.pyx +0 -0
  166. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/__init__.py +0 -0
  167. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/base.py +0 -0
  168. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  169. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/config.yml.j2 +0 -0
  170. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  171. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  172. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  173. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  174. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  175. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/project/template.yml +0 -0
  176. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  177. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  178. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/config.yml.j2 +0 -0
  179. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  180. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  181. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  182. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/templates/simple/template.yml +0 -0
  183. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/__init__.py +0 -0
  184. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/composite.py +0 -0
  185. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/rebalancers.py +0 -0
  186. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/riskctrl.py +0 -0
  187. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/trackers/sizers.py +0 -0
  188. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/__init__.py +0 -0
  189. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/_pyxreloader.py +0 -0
  190. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/charting/lookinglass.py +0 -0
  191. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  192. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/collections.py +0 -0
  193. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/marketdata/binance.py +0 -0
  194. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/marketdata/ccxt.py +0 -0
  195. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/marketdata/dukas.py +0 -0
  196. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/misc.py +0 -0
  197. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/ntp.py +0 -0
  198. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/numbers_utils.py +0 -0
  199. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/orderbook.py +0 -0
  200. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/__init__.py +0 -0
  201. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/dashboard.py +0 -0
  202. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/data.py +0 -0
  203. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/interfaces.py +0 -0
  204. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  205. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  206. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/questdb.py +0 -0
  207. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/__init__.py +0 -0
  208. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  209. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/accounts.py +0 -0
  210. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/configs.py +0 -0
  211. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/factory.py +0 -0
  212. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/runner/runner.py +0 -0
  213. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/time.py +0 -0
  214. {qubx-0.6.71 → qubx-0.6.72}/src/qubx/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.71
3
+ Version: 0.6.72
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.71"
7
+ version = "0.6.72"
8
8
  description = "Qubx - Quantitative Trading Framework"
9
9
  authors = [ "Dmitry Marienko <dmitry.marienko@xlydian.com>", "Yuriy Arabskyy <yuriy.arabskyy@xlydian.com>",]
10
10
  readme = "README.md"
@@ -4,10 +4,9 @@ import numpy as np
4
4
  import pandas as pd
5
5
  from tqdm.auto import tqdm
6
6
 
7
- from qubx import logger
7
+ from qubx import QubxLogConfig, logger
8
8
  from qubx.backtester.sentinels import NoDataContinue
9
9
  from qubx.backtester.simulated_data import IterableSimulationData
10
- from qubx.backtester.utils import SimulationDataConfig, TimeGuardedWrapper
11
10
  from qubx.core.account import CompositeAccountProcessor
12
11
  from qubx.core.basics import SW, DataType, Instrument, TransactionCostsCalculator
13
12
  from qubx.core.context import StrategyContext
@@ -41,6 +40,7 @@ from .utils import (
41
40
  SimulatedTimeProvider,
42
41
  SimulationDataConfig,
43
42
  SimulationSetup,
43
+ TimeGuardedWrapper,
44
44
  )
45
45
 
46
46
 
@@ -356,8 +356,9 @@ class SimulationRunner:
356
356
  return False # No scheduled events, stop simulation
357
357
 
358
358
  def print_latency_report(self) -> None:
359
- _l_r = SW.latency_report()
360
- if _l_r is not None:
359
+ if (_l_r := SW.latency_report()) is not None:
360
+ _llvl = QubxLogConfig.get_log_level()
361
+ QubxLogConfig.set_log_level("INFO")
361
362
  logger.info(
362
363
  "<BLUE> Time spent in simulation report </BLUE>\n<r>"
363
364
  + _frame_to_str(
@@ -365,6 +366,7 @@ class SimulationRunner:
365
366
  )
366
367
  + "</r>"
367
368
  )
369
+ QubxLogConfig.set_log_level(_llvl)
368
370
 
369
371
  def _create_backtest_context(self) -> IStrategyContext:
370
372
  logger.debug(
@@ -0,0 +1,250 @@
1
+ """
2
+ Simplified polling adapter to convert CCXT fetch_* methods into watch_* behavior.
3
+
4
+ This adapter provides a much simpler approach:
5
+ - No background tasks or queues
6
+ - get_next_data() waits until it's time to poll, then polls synchronously
7
+ - Time-aligned polling (e.g., 11:30, 11:35, 11:40 for 5-minute intervals)
8
+ - Immediate polling when symbols change
9
+ """
10
+
11
+ import asyncio
12
+ import math
13
+ import time
14
+ from dataclasses import dataclass
15
+ from typing import Any, Callable, Dict, List, Optional, Set
16
+
17
+ from qubx import logger
18
+
19
+ # Constants
20
+ DEFAULT_POLL_INTERVAL = 300 # 5 minutes
21
+ MIN_POLL_INTERVAL = 1 # 1 second minimum
22
+ MAX_POLL_INTERVAL = 3600 # 1 hour maximum
23
+
24
+
25
+ @dataclass
26
+ class PollingConfig:
27
+ """Configuration for polling adapter."""
28
+
29
+ poll_interval_seconds: float = DEFAULT_POLL_INTERVAL
30
+
31
+ def __post_init__(self):
32
+ """Validate configuration after initialization."""
33
+ if not MIN_POLL_INTERVAL <= self.poll_interval_seconds <= MAX_POLL_INTERVAL:
34
+ raise ValueError(
35
+ f"poll_interval_seconds must be between {MIN_POLL_INTERVAL} and {MAX_POLL_INTERVAL}, "
36
+ f"got {self.poll_interval_seconds}"
37
+ )
38
+
39
+
40
+ class PollingToWebSocketAdapter:
41
+ """
42
+ Simplified polling adapter that polls synchronously when data is requested.
43
+
44
+ Key features:
45
+ - No background tasks or queues
46
+ - Time-aligned polling (respects clock boundaries)
47
+ - Immediate polling when symbols change
48
+ - Thread-safe symbol management
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ fetch_method: Callable,
54
+ symbols: Optional[List[str]] = None,
55
+ params: Optional[Dict[str, Any]] = None,
56
+ config: Optional[PollingConfig] = None,
57
+ ):
58
+ """
59
+ Initialize the simplified polling adapter.
60
+
61
+ Args:
62
+ fetch_method: The CCXT fetch_* method to call
63
+ symbols: Initial list of symbols to watch
64
+ params: Additional parameters for fetch_method
65
+ config: PollingConfig instance (uses default if None)
66
+ """
67
+ self.config = config if config is not None else PollingConfig()
68
+ self.fetch_method = fetch_method
69
+ self.params = params or {}
70
+ self.adapter_id = f"polling_adapter_{id(self)}"
71
+
72
+ # Thread-safe symbol management
73
+ self._symbols_lock = asyncio.Lock()
74
+ self._symbols: Set[str] = set(symbols or [])
75
+
76
+ # Polling state
77
+ self._last_poll_time: Optional[float] = None
78
+ self._symbols_changed = False # Flag to trigger immediate poll
79
+
80
+ # Statistics
81
+ self._poll_count = 0
82
+ self._error_count = 0
83
+
84
+ async def get_next_data(self) -> Dict[str, Any]:
85
+ """
86
+ Get the next available data by waiting until it's time to poll, then polling.
87
+
88
+ This method:
89
+ 1. Checks if symbols changed (immediate poll)
90
+ 2. Calculates when next poll should happen based on time alignment
91
+ 3. Waits until that time
92
+ 4. Polls and returns fresh data
93
+
94
+ Returns:
95
+ Dictionary containing fetched data for symbols
96
+ """
97
+ async with self._symbols_lock:
98
+ current_symbols = list(self._symbols)
99
+ symbols_changed = self._symbols_changed
100
+
101
+ if not current_symbols:
102
+ raise ValueError(f"No symbols configured for adapter {self.adapter_id}")
103
+
104
+ # If symbols changed, poll immediately
105
+ if symbols_changed:
106
+ logger.debug(f"Symbols changed, polling immediately for adapter {self.adapter_id}")
107
+ async with self._symbols_lock:
108
+ self._symbols_changed = False
109
+ return await self._poll_now(current_symbols)
110
+
111
+ # Calculate wait time for next aligned poll
112
+ wait_time = self._calculate_wait_time()
113
+
114
+ if wait_time > 0:
115
+ logger.debug(f"Waiting {wait_time:.1f}s for next poll cycle for adapter {self.adapter_id}")
116
+ await asyncio.sleep(wait_time)
117
+
118
+ # Time to poll
119
+ logger.debug(f"Polling now for adapter {self.adapter_id}")
120
+ return await self._poll_now(current_symbols)
121
+
122
+ def _calculate_wait_time(self) -> float:
123
+ """
124
+ Calculate how long to wait until the next aligned poll time.
125
+
126
+ For intervals >= 1 minute: aligns to clock boundaries (11:30, 11:35, 11:40)
127
+ For intervals < 1 minute: uses simple interval-based timing
128
+
129
+ Returns:
130
+ Number of seconds to wait (0 if should poll now)
131
+ """
132
+ current_time = time.time()
133
+ interval_seconds = self.config.poll_interval_seconds
134
+
135
+ # First poll is always immediate
136
+ if self._last_poll_time is None:
137
+ return 0
138
+
139
+ if interval_seconds >= 60:
140
+ # Time-aligned polling for intervals >= 1 minute using UTC
141
+ # Calculate next boundary based on seconds since epoch
142
+ next_boundary = math.ceil(current_time / interval_seconds) * interval_seconds
143
+ wait_time = next_boundary - current_time
144
+ return max(0, wait_time)
145
+ else:
146
+ # Simple interval-based polling for sub-minute intervals
147
+ next_poll_time = self._last_poll_time + interval_seconds
148
+ wait_time = next_poll_time - current_time
149
+ return max(0, wait_time)
150
+
151
+ async def _poll_now(self, symbols: List[str]) -> Dict[str, Any]:
152
+ """
153
+ Perform a poll operation immediately.
154
+
155
+ Args:
156
+ symbols: List of symbols to poll for
157
+
158
+ Returns:
159
+ Dictionary containing fetched data for symbols
160
+ """
161
+ self._poll_count += 1
162
+ self._last_poll_time = time.time()
163
+
164
+ logger.debug(f"Polling {len(symbols)} symbols for adapter {self.adapter_id}")
165
+
166
+ try:
167
+ # Filter out adapter-specific parameters
168
+ adapter_params = {"pollInterval", "interval", "updateInterval", "poll_interval_minutes"}
169
+ fetch_params = {k: v for k, v in self.params.items() if k not in adapter_params}
170
+
171
+ # Call the fetch method
172
+ result = await self.fetch_method(symbols, **fetch_params)
173
+
174
+ logger.debug(f"Poll completed successfully for adapter {self.adapter_id}")
175
+ return result
176
+
177
+ except Exception as e:
178
+ self._error_count += 1
179
+ logger.error(f"Poll failed for adapter {self.adapter_id}: {e}")
180
+ raise
181
+
182
+ async def update_symbols(self, new_symbols: List[str]) -> None:
183
+ """
184
+ Update the symbol list.
185
+
186
+ If symbols changed, the next call to get_next_data() will poll immediately.
187
+
188
+ Args:
189
+ new_symbols: New complete list of symbols to watch
190
+ """
191
+ async with self._symbols_lock:
192
+ old_symbols = self._symbols.copy()
193
+ self._symbols = set(new_symbols or [])
194
+ symbols_changed = old_symbols != self._symbols
195
+
196
+ if symbols_changed:
197
+ self._symbols_changed = True
198
+ logger.debug(
199
+ f"Symbols updated for adapter {self.adapter_id}: {len(old_symbols)} -> {len(self._symbols)}"
200
+ )
201
+
202
+ async def add_symbols(self, new_symbols: List[str]) -> None:
203
+ """Add symbols to the existing watch list."""
204
+ if not new_symbols:
205
+ return
206
+
207
+ async with self._symbols_lock:
208
+ before_count = len(self._symbols)
209
+ self._symbols.update(new_symbols)
210
+ after_count = len(self._symbols)
211
+
212
+ if after_count > before_count:
213
+ self._symbols_changed = True
214
+ logger.debug(f"Added {after_count - before_count} symbols to adapter {self.adapter_id}")
215
+
216
+ async def remove_symbols(self, symbols_to_remove: List[str]) -> None:
217
+ """Remove symbols from the watch list."""
218
+ if not symbols_to_remove:
219
+ return
220
+
221
+ async with self._symbols_lock:
222
+ before_count = len(self._symbols)
223
+ self._symbols.difference_update(symbols_to_remove)
224
+ after_count = len(self._symbols)
225
+
226
+ if after_count < before_count:
227
+ self._symbols_changed = True
228
+ logger.debug(f"Removed {before_count - after_count} symbols from adapter {self.adapter_id}")
229
+
230
+ def is_watching(self, symbol: Optional[str] = None) -> bool:
231
+ """Check if adapter has symbols configured to watch."""
232
+ if symbol is None:
233
+ return len(self._symbols) > 0
234
+ else:
235
+ return symbol in self._symbols
236
+
237
+ def get_statistics(self) -> Dict[str, Any]:
238
+ """Get adapter statistics for monitoring."""
239
+ return {
240
+ "adapter_id": self.adapter_id,
241
+ "symbol_count": len(self._symbols),
242
+ "poll_count": self._poll_count,
243
+ "error_count": self._error_count,
244
+ "last_poll_time": self._last_poll_time,
245
+ "poll_interval_seconds": self.config.poll_interval_seconds,
246
+ }
247
+
248
+ async def stop(self) -> None:
249
+ """Stop the adapter (cleanup method for compatibility)."""
250
+ logger.debug(f"Adapter {self.adapter_id} stopped (polled {self._poll_count} times, {self._error_count} errors)")
@@ -2,6 +2,7 @@
2
2
  This module contains the CCXT connectors for the exchanges.
3
3
  """
4
4
 
5
+ from dataclasses import dataclass
5
6
  from functools import partial
6
7
 
7
8
  import ccxt.pro as cxp
@@ -11,9 +12,21 @@ from .binance.broker import BinanceCcxtBroker
11
12
  from .binance.exchange import BINANCE_UM_MM, BinancePortfolioMargin, BinanceQV, BinanceQVUSDM
12
13
  from .bitfinex.bitfinex import BitfinexF
13
14
  from .bitfinex.bitfinex_account import BitfinexAccountProcessor
15
+ from .hyperliquid.broker import HyperliquidCcxtBroker
14
16
  from .hyperliquid.hyperliquid import Hyperliquid, HyperliquidF
15
17
  from .kraken.kraken import CustomKrakenFutures
16
18
 
19
+
20
+ @dataclass
21
+ class ReaderCapabilities:
22
+ """Configuration for exchange-specific reader capabilities."""
23
+
24
+ supports_bulk_funding: bool = True
25
+ supports_bulk_ohlcv: bool = True
26
+ max_symbols_per_request: int = 1000
27
+ default_funding_interval_hours: float = 8.0 # Default for most exchanges (Binance, etc.)
28
+
29
+
17
30
  EXCHANGE_ALIASES = {
18
31
  "binance": "binanceqv",
19
32
  "binance.um": "binanceqv_usdm",
@@ -32,12 +45,25 @@ CUSTOM_BROKERS = {
32
45
  "binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
33
46
  "binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
34
47
  "bitfinex.f": partial(CcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
48
+ "hyperliquid": partial(HyperliquidCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
49
+ "hyperliquid.f": partial(HyperliquidCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
35
50
  }
36
51
 
37
52
  CUSTOM_ACCOUNTS = {
38
53
  "bitfinex.f": BitfinexAccountProcessor,
39
54
  }
40
55
 
56
+ READER_CAPABILITIES = {
57
+ "hyperliquid": ReaderCapabilities(
58
+ supports_bulk_funding=False,
59
+ default_funding_interval_hours=1.0 # Hyperliquid uses 1-hour funding
60
+ ),
61
+ "hyperliquid.f": ReaderCapabilities(
62
+ supports_bulk_funding=False,
63
+ default_funding_interval_hours=1.0 # Hyperliquid uses 1-hour funding
64
+ ),
65
+ }
66
+
41
67
  cxp.binanceqv = BinanceQV # type: ignore
42
68
  cxp.binanceqv_usdm = BinanceQVUSDM # type: ignore
43
69
  cxp.binancepm = BinancePortfolioMargin # type: ignore
@@ -1,4 +1,4 @@
1
- from typing import Dict, List
1
+ from typing import Dict, List, cast
2
2
 
3
3
  import ccxt.pro as cxp
4
4
  from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp
@@ -33,7 +33,7 @@ class BinanceQV(cxp.binance):
33
33
  "watchTrades": {
34
34
  "name": "aggTrade",
35
35
  },
36
- "localOrderBookLimit": 10_000, # set a large limit to avoid cutting off the orderbook
36
+ "localOrderBookLimit": 50_000, # set a large limit to avoid cutting off the orderbook
37
37
  },
38
38
  "exceptions": {
39
39
  "exact": {
@@ -243,23 +243,23 @@ class BinanceQV(cxp.binance):
243
243
  def clean_stream_state(self, subscription: dict):
244
244
  """
245
245
  Clean up stream state mappings during unsubscription to prevent UnsubscribeError.
246
-
246
+
247
247
  This fixes the root cause of bulk unsubscription failures by properly cleaning up
248
248
  stale stream mappings that cause state conflicts in CCXT.
249
249
  """
250
- symbols = self.safe_list(subscription, 'symbols', [])
251
- topic = self.safe_string(subscription, 'topic', '')
252
-
250
+ symbols = self.safe_list(subscription, "symbols", [])
251
+ topic = self.safe_string(subscription, "topic", "")
252
+
253
253
  if topic and len(symbols) > 1:
254
254
  # Clean up bulk subscription stream mappings
255
255
  subscription_hash = f"multiple{topic.upper()}"
256
- streamBySubscriptionsHash = self.safe_dict(self.options, 'streamBySubscriptionsHash', {})
256
+ streamBySubscriptionsHash = self.safe_dict(self.options, "streamBySubscriptionsHash", {})
257
257
  if subscription_hash in streamBySubscriptionsHash:
258
258
  stream = streamBySubscriptionsHash[subscription_hash]
259
259
  del streamBySubscriptionsHash[subscription_hash]
260
-
260
+
261
261
  # Clean up subscription counts to maintain accurate state
262
- numSubscriptionsByStream = self.safe_dict(self.options, 'numSubscriptionsByStream', {})
262
+ numSubscriptionsByStream = self.safe_dict(self.options, "numSubscriptionsByStream", {})
263
263
  if stream in numSubscriptionsByStream:
264
264
  current_count = numSubscriptionsByStream[stream]
265
265
  new_count = max(0, current_count - len(symbols))
@@ -267,11 +267,11 @@ class BinanceQV(cxp.binance):
267
267
  del numSubscriptionsByStream[stream]
268
268
  else:
269
269
  numSubscriptionsByStream[stream] = new_count
270
-
270
+
271
271
  def clean_cache(self, subscription: dict):
272
272
  """
273
273
  Override clean_cache to include stream state cleanup.
274
-
274
+
275
275
  This ensures proper cleanup during unsubscription operations.
276
276
  """
277
277
  super().clean_cache(subscription)
@@ -280,94 +280,100 @@ class BinanceQV(cxp.binance):
280
280
  async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}):
281
281
  """
282
282
  Enhanced bulk OHLCV unsubscription with proper state validation and cleanup.
283
-
283
+
284
284
  This override fixes UnsubscribeError issues by:
285
285
  1. Validating subscription state before attempting unsubscription
286
- 2. Filtering to only valid subscriptions
286
+ 2. Filtering to only valid subscriptions
287
287
  3. Implementing graceful error handling for state conflicts
288
288
  4. Forcing cleanup of remaining state on errors
289
289
  """
290
290
  await self.load_markets()
291
-
291
+
292
292
  # Standard setup from parent class
293
- type = 'spot'
293
+ type = "spot"
294
294
  marketType = None
295
295
  firstMarket = None
296
-
296
+
297
297
  # Handle futures market detection
298
298
  if len(symbolsAndTimeframes) > 0:
299
299
  firstSymbol = symbolsAndTimeframes[0][0]
300
300
  firstMarket = self.market(firstSymbol)
301
- marketType, params = self.handle_market_type_and_params('unWatchOHLCVForSymbols', firstMarket, params)
302
- if marketType != 'spot':
303
- type = 'future'
304
-
301
+ marketType, params = self.handle_market_type_and_params("unWatchOHLCVForSymbols", firstMarket, params)
302
+ if marketType != "spot":
303
+ type = "future"
304
+
305
305
  # Build subscription hashes
306
306
  messageHashes = []
307
307
  subMessageHashes = []
308
308
  rawHashes = []
309
-
309
+
310
310
  for symbolAndTimeframe in symbolsAndTimeframes:
311
311
  symbol = symbolAndTimeframe[0]
312
312
  timeframe = symbolAndTimeframe[1]
313
313
  market = self.market(symbol)
314
- lowercaseId = market['lowercaseId']
314
+ lowercaseId = market["lowercaseId"]
315
315
  interval = self.timeframes[timeframe]
316
- rawHash = lowercaseId + '@kline_' + interval
316
+ rawHash = lowercaseId + "@kline_" + interval
317
317
  rawHashes.append(rawHash)
318
- messageHash = 'ohlcv:' + symbol + ':' + timeframe
318
+ messageHash = "ohlcv:" + symbol + ":" + timeframe
319
319
  messageHashes.append(messageHash)
320
320
  subMessageHashes.append(rawHash)
321
-
321
+
322
322
  # Get client and validate subscription state BEFORE attempting unsubscription
323
- url = self.urls['api']['ws'][type] + '/' + self.stream(type, 'multipleOHLCV')
323
+ url = self.urls["api"]["ws"][type] + "/" + self.stream(type, "multipleOHLCV")
324
324
  client = self.client(url)
325
-
325
+
326
326
  # Filter to only valid subscriptions to prevent state conflicts
327
327
  valid_messageHashes = []
328
328
  valid_subMessageHashes = []
329
329
  valid_rawHashes = []
330
-
330
+
331
331
  for i, subHash in enumerate(subMessageHashes):
332
332
  if subHash in client.subscriptions:
333
333
  valid_messageHashes.append(messageHashes[i])
334
334
  valid_subMessageHashes.append(subHash)
335
335
  valid_rawHashes.append(rawHashes[i])
336
-
336
+
337
337
  if not valid_messageHashes:
338
338
  # Nothing to unsubscribe - return success
339
339
  return True
340
-
340
+
341
341
  # Build unsubscription request with only valid subscriptions
342
342
  requestId = self.request_id(url)
343
343
  request = {
344
- 'method': 'UNSUBSCRIBE',
345
- 'params': valid_rawHashes,
346
- 'id': requestId,
344
+ "method": "UNSUBSCRIBE",
345
+ "params": valid_rawHashes,
346
+ "id": requestId,
347
347
  }
348
-
348
+
349
349
  subscription = {
350
- 'unsubscribe': True,
351
- 'id': str(requestId),
352
- 'subMessageHashes': valid_subMessageHashes,
353
- 'messageHashes': valid_messageHashes,
354
- 'symbols': [st[0] for st in symbolsAndTimeframes if st[0] in [sh.split('@')[0].upper() for sh in valid_subMessageHashes]],
355
- 'topic': 'ohlcv',
350
+ "unsubscribe": True,
351
+ "id": str(requestId),
352
+ "subMessageHashes": valid_subMessageHashes,
353
+ "messageHashes": valid_messageHashes,
354
+ "symbols": [
355
+ st[0]
356
+ for st in symbolsAndTimeframes
357
+ if st[0] in [sh.split("@")[0].upper() for sh in valid_subMessageHashes]
358
+ ],
359
+ "topic": "ohlcv",
356
360
  }
357
-
361
+
358
362
  try:
359
363
  # Attempt unsubscription with validated state
360
- return await self.watch_multiple(url, valid_messageHashes, self.extend(request, params), valid_messageHashes, subscription)
361
-
364
+ return await self.watch_multiple(
365
+ url, valid_messageHashes, self.extend(request, params), valid_messageHashes, subscription
366
+ )
367
+
362
368
  except Exception as e:
363
369
  # Handle UnsubscribeError and other state conflicts gracefully
364
370
  from ccxt.base.errors import UnsubscribeError
365
-
366
- if isinstance(e, UnsubscribeError) or 'UnsubscribeError' in str(type(e)):
371
+
372
+ if isinstance(e, UnsubscribeError) or "UnsubscribeError" in str(type(e)):
367
373
  # Log the issue but don't crash - force cleanup instead
368
- if hasattr(self, 'logger'):
374
+ if hasattr(self, "logger"):
369
375
  self.logger.warning(f"Bulk OHLCV unsubscription state conflict, forcing cleanup: {e}")
370
-
376
+
371
377
  # Force cleanup of remaining subscription state
372
378
  for subHash in valid_subMessageHashes:
373
379
  if subHash in client.subscriptions:
@@ -378,10 +384,10 @@ class BinanceQV(cxp.binance):
378
384
  except:
379
385
  pass
380
386
  del client.futures[subHash]
381
-
387
+
382
388
  # Clean up our stream state as well
383
389
  self.clean_stream_state(subscription)
384
-
390
+
385
391
  return True # Return success after cleanup
386
392
  else:
387
393
  # Re-raise other errors
@@ -418,32 +424,40 @@ class BinanceQVUSDM(cxp.binanceusdm, BinanceQV):
418
424
  },
419
425
  )
420
426
 
427
+ async def get_funding_interval_hours_for_symbol(self, symbol: str) -> float:
428
+ await self._update_funding_intervals()
429
+ return cast(float, self._funding_intervals.get(symbol, 8.0))
430
+
431
+ async def get_funding_interval_hours(self) -> dict[str, float]:
432
+ await self._update_funding_intervals()
433
+ return cast(dict[str, float], self._funding_intervals)
434
+
421
435
  async def watch_funding_rates(self, symbols: List[str] | None = None):
422
436
  symbol_count = len(symbols) if symbols else 0
423
-
437
+
424
438
  try:
425
439
  await self.load_markets()
426
440
  await self._update_funding_intervals()
427
-
441
+
428
442
  # Use watch_mark_prices which streams one symbol per WebSocket message
429
443
  # This is normal behavior - WebSocket messages contain one symbol at a time
430
444
  mark_prices = await self.watch_mark_prices(symbols)
431
-
445
+
432
446
  if not mark_prices:
433
447
  raise Exception("No mark price data received")
434
-
448
+
435
449
  # Process whatever symbol(s) we received (usually 1 per WebSocket message)
436
450
  funding_rates = {}
437
451
  processed_count = 0
438
-
452
+
439
453
  for symbol, info in mark_prices.items():
440
454
  try:
441
455
  interval = self._funding_intervals.get(symbol, "8h")
442
-
456
+
443
457
  # Ensure we have the required fields for funding rate
444
458
  if "info" not in info or "r" not in info["info"]:
445
459
  continue
446
-
460
+
447
461
  funding_rates[symbol] = {
448
462
  "timestamp": info["timestamp"],
449
463
  "interval": interval,
@@ -453,15 +467,15 @@ class BinanceQVUSDM(cxp.binanceusdm, BinanceQV):
453
467
  "indexPrice": info["indexPrice"],
454
468
  }
455
469
  processed_count += 1
456
-
470
+
457
471
  except Exception as e:
458
472
  continue
459
-
473
+
460
474
  if processed_count == 0:
461
475
  raise Exception("No funding rates could be processed from mark price data")
462
-
476
+
463
477
  return funding_rates
464
-
478
+
465
479
  except Exception as e:
466
480
  raise
467
481
 
@@ -568,20 +582,21 @@ class BinanceQVUSDM(cxp.binanceusdm, BinanceQV):
568
582
  async def un_watch_funding_rates(self):
569
583
  """Unwatch funding rates to ensure fresh connections"""
570
584
  from qubx import logger
585
+
571
586
  logger.debug("un_watch_funding_rates called - resetting connection")
572
-
587
+
573
588
  # Try to unwatch mark prices if possible
574
- if hasattr(self, 'un_watch_mark_prices'):
589
+ if hasattr(self, "un_watch_mark_prices"):
575
590
  try:
576
591
  await self.un_watch_mark_prices()
577
592
  except Exception as e:
578
593
  logger.debug(f"Error unwatching mark prices: {e}")
579
-
594
+
580
595
  # Clear any internal caches that might exist
581
- if hasattr(self, 'markPrices') and self.markPrices:
596
+ if hasattr(self, "markPrices") and self.markPrices:
582
597
  self.markPrices.clear()
583
598
  logger.debug("Cleared mark prices cache")
584
-
599
+
585
600
  return None
586
601
 
587
602
 
@@ -0,0 +1,3 @@
1
+ # Hyperliquid exchange overrides
2
+
3
+ from .broker import HyperliquidCcxtBroker