Qubx 0.6.78__tar.gz → 0.6.85__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 (216) hide show
  1. {qubx-0.6.78 → qubx-0.6.85}/PKG-INFO +4 -2
  2. {qubx-0.6.78 → qubx-0.6.85}/pyproject.toml +1 -1
  3. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/management.py +12 -1
  4. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/runner.py +9 -3
  5. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/account.py +23 -4
  6. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchange_manager.py +66 -63
  7. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +17 -9
  8. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/orderbook.py +8 -6
  9. qubx-0.6.85/src/qubx/connectors/ccxt/handlers/trade.py +207 -0
  10. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/account.py +2 -1
  11. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/basics.py +4 -0
  12. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/helpers.py +9 -3
  13. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/metrics.py +101 -2
  14. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/subscription.py +7 -1
  15. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/series.pxd +3 -2
  16. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/series.pyi +3 -2
  17. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/series.pyx +30 -5
  18. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/helpers.py +34 -12
  19. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/ta/indicators.pxd +5 -1
  20. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/ta/indicators.pyi +7 -2
  21. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/ta/indicators.pyx +136 -18
  22. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/riskctrl.py +83 -3
  23. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/charting/lookinglass.py +42 -0
  24. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/misc.py +23 -6
  25. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/configs.py +1 -0
  26. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/runner.py +27 -2
  27. qubx-0.6.78/src/qubx/connectors/ccxt/handlers/trade.py +0 -111
  28. {qubx-0.6.78 → qubx-0.6.85}/LICENSE +0 -0
  29. {qubx-0.6.78 → qubx-0.6.85}/README.md +0 -0
  30. {qubx-0.6.78 → qubx-0.6.85}/build.py +0 -0
  31. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/__init__.py +0 -0
  32. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/_nb_magic.py +0 -0
  33. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/__init__.py +0 -0
  34. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/account.py +0 -0
  35. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/broker.py +0 -0
  36. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/data.py +0 -0
  37. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/ome.py +0 -0
  38. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/optimization.py +0 -0
  39. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/sentinels.py +0 -0
  40. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/simulated_data.py +0 -0
  41. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/simulated_exchange.py +0 -0
  42. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/simulator.py +0 -0
  43. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/backtester/utils.py +0 -0
  44. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/__init__.py +0 -0
  45. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/commands.py +0 -0
  46. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/deploy.py +0 -0
  47. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/misc.py +0 -0
  48. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/release.py +0 -0
  49. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/cli/tui.py +0 -0
  50. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/__init__.py +0 -0
  51. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  52. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  53. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/broker.py +0 -0
  54. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  55. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/data.py +0 -0
  56. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  57. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  58. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  59. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  60. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  61. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  62. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  63. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  64. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  65. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  66. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/factory.py +0 -0
  67. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  68. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  69. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  70. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  71. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  72. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  73. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  74. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  75. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/reader.py +0 -0
  76. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  77. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  78. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  79. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/utils.py +0 -0
  80. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  81. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/tardis/data.py +0 -0
  82. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/connectors/tardis/utils.py +0 -0
  83. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/__init__.py +0 -0
  84. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/context.py +0 -0
  85. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/deque.py +0 -0
  86. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/errors.py +0 -0
  87. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/exceptions.py +0 -0
  88. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/initializer.py +0 -0
  89. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/interfaces.py +0 -0
  90. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/loggers.py +0 -0
  91. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/lookups.py +0 -0
  92. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/__init__.py +0 -0
  93. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/market.py +0 -0
  94. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/processing.py +0 -0
  95. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/trading.py +0 -0
  96. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/universe.py +0 -0
  97. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/mixins/utils.py +0 -0
  98. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/stale_data_detector.py +0 -0
  99. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/utils.pyi +0 -0
  100. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/core/utils.pyx +0 -0
  101. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/__init__.py +0 -0
  102. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/composite.py +0 -0
  103. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/hft.py +0 -0
  104. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/readers.py +0 -0
  105. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/registry.py +0 -0
  106. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/data/tardis.py +0 -0
  107. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/__init__.py +0 -0
  108. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/base.py +0 -0
  109. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/composite.py +0 -0
  110. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/csv.py +0 -0
  111. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/indicator.py +0 -0
  112. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/inmemory.py +0 -0
  113. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/prometheus.py +0 -0
  114. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/emitters/questdb.py +0 -0
  115. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/__init__.py +0 -0
  116. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/composite.py +0 -0
  117. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/formatters/__init__.py +0 -0
  118. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/formatters/base.py +0 -0
  119. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/formatters/incremental.py +0 -0
  120. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/formatters/slack.py +0 -0
  121. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/redis_streams.py +0 -0
  122. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/exporters/slack.py +0 -0
  123. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/__init__.py +0 -0
  124. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/core.py +0 -0
  125. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/orderbook.py +0 -0
  126. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/price.py +0 -0
  127. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/trades.py +0 -0
  128. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/features/utils.py +0 -0
  129. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/gathering/simplest.py +0 -0
  130. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/health/__init__.py +0 -0
  131. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/health/base.py +0 -0
  132. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/__init__.py +0 -0
  133. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/csv.py +0 -0
  134. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/factory.py +0 -0
  135. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/inmemory.py +0 -0
  136. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/loggers/mongo.py +0 -0
  137. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/math/__init__.py +0 -0
  138. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/math/stats.py +0 -0
  139. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/notifications/__init__.py +0 -0
  140. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/notifications/composite.py +0 -0
  141. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/notifications/slack.py +0 -0
  142. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/notifications/throttler.py +0 -0
  143. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/pandaz/__init__.py +0 -0
  144. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/pandaz/ta.py +0 -0
  145. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/pandaz/utils.py +0 -0
  146. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/_build.py +0 -0
  147. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/crypto-fees.ini +0 -0
  148. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  149. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  150. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  151. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  152. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  153. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  154. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  155. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  156. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  157. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  158. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  159. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restarts/__init__.py +0 -0
  160. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restarts/state_resolvers.py +0 -0
  161. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restarts/time_finders.py +0 -0
  162. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/__init__.py +0 -0
  163. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/balance.py +0 -0
  164. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/factory.py +0 -0
  165. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/interfaces.py +0 -0
  166. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/position.py +0 -0
  167. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/signal.py +0 -0
  168. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/state.py +0 -0
  169. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/restorers/utils.py +0 -0
  170. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/ta/__init__.py +0 -0
  171. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/__init__.py +0 -0
  172. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/base.py +0 -0
  173. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  174. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/config.yml.j2 +0 -0
  175. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  176. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  177. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  178. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  179. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  180. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/project/template.yml +0 -0
  181. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  182. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  183. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/config.yml.j2 +0 -0
  184. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  185. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  186. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  187. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/templates/simple/template.yml +0 -0
  188. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/__init__.py +0 -0
  189. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/advanced.py +0 -0
  190. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/composite.py +0 -0
  191. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/rebalancers.py +0 -0
  192. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/trackers/sizers.py +0 -0
  193. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/__init__.py +0 -0
  194. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/_pyxreloader.py +0 -0
  195. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  196. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/charting/orderbook.py +0 -0
  197. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/collections.py +0 -0
  198. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/marketdata/binance.py +0 -0
  199. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/marketdata/ccxt.py +0 -0
  200. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/marketdata/dukas.py +0 -0
  201. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/ntp.py +0 -0
  202. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/numbers_utils.py +0 -0
  203. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/orderbook.py +0 -0
  204. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/__init__.py +0 -0
  205. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/dashboard.py +0 -0
  206. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/data.py +0 -0
  207. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/interfaces.py +0 -0
  208. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  209. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  210. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/questdb.py +0 -0
  211. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/__init__.py +0 -0
  212. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  213. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/accounts.py +0 -0
  214. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/runner/factory.py +0 -0
  215. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/time.py +0 -0
  216. {qubx-0.6.78 → qubx-0.6.85}/src/qubx/utils/version.py +0 -0
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.78
3
+ Version: 0.6.85
4
4
  Summary: Qubx - Quantitative Trading Framework
5
+ License-File: LICENSE
5
6
  Author: Dmitry Marienko
6
7
  Author-email: dmitry.marienko@xlydian.com
7
8
  Requires-Python: >=3.10,<4.0
@@ -10,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
10
11
  Classifier: Programming Language :: Python :: 3.11
11
12
  Classifier: Programming Language :: Python :: 3.12
12
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
13
15
  Requires-Dist: aiohttp (>=3.10.11,<3.11.0)
14
16
  Requires-Dist: ccxt (>=4.2.68,<5.0.0)
15
17
  Requires-Dist: croniter (>=2.0.5,<3.0.0)
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.78"
7
+ version = "0.6.85"
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"
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  import numpy as np
7
7
  import pandas as pd
8
8
  import yaml
9
+ from tqdm.auto import tqdm
9
10
 
10
11
  from qubx.core.metrics import TradingSessionResult
11
12
  from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
@@ -420,8 +421,10 @@ class BacktestsResultsManager:
420
421
  Returns:
421
422
  plotly.graph_objects.Figure: The plot of the variation.
422
423
  """
423
- import plotly.express as px
424
424
  from itertools import cycle
425
+
426
+ import plotly.express as px
427
+
425
428
  from qubx.utils.misc import string_shortener
426
429
 
427
430
  _vars = self.variations.get(variation_idx)
@@ -507,3 +510,11 @@ class BacktestsResultsManager:
507
510
  )
508
511
  )
509
512
  return figure
513
+
514
+ def export_backtests_to_markdown(self, path: str, tags: tuple[str] | None = None):
515
+ """
516
+ Export backtests to markdown format
517
+ """
518
+ for n, v in tqdm(self.results.items()):
519
+ r = TradingSessionResult.from_file(v.get("path"))
520
+ r.to_markdown(path, list(tags) if tags else None)
@@ -15,6 +15,7 @@ from qubx.core.helpers import extract_parameters_from_object, full_qualified_cla
15
15
  from qubx.core.initializer import BasicStrategyInitializer
16
16
  from qubx.core.interfaces import (
17
17
  CtrlChannel,
18
+ IDataProvider,
18
19
  IMetricEmitter,
19
20
  IStrategy,
20
21
  IStrategyContext,
@@ -224,7 +225,7 @@ class SimulationRunner:
224
225
  cc = self.channel
225
226
  t = np.datetime64(data.time, "ns")
226
227
  _account = self.account.get_account_processor(instrument.exchange)
227
- _data_provider = self._exchange_to_data_provider[instrument.exchange]
228
+ _data_provider = self._get_data_provider(instrument.exchange)
228
229
  assert isinstance(_account, SimulatedAccountProcessor)
229
230
  assert isinstance(_data_provider, SimulatedDataProvider)
230
231
 
@@ -249,7 +250,7 @@ class SimulationRunner:
249
250
  cc = self.channel
250
251
  t = np.datetime64(data.time, "ns")
251
252
  _account = self.account.get_account_processor(instrument.exchange)
252
- _data_provider = self._exchange_to_data_provider[instrument.exchange]
253
+ _data_provider = self._get_data_provider(instrument.exchange)
253
254
  assert isinstance(_account, SimulatedAccountProcessor)
254
255
  assert isinstance(_data_provider, SimulatedDataProvider)
255
256
 
@@ -267,6 +268,11 @@ class SimulationRunner:
267
268
 
268
269
  return cc.control.is_set()
269
270
 
271
+ def _get_data_provider(self, exchange: str) -> IDataProvider:
272
+ if exchange in self._exchange_to_data_provider:
273
+ return self._exchange_to_data_provider[exchange]
274
+ raise ValueError(f"Data provider for exchange {exchange} not found")
275
+
270
276
  def _run(self, start: pd.Timestamp, stop: pd.Timestamp, silent: bool = False) -> None:
271
277
  logger.info(f"{self.__class__.__name__} ::: Simulation started at {start} :::")
272
278
 
@@ -328,7 +334,7 @@ class SimulationRunner:
328
334
 
329
335
  if not _run(instrument, data_type, event, is_hist):
330
336
  return False
331
-
337
+
332
338
  return True
333
339
 
334
340
  def _handle_no_data_scenario(self, stop_time):
@@ -82,6 +82,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
82
82
  open_order_backoff: str = "1Min",
83
83
  max_position_restore_days: int = 5,
84
84
  max_retries: int = 10,
85
+ connection_timeout: int = 30,
85
86
  read_only: bool = False,
86
87
  ):
87
88
  super().__init__(
@@ -109,6 +110,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
109
110
  self._latest_instruments = set()
110
111
  self._subscription_manager = None
111
112
  self._read_only = read_only
113
+ self._connection_timeout = connection_timeout
112
114
 
113
115
  def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
114
116
  self._subscription_manager = manager
@@ -128,6 +130,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
128
130
 
129
131
  if not self.exchange_manager.exchange.isSandboxModeEnabled:
130
132
  # - start polling tasks
133
+ self._loop.submit(self.exchange_manager.exchange.load_markets()).result()
131
134
  self._polling_tasks["balance"] = self._loop.submit(
132
135
  self._poller("balance", self._update_balance, self.balance_interval)
133
136
  )
@@ -172,6 +175,14 @@ class CcxtAccountProcessor(BasicAccountProcessor):
172
175
  super().update_position_price(time, instrument, update)
173
176
 
174
177
  def get_total_capital(self, exchange: str | None = None) -> float:
178
+ # If polling is not running yet, we need to fetch balance data directly
179
+ if not self._is_running and self.exchange_manager.exchange:
180
+ try:
181
+ future = self._loop.submit(self._update_balance())
182
+ future.result(timeout=self._connection_timeout)
183
+ except Exception as e:
184
+ logger.warning(f"Failed to fetch balance data before polling started: {e}")
185
+
175
186
  # sum of balances + market value of all positions on non spot/margin
176
187
  _currency_to_value = {c: self._get_currency_value(b.total, c) for c, b in self._balances.items()}
177
188
  _positions_value = sum([p.market_value_funds for p in self._positions.values() if p.instrument.is_futures()])
@@ -294,7 +305,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
294
305
  async def _update_positions(self) -> None:
295
306
  # fetch and update positions from exchange
296
307
  ccxt_positions = await self.exchange_manager.exchange.fetch_positions()
297
- positions = ccxt_convert_positions(ccxt_positions, self.exchange_manager.exchange.name, self.exchange_manager.exchange.markets) # type: ignore
308
+ positions = ccxt_convert_positions(
309
+ ccxt_positions, self.exchange_manager.exchange.name, self.exchange_manager.exchange.markets
310
+ ) # type: ignore
298
311
  # update required instruments that we need to subscribe to
299
312
  self._required_instruments.update([p.instrument for p in positions])
300
313
  # update positions
@@ -458,7 +471,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
458
471
 
459
472
  async def _cancel_order(order: Order) -> None:
460
473
  try:
461
- await self.exchange_manager.exchange.cancel_order(order.id, symbol=instrument_to_ccxt_symbol(order.instrument))
474
+ await self.exchange_manager.exchange.cancel_order(
475
+ order.id, symbol=instrument_to_ccxt_symbol(order.instrument)
476
+ )
462
477
  logger.debug(
463
478
  f" :: [SYNC] Canceled {order.id} {order.instrument.symbol} {order.side} {order.quantity} @ {order.price} ({order.status})"
464
479
  )
@@ -476,7 +491,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
476
491
  ) -> dict[str, Order]:
477
492
  _start_ms = self._get_start_time_in_ms(days_before) if limit is None else None
478
493
  _ccxt_symbol = instrument_to_ccxt_symbol(instrument)
479
- _fetcher = self.exchange_manager.exchange.fetch_open_orders if is_open else self.exchange_manager.exchange.fetch_orders
494
+ _fetcher = (
495
+ self.exchange_manager.exchange.fetch_open_orders if is_open else self.exchange_manager.exchange.fetch_orders
496
+ )
480
497
  _raw_orders = await _fetcher(_ccxt_symbol, since=_start_ms, limit=limit)
481
498
  _orders = [ccxt_convert_order_info(instrument, o) for o in _raw_orders]
482
499
  _id_to_order = {o.id: o for o in _orders}
@@ -533,7 +550,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
533
550
  async def _watch_executions():
534
551
  exec = await self.exchange_manager.exchange.watch_orders()
535
552
  for report in exec:
536
- instrument = ccxt_find_instrument(report["symbol"], self.exchange_manager.exchange, _symbol_to_instrument)
553
+ instrument = ccxt_find_instrument(
554
+ report["symbol"], self.exchange_manager.exchange, _symbol_to_instrument
555
+ )
537
556
  order = ccxt_convert_order_info(instrument, report)
538
557
  deals = ccxt_extract_deals_from_exec(report)
539
558
  channel.send((instrument, "order", order, False))
@@ -24,12 +24,13 @@ SECONDS_PER_HOUR = 3600
24
24
 
25
25
  # Custom stall detection thresholds (in seconds)
26
26
  STALL_THRESHOLDS = {
27
- 'funding_payment': 12 * SECONDS_PER_HOUR, # 12 hours = 43,200s
28
- 'open_interest': 30 * 60, # 30 minutes = 1,800s
29
- 'orderbook': 5 * 60, # 5 minutes = 300s
30
- 'trade': 60 * 60, # 60 minutes = 3,600s
31
- 'liquidation': 7 * 24 * SECONDS_PER_HOUR, # 7 days = 604,800s
32
- 'ohlc': 15 * 60, # 15 minutes = 900s
27
+ "funding_payment": 12 * SECONDS_PER_HOUR, # 12 hours = 43,200s
28
+ "open_interest": 30 * 60, # 30 minutes = 1,800s
29
+ "orderbook": 5 * 60, # 5 minutes = 300s
30
+ "trade": 60 * 60, # 60 minutes = 3,600s
31
+ "liquidation": 7 * 24 * SECONDS_PER_HOUR, # 7 days = 604,800s
32
+ "ohlc": 5 * 60, # 5 minutes = 300s
33
+ "quote": 5 * 60, # 5 minutes = 300s
33
34
  }
34
35
  DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
35
36
 
@@ -37,10 +38,10 @@ DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
37
38
  class ExchangeManager(IDataArrivalListener):
38
39
  """
39
40
  Wrapper for CCXT Exchange that handles recreation internally with self-monitoring.
40
-
41
+
41
42
  Exposes the underlying exchange via .exchange property for explicit access.
42
43
  Self-monitors for data stalls and triggers recreation automatically.
43
-
44
+
44
45
  Key Features:
45
46
  - Explicit .exchange property for CCXT access
46
47
  - Self-contained stall detection and recreation triggering
@@ -48,7 +49,7 @@ class ExchangeManager(IDataArrivalListener):
48
49
  - Atomic exchange transitions during recreation
49
50
  - Background monitoring thread for stall detection
50
51
  """
51
-
52
+
52
53
  _exchange: cxp.Exchange # Type hint that this is always a valid exchange
53
54
 
54
55
  def __init__(
@@ -61,10 +62,10 @@ class ExchangeManager(IDataArrivalListener):
61
62
  check_interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
62
63
  ):
63
64
  """Initialize ExchangeManager with underlying CCXT exchange.
64
-
65
+
65
66
  Args:
66
67
  exchange_name: Exchange name for factory (e.g., "binance.um")
67
- factory_params: Parameters for get_ccxt_exchange()
68
+ factory_params: Parameters for get_ccxt_exchange()
68
69
  initial_exchange: Pre-created exchange instance (from factory)
69
70
  max_recreations: Maximum recreation attempts before giving up
70
71
  reset_interval_hours: Hours between recreation count resets
@@ -74,24 +75,24 @@ class ExchangeManager(IDataArrivalListener):
74
75
  self._factory_params = factory_params.copy()
75
76
  self._max_recreations = max_recreations
76
77
  self._reset_interval_hours = reset_interval_hours
77
-
78
+
78
79
  # Recreation state
79
80
  self._recreation_count = 0
80
81
  self._recreation_lock = threading.RLock()
81
82
  self._last_successful_reset = time.time()
82
-
83
+
83
84
  # Stall detection state
84
85
  self._check_interval = check_interval_seconds
85
86
  self._last_data_times: dict[str, float] = {}
86
87
  self._data_lock = threading.RLock()
87
-
88
+
88
89
  # Monitoring control
89
90
  self._monitoring_enabled = False
90
91
  self._monitor_thread = None
91
-
92
+
92
93
  # Recreation callback management
93
94
  self._recreation_callbacks: list[Callable[[], None]] = []
94
-
95
+
95
96
  # Use provided exchange or create new one
96
97
  if initial_exchange:
97
98
  self._exchange = initial_exchange
@@ -105,23 +106,23 @@ class ExchangeManager(IDataArrivalListener):
105
106
  try:
106
107
  # Import here to avoid circular import (factory → broker → exchange_manager)
107
108
  from .factory import get_ccxt_exchange
108
-
109
+
109
110
  # Create raw exchange using factory logic
110
111
  ccxt_exchange = get_ccxt_exchange(**self._factory_params)
111
-
112
+
112
113
  # Setup exception handler for the new exchange
113
114
  self._setup_ccxt_exception_handler(ccxt_exchange)
114
-
115
+
115
116
  logger.debug(f"Created new {self._exchange_name} exchange instance")
116
117
  return ccxt_exchange
117
-
118
+
118
119
  except Exception as e:
119
120
  logger.error(f"Failed to create {self._exchange_name} exchange: {e}")
120
121
  raise RuntimeError(f"Failed to create {self._exchange_name} exchange: {e}") from e
121
-
122
+
122
123
  def register_recreation_callback(self, callback: Callable[[], None]) -> None:
123
124
  """Register callback to be called after successful exchange recreation.
124
-
125
+
125
126
  Args:
126
127
  callback: Function to call after successful recreation (no parameters)
127
128
  """
@@ -131,7 +132,7 @@ class ExchangeManager(IDataArrivalListener):
131
132
  def _call_recreation_callbacks(self) -> None:
132
133
  """Call all registered recreation callbacks after successful exchange recreation."""
133
134
  logger.debug(f"Calling {len(self._recreation_callbacks)} recreation callbacks for {self._exchange_name}")
134
-
135
+
135
136
  for callback in self._recreation_callbacks:
136
137
  try:
137
138
  callback()
@@ -142,66 +143,68 @@ class ExchangeManager(IDataArrivalListener):
142
143
  def force_recreation(self) -> bool:
143
144
  """
144
145
  Force recreation due to data stalls (called by BaseHealthMonitor).
145
-
146
+
146
147
  Returns:
147
148
  True if recreation successful, False if failed/limit exceeded
148
149
  """
149
150
  with self._recreation_lock:
150
151
  # Check recreation limit
151
152
  if self._recreation_count >= self._max_recreations:
152
- logger.error(f"Cannot recreate {self._exchange_name}: recreation limit ({self._max_recreations}) exceeded")
153
+ logger.error(
154
+ f"Cannot recreate {self._exchange_name}: recreation limit ({self._max_recreations}) exceeded"
155
+ )
153
156
  return False
154
-
157
+
155
158
  logger.info(f"Stall-triggered recreation for {self._exchange_name}")
156
159
  return self._recreate_exchange()
157
-
160
+
158
161
  def _recreate_exchange(self) -> bool:
159
162
  """Recreate the underlying exchange (must be called with _recreation_lock held)."""
160
163
  self._recreation_count += 1
161
- logger.warning(f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count}/{self._max_recreations})")
162
-
164
+ logger.warning(
165
+ f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count}/{self._max_recreations})"
166
+ )
167
+
163
168
  # Create new exchange
164
169
  try:
165
170
  new_exchange = self._create_exchange()
166
171
  except Exception as e:
167
172
  logger.error(f"Failed to recreate {self._exchange_name} exchange: {e}")
168
173
  return False
169
-
174
+
170
175
  # Atomically replace the exchange
171
176
  old_exchange = self._exchange
172
177
  self._exchange = new_exchange
173
-
178
+
174
179
  # Clean up old exchange
175
180
  try:
176
- if hasattr(old_exchange, 'close') and hasattr(old_exchange, 'asyncio_loop'):
177
- old_exchange.asyncio_loop.call_soon_threadsafe(
178
- lambda: asyncio.create_task(old_exchange.close())
179
- )
181
+ if hasattr(old_exchange, "close") and hasattr(old_exchange, "asyncio_loop"):
182
+ old_exchange.asyncio_loop.call_soon_threadsafe(lambda: asyncio.create_task(old_exchange.close()))
180
183
  except Exception as e:
181
184
  logger.warning(f"Error closing old {self._exchange_name} exchange: {e}")
182
-
185
+
183
186
  logger.info(f"Successfully recreated {self._exchange_name} exchange")
184
-
187
+
185
188
  # Call recreation callbacks after successful recreation
186
189
  self._call_recreation_callbacks()
187
-
190
+
188
191
  return True
189
-
192
+
190
193
  def reset_recreation_count_if_needed(self) -> None:
191
194
  """Reset recreation count periodically (called by monitoring loop)."""
192
195
  reset_interval_seconds = self._reset_interval_hours * SECONDS_PER_HOUR
193
-
196
+
194
197
  current_time = time.time()
195
198
  time_since_reset = current_time - self._last_successful_reset
196
-
199
+
197
200
  if time_since_reset >= reset_interval_seconds and self._recreation_count > 0:
198
201
  logger.info(f"Resetting recreation count for {self._exchange_name} (was {self._recreation_count})")
199
202
  self._recreation_count = 0
200
203
  self._last_successful_reset = current_time
201
-
204
+
202
205
  def on_data_arrival(self, event_type: str, event_time: dt_64) -> None:
203
206
  """Record data arrival for stall detection.
204
-
207
+
205
208
  Args:
206
209
  event_type: Type of data event (e.g., "ohlcv", "trade", "orderbook")
207
210
  event_time: Timestamp of the data event (unused for stall detection)
@@ -209,10 +212,10 @@ class ExchangeManager(IDataArrivalListener):
209
212
  current_timestamp = time.time()
210
213
  with self._data_lock:
211
214
  self._last_data_times[event_type] = current_timestamp
212
-
215
+
213
216
  def _extract_ohlc_timeframe(self, event_type: str) -> Optional[str]:
214
217
  """Extract timeframe from OHLC event type like 'ohlc(1m)' -> '1m'."""
215
- if event_type.startswith('ohlc(') and event_type.endswith(')'):
218
+ if event_type.startswith("ohlc(") and event_type.endswith(")"):
216
219
  return event_type[5:-1] # Simple slice: ohlc(1m) -> 1m
217
220
  return None
218
221
 
@@ -222,30 +225,30 @@ class ExchangeManager(IDataArrivalListener):
222
225
 
223
226
  def _get_stall_threshold(self, event_type: str) -> float:
224
227
  """Get stall threshold for specific event type.
225
-
228
+
226
229
  Extracts base data type from parameterized types like 'ohlc(1m)' -> 'ohlc'.
227
230
  """
228
231
  # Extract base data type (everything before first '(' if present)
229
- base_event_type = event_type.split('(')[0]
232
+ base_event_type = event_type.split("(")[0]
230
233
  return float(STALL_THRESHOLDS.get(base_event_type, DEFAULT_STALL_THRESHOLD_SECONDS))
231
-
234
+
232
235
  def start_monitoring(self) -> None:
233
236
  """Start background stall detection monitoring."""
234
237
  if self._monitoring_enabled:
235
238
  return
236
-
239
+
237
240
  self._monitoring_enabled = True
238
241
  self._monitor_thread = threading.Thread(target=self._stall_monitor_loop, daemon=True)
239
242
  self._monitor_thread.start()
240
243
  logger.debug(f"ExchangeManager: Started stall monitoring for {self._exchange_name}")
241
-
244
+
242
245
  def stop_monitoring(self) -> None:
243
246
  """Stop background stall detection monitoring."""
244
247
  self._monitoring_enabled = False
245
248
  if self._monitor_thread:
246
249
  self._monitor_thread = None
247
250
  logger.debug(f"ExchangeManager: Stopped stall monitoring for {self._exchange_name}")
248
-
251
+
249
252
  def _stall_monitor_loop(self) -> None:
250
253
  """Background thread that checks for data stalls and triggers self-recreation."""
251
254
  while self._monitoring_enabled:
@@ -256,26 +259,26 @@ class ExchangeManager(IDataArrivalListener):
256
259
  except Exception as e:
257
260
  logger.error(f"Error in ExchangeManager stall detection: {e}")
258
261
  time.sleep(self._check_interval)
259
-
262
+
260
263
  def _check_and_handle_stalls(self) -> None:
261
264
  """Check for stalls using custom thresholds per data type."""
262
265
  current_time = time.time()
263
266
  stalled_types = []
264
-
267
+
265
268
  with self._data_lock:
266
269
  for event_type, last_data_time in self._last_data_times.items():
267
270
  time_since_data = current_time - last_data_time
268
271
  threshold = self._get_stall_threshold(event_type)
269
-
272
+
270
273
  if time_since_data > threshold:
271
274
  stalled_types.append((event_type, time_since_data))
272
-
275
+
273
276
  if not stalled_types:
274
277
  return # No stalls detected
275
-
278
+
276
279
  stall_info = ", ".join([f"{event_type}({int(time_since)}s)" for event_type, time_since in stalled_types])
277
280
  logger.error(f"Data stalls detected in {self._exchange_name}: {stall_info}")
278
-
281
+
279
282
  try:
280
283
  logger.info(f"Self-triggering recreation for {self._exchange_name} due to stalls...")
281
284
  if self.force_recreation():
@@ -288,14 +291,14 @@ class ExchangeManager(IDataArrivalListener):
288
291
  logger.error(f"Stall-triggered recreation failed for {self._exchange_name}")
289
292
  except Exception as e:
290
293
  logger.error(f"Error during stall-triggered recreation: {e}")
291
-
294
+
292
295
  def _setup_ccxt_exception_handler(self, exchange: cxp.Exchange) -> None:
293
296
  """
294
297
  Set up global exception handler for the CCXT async loop to handle unretrieved futures.
295
298
 
296
299
  This prevents 'Future exception was never retrieved' warnings from CCXT's internal
297
300
  per-symbol futures that complete with UnsubscribeError during resubscription.
298
-
301
+
299
302
  Applied to every newly created exchange (initial and recreated).
300
303
  """
301
304
  asyncio_loop = exchange.asyncio_loop
@@ -324,15 +327,15 @@ class ExchangeManager(IDataArrivalListener):
324
327
  # Set the custom exception handler on the CCXT loop
325
328
  asyncio_loop.set_exception_handler(handle_ccxt_exception)
326
329
 
327
- # === Exchange Property Access ===
330
+ # === Exchange Property Access ===
328
331
  # Explicit property to access underlying CCXT exchange
329
-
332
+
330
333
  @property
331
334
  def exchange(self) -> cxp.Exchange:
332
335
  """Access to the underlying CCXT exchange instance.
333
-
336
+
334
337
  Use this property to call CCXT methods: exchange_manager.exchange.fetch_ticker(symbol)
335
-
338
+
336
339
  Returns:
337
340
  The current CCXT exchange instance (may change after recreation)
338
341
  """
@@ -9,7 +9,7 @@ from qubx.core.exceptions import BadRequest
9
9
  class HyperliquidCcxtBroker(CcxtBroker):
10
10
  """
11
11
  HyperLiquid-specific broker that handles market order slippage requirements.
12
-
12
+
13
13
  HyperLiquid requires a price even for market orders to calculate max slippage.
14
14
  This broker automatically calculates slippage-protected prices for market orders.
15
15
  """
@@ -17,7 +17,7 @@ class HyperliquidCcxtBroker(CcxtBroker):
17
17
  def __init__(
18
18
  self,
19
19
  *args,
20
- market_order_slippage: float = 0.05, # 5% default slippage
20
+ market_order_slippage: float = 0.01, # 5% default slippage
21
21
  **kwargs,
22
22
  ):
23
23
  super().__init__(*args, **kwargs)
@@ -38,21 +38,29 @@ class HyperliquidCcxtBroker(CcxtBroker):
38
38
  if order_type.lower() == "market" and price is None:
39
39
  quote = self.data_provider.get_quote(instrument)
40
40
  if quote is None:
41
- logger.warning(f"[<y>{instrument.symbol}</y>] :: Quote is not available for market order slippage calculation.")
42
- raise BadRequest(f"Quote is not available for market order slippage calculation for {instrument.symbol}")
41
+ logger.warning(
42
+ f"[<y>{instrument.symbol}</y>] :: Quote is not available for market order slippage calculation."
43
+ )
44
+ raise BadRequest(
45
+ f"Quote is not available for market order slippage calculation for {instrument.symbol}"
46
+ )
43
47
 
44
48
  # Get slippage from options or use default
45
49
  slippage = options.get("slippage", self.market_order_slippage)
46
-
50
+
47
51
  # Calculate slippage-protected price
48
52
  if order_side.upper() == "BUY":
49
53
  # For buy orders, add slippage to ask price to ensure execution
50
54
  price = quote.ask * (1 + slippage)
51
- logger.debug(f"[<y>{instrument.symbol}</y>] :: Market BUY order: using slippage-protected price {price:.6f} (ask: {quote.ask:.6f}, slippage: {slippage:.1%})")
55
+ logger.debug(
56
+ f"[<y>{instrument.symbol}</y>] :: Market BUY order: using slippage-protected price {price:.6f} (ask: {quote.ask:.6f}, slippage: {slippage:.1%})"
57
+ )
52
58
  else: # SELL
53
59
  # For sell orders, subtract slippage from bid price to ensure execution
54
60
  price = quote.bid * (1 - slippage)
55
- logger.debug(f"[<y>{instrument.symbol}</y>] :: Market SELL order: using slippage-protected price {price:.6f} (bid: {quote.bid:.6f}, slippage: {slippage:.1%})")
61
+ logger.debug(
62
+ f"[<y>{instrument.symbol}</y>] :: Market SELL order: using slippage-protected price {price:.6f} (bid: {quote.bid:.6f}, slippage: {slippage:.1%})"
63
+ )
56
64
 
57
65
  # Call parent implementation with calculated price
58
66
  payload = super()._prepare_order_payload(
@@ -64,6 +72,6 @@ class HyperliquidCcxtBroker(CcxtBroker):
64
72
  if "slippage" in options:
65
73
  # HyperLiquid accepts slippage as a percentage (e.g., 0.05 for 5%)
66
74
  params["px"] = price # Explicit price for slippage calculation
67
-
75
+
68
76
  payload["params"] = params
69
- return payload
77
+ return payload
@@ -65,7 +65,7 @@ class OrderBookDataHandler(BaseDataTypeHandler):
65
65
 
66
66
  # Notify all listeners
67
67
  self._data_provider.notify_data_arrival(sub_type, dt_64(ob.time, "ns"))
68
-
68
+
69
69
  channel.send((instrument, sub_type, ob, False))
70
70
  return True
71
71
 
@@ -150,7 +150,7 @@ class OrderBookDataHandler(BaseDataTypeHandler):
150
150
  ) -> SubscriptionConfiguration:
151
151
  """
152
152
  Prepare subscription configuration for individual instruments.
153
-
153
+
154
154
  Creates separate subscriber functions for each instrument to enable independent
155
155
  WebSocket streams without waiting for all instruments. This follows the same
156
156
  pattern as the OHLC handler for proper individual stream management.
@@ -169,10 +169,10 @@ class OrderBookDataHandler(BaseDataTypeHandler):
169
169
  try:
170
170
  # Watch orderbook for single instrument
171
171
  ccxt_ob = await self._exchange_manager.exchange.watch_order_book(symbol)
172
-
172
+
173
173
  # Use private processing method to avoid duplication
174
174
  self._process_orderbook(ccxt_ob, inst, sub_type, channel, depth, tick_size_pct)
175
-
175
+
176
176
  except Exception as e:
177
177
  logger.error(
178
178
  f"<yellow>{exchange_id}</yellow> Error in individual orderbook subscription for {inst.symbol}: {e}"
@@ -186,13 +186,15 @@ class OrderBookDataHandler(BaseDataTypeHandler):
186
186
  # Create individual unsubscriber if exchange supports it
187
187
  un_watch_method = getattr(self._exchange_manager.exchange, "un_watch_order_book", None)
188
188
  if un_watch_method is not None and callable(un_watch_method):
189
-
189
+
190
190
  def create_individual_unsubscriber(symbol=ccxt_symbol, exchange_id=self._exchange_id):
191
191
  async def individual_unsubscriber():
192
192
  try:
193
193
  await self._exchange_manager.exchange.un_watch_order_book(symbol)
194
194
  except Exception as e:
195
- logger.error(f"<yellow>{exchange_id}</yellow> Error unsubscribing orderbook for {symbol}: {e}")
195
+ logger.error(
196
+ f"<yellow>{exchange_id}</yellow> Error unsubscribing orderbook for {symbol}: {e}"
197
+ )
196
198
 
197
199
  return individual_unsubscriber
198
200