Qubx 0.6.73__tar.gz → 0.6.74__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.73 → qubx-0.6.74}/PKG-INFO +1 -1
  2. {qubx-0.6.73 → qubx-0.6.74}/pyproject.toml +1 -1
  3. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/broker.py +19 -12
  4. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/connection_manager.py +8 -2
  5. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/data.py +97 -53
  6. qubx-0.6.74/src/qubx/connectors/ccxt/exchange_manager.py +339 -0
  7. qubx-0.6.74/src/qubx/connectors/ccxt/factory.py +186 -0
  8. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/base.py +6 -5
  9. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/factory.py +7 -7
  10. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/funding_rate.py +8 -10
  11. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/liquidation.py +6 -6
  12. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/ohlc.py +14 -12
  13. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/open_interest.py +6 -4
  14. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/orderbook.py +10 -10
  15. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/quote.py +10 -7
  16. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/trade.py +8 -5
  17. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/subscription_orchestrator.py +12 -7
  18. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/warmup_service.py +9 -3
  19. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/tardis/data.py +1 -1
  20. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/context.py +7 -3
  21. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/interfaces.py +20 -3
  22. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/metrics.py +10 -3
  23. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/mixins/market.py +17 -3
  24. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/mixins/processing.py +5 -2
  25. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/mixins/subscription.py +13 -4
  26. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/mixins/trading.py +26 -13
  27. qubx-0.6.74/src/qubx/core/mixins/utils.py +4 -0
  28. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/series.pyx +1 -1
  29. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/emitters/indicator.py +5 -5
  30. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/exporters/formatters/incremental.py +4 -4
  31. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/health/base.py +26 -19
  32. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/pandaz/ta.py +1 -12
  33. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/ta/indicators.pxd +10 -0
  34. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/ta/indicators.pyi +8 -2
  35. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/ta/indicators.pyx +83 -6
  36. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/trackers/riskctrl.py +400 -1
  37. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/charting/lookinglass.py +11 -1
  38. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/runner/runner.py +7 -8
  39. qubx-0.6.73/src/qubx/connectors/ccxt/factory.py +0 -108
  40. {qubx-0.6.73 → qubx-0.6.74}/LICENSE +0 -0
  41. {qubx-0.6.73 → qubx-0.6.74}/README.md +0 -0
  42. {qubx-0.6.73 → qubx-0.6.74}/build.py +0 -0
  43. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/__init__.py +0 -0
  44. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/_nb_magic.py +0 -0
  45. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/__init__.py +0 -0
  46. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/account.py +0 -0
  47. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/broker.py +0 -0
  48. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/data.py +0 -0
  49. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/management.py +0 -0
  50. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/ome.py +0 -0
  51. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/optimization.py +0 -0
  52. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/runner.py +0 -0
  53. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/sentinels.py +0 -0
  54. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/simulated_data.py +0 -0
  55. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/simulated_exchange.py +0 -0
  56. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/simulator.py +0 -0
  57. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/backtester/utils.py +0 -0
  58. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/cli/__init__.py +0 -0
  59. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/cli/commands.py +0 -0
  60. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/cli/deploy.py +0 -0
  61. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/cli/misc.py +0 -0
  62. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/cli/release.py +0 -0
  63. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/cli/tui.py +0 -0
  64. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/__init__.py +0 -0
  65. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/account.py +0 -0
  66. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  67. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  68. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  69. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  70. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  71. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  72. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  73. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  74. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  75. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  76. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  77. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  78. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  79. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  80. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/reader.py +0 -0
  81. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  82. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  83. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/ccxt/utils.py +0 -0
  84. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/connectors/tardis/utils.py +0 -0
  85. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/__init__.py +0 -0
  86. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/account.py +0 -0
  87. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/basics.py +0 -0
  88. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/deque.py +0 -0
  89. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/errors.py +0 -0
  90. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/exceptions.py +0 -0
  91. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/helpers.py +0 -0
  92. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/initializer.py +0 -0
  93. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/loggers.py +0 -0
  94. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/lookups.py +0 -0
  95. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/mixins/__init__.py +0 -0
  96. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/mixins/universe.py +0 -0
  97. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/series.pxd +0 -0
  98. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/series.pyi +0 -0
  99. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/stale_data_detector.py +0 -0
  100. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/utils.pyi +0 -0
  101. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/core/utils.pyx +0 -0
  102. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/data/__init__.py +0 -0
  103. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/data/composite.py +0 -0
  104. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/data/helpers.py +0 -0
  105. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/data/hft.py +0 -0
  106. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/data/readers.py +0 -0
  107. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/data/registry.py +0 -0
  108. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/data/tardis.py +0 -0
  109. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/emitters/__init__.py +0 -0
  110. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/emitters/base.py +0 -0
  111. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/emitters/composite.py +0 -0
  112. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/emitters/csv.py +0 -0
  113. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/emitters/inmemory.py +0 -0
  114. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/emitters/prometheus.py +0 -0
  115. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/emitters/questdb.py +0 -0
  116. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/exporters/__init__.py +0 -0
  117. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/exporters/composite.py +0 -0
  118. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/exporters/formatters/__init__.py +0 -0
  119. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/exporters/formatters/base.py +0 -0
  120. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/exporters/formatters/slack.py +0 -0
  121. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/exporters/redis_streams.py +0 -0
  122. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/exporters/slack.py +0 -0
  123. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/features/__init__.py +0 -0
  124. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/features/core.py +0 -0
  125. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/features/orderbook.py +0 -0
  126. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/features/price.py +0 -0
  127. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/features/trades.py +0 -0
  128. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/features/utils.py +0 -0
  129. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/gathering/simplest.py +0 -0
  130. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/health/__init__.py +0 -0
  131. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/loggers/__init__.py +0 -0
  132. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/loggers/csv.py +0 -0
  133. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/loggers/factory.py +0 -0
  134. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/loggers/inmemory.py +0 -0
  135. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/loggers/mongo.py +0 -0
  136. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/math/__init__.py +0 -0
  137. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/math/stats.py +0 -0
  138. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/notifications/__init__.py +0 -0
  139. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/notifications/composite.py +0 -0
  140. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/notifications/slack.py +0 -0
  141. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/notifications/throttler.py +0 -0
  142. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/pandaz/__init__.py +0 -0
  143. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/pandaz/utils.py +0 -0
  144. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/_build.py +0 -0
  145. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/crypto-fees.ini +0 -0
  146. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  147. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  148. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  149. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  150. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  151. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  152. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  153. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  154. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  155. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  156. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  157. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restarts/__init__.py +0 -0
  158. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restarts/state_resolvers.py +0 -0
  159. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restarts/time_finders.py +0 -0
  160. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restorers/__init__.py +0 -0
  161. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restorers/balance.py +0 -0
  162. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restorers/factory.py +0 -0
  163. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restorers/interfaces.py +0 -0
  164. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restorers/position.py +0 -0
  165. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restorers/signal.py +0 -0
  166. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restorers/state.py +0 -0
  167. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/restorers/utils.py +0 -0
  168. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/ta/__init__.py +0 -0
  169. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/__init__.py +0 -0
  170. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/base.py +0 -0
  171. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  172. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/project/config.yml.j2 +0 -0
  173. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  174. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  175. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  176. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  177. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  178. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/project/template.yml +0 -0
  179. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  180. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  181. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/simple/config.yml.j2 +0 -0
  182. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  183. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  184. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  185. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/templates/simple/template.yml +0 -0
  186. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/trackers/__init__.py +0 -0
  187. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/trackers/advanced.py +0 -0
  188. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/trackers/composite.py +0 -0
  189. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/trackers/rebalancers.py +0 -0
  190. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/trackers/sizers.py +0 -0
  191. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/__init__.py +0 -0
  192. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/_pyxreloader.py +0 -0
  193. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  194. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/charting/orderbook.py +0 -0
  195. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/collections.py +0 -0
  196. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/marketdata/binance.py +0 -0
  197. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/marketdata/ccxt.py +0 -0
  198. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/marketdata/dukas.py +0 -0
  199. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/misc.py +0 -0
  200. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/ntp.py +0 -0
  201. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/numbers_utils.py +0 -0
  202. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/orderbook.py +0 -0
  203. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/plotting/__init__.py +0 -0
  204. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/plotting/dashboard.py +0 -0
  205. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/plotting/data.py +0 -0
  206. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/plotting/interfaces.py +0 -0
  207. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  208. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  209. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/questdb.py +0 -0
  210. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/runner/__init__.py +0 -0
  211. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  212. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/runner/accounts.py +0 -0
  213. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/runner/configs.py +0 -0
  214. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/runner/factory.py +0 -0
  215. {qubx-0.6.73 → qubx-0.6.74}/src/qubx/utils/time.py +0 -0
  216. {qubx-0.6.73 → qubx-0.6.74}/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.73
3
+ Version: 0.6.74
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.73"
7
+ version = "0.6.74"
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"
@@ -5,7 +5,7 @@ from typing import Any
5
5
  import pandas as pd
6
6
 
7
7
  import ccxt
8
- import ccxt.pro as cxp
8
+
9
9
  from ccxt.base.errors import ExchangeError
10
10
  from qubx import logger
11
11
  from qubx.core.basics import (
@@ -24,16 +24,16 @@ from qubx.core.interfaces import (
24
24
  )
25
25
  from qubx.utils.misc import AsyncThreadLoop
26
26
 
27
+ from .exchange_manager import ExchangeManager
27
28
  from .utils import ccxt_convert_order_info, instrument_to_ccxt_symbol
28
29
 
29
30
 
30
31
  class CcxtBroker(IBroker):
31
- _exchange: cxp.Exchange
32
- _loop: AsyncThreadLoop
32
+ _exchange_manager: ExchangeManager
33
33
 
34
34
  def __init__(
35
35
  self,
36
- exchange: cxp.Exchange,
36
+ exchange_manager: ExchangeManager,
37
37
  channel: CtrlChannel,
38
38
  time_provider: ITimeProvider,
39
39
  account: IAccountProcessor,
@@ -44,19 +44,24 @@ class CcxtBroker(IBroker):
44
44
  enable_create_order_ws: bool = False,
45
45
  enable_cancel_order_ws: bool = False,
46
46
  ):
47
- self._exchange = exchange
48
- self.ccxt_exchange_id = str(exchange.name)
47
+ self._exchange_manager = exchange_manager
48
+ self.ccxt_exchange_id = str(self._exchange_manager.exchange.name)
49
49
  self.channel = channel
50
50
  self.time_provider = time_provider
51
51
  self.account = account
52
52
  self.data_provider = data_provider
53
- self._loop = AsyncThreadLoop(exchange.asyncio_loop)
54
53
  self.cancel_timeout = cancel_timeout
55
54
  self.cancel_retry_interval = cancel_retry_interval
56
55
  self.max_cancel_retries = max_cancel_retries
57
56
  self.enable_create_order_ws = enable_create_order_ws
58
57
  self.enable_cancel_order_ws = enable_cancel_order_ws
59
58
 
59
+ @property
60
+ def _loop(self) -> AsyncThreadLoop:
61
+ """Get current AsyncThreadLoop for the exchange."""
62
+ return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
63
+
64
+
60
65
  @property
61
66
  def is_simulated_trading(self) -> bool:
62
67
  return False
@@ -255,9 +260,9 @@ class CcxtBroker(IBroker):
255
260
  instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
256
261
  )
257
262
  if self.enable_create_order_ws:
258
- r = await self._exchange.create_order_ws(**payload)
263
+ r = await self._exchange_manager.exchange.create_order_ws(**payload)
259
264
  else:
260
- r = await self._exchange.create_order(**payload)
265
+ r = await self._exchange_manager.exchange.create_order(**payload)
261
266
 
262
267
  if r is None:
263
268
  msg = "(::_create_order) No response from exchange"
@@ -294,12 +299,14 @@ class CcxtBroker(IBroker):
294
299
  raise BadRequest(f"Quote is not available for order creation for {instrument.symbol}")
295
300
 
296
301
  # TODO: think about automatically setting reduce only when needed
297
- if not options.get("reduceOnly", False):
302
+ if not (reduce_only := options.get("reduceOnly", False)):
298
303
  min_notional = instrument.min_notional
299
304
  if min_notional > 0 and abs(amount) * quote.mid_price() < min_notional:
300
305
  raise InvalidOrderParameters(
301
306
  f"[{instrument.symbol}] Order amount {amount} is too small. Minimum notional is {min_notional}"
302
307
  )
308
+ else:
309
+ params["reduceOnly"] = reduce_only
303
310
 
304
311
  # - handle trigger (stop) orders
305
312
  if _is_trigger_order:
@@ -357,9 +364,9 @@ class CcxtBroker(IBroker):
357
364
  while True:
358
365
  try:
359
366
  if self.enable_cancel_order_ws:
360
- await self._exchange.cancel_order_ws(order_id, symbol=instrument_to_ccxt_symbol(instrument))
367
+ await self._exchange_manager.exchange.cancel_order_ws(order_id, symbol=instrument_to_ccxt_symbol(instrument))
361
368
  else:
362
- await self._exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(instrument))
369
+ await self._exchange_manager.exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(instrument))
363
370
  return True
364
371
  except ccxt.OperationRejected as err:
365
372
  err_msg = str(err).lower()
@@ -19,6 +19,7 @@ from qubx.core.basics import CtrlChannel
19
19
  from qubx.utils.misc import AsyncThreadLoop
20
20
 
21
21
  from .exceptions import CcxtSymbolNotRecognized
22
+ from .exchange_manager import ExchangeManager
22
23
  from .subscription_manager import SubscriptionManager
23
24
 
24
25
 
@@ -36,13 +37,13 @@ class ConnectionManager:
36
37
  def __init__(
37
38
  self,
38
39
  exchange_id: str,
39
- loop: AsyncThreadLoop,
40
+ exchange_manager: ExchangeManager,
40
41
  max_ws_retries: int = 10,
41
42
  subscription_manager: SubscriptionManager | None = None,
42
43
  cleanup_timeout: float = 3.0,
43
44
  ):
44
45
  self._exchange_id = exchange_id
45
- self._loop = loop
46
+ self._exchange_manager = exchange_manager
46
47
  self.max_ws_retries = max_ws_retries
47
48
  self._subscription_manager = subscription_manager
48
49
  self._cleanup_timeout = cleanup_timeout
@@ -54,6 +55,11 @@ class ConnectionManager:
54
55
  # Connection tracking
55
56
  self._stream_to_coro: dict[str, concurrent.futures.Future] = {}
56
57
 
58
+ @property
59
+ def _loop(self) -> AsyncThreadLoop:
60
+ """Get current AsyncThreadLoop from exchange manager."""
61
+ return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
62
+
57
63
  def set_subscription_manager(self, subscription_manager: SubscriptionManager) -> None:
58
64
  """Set the subscription manager for state coordination."""
59
65
  self._subscription_manager = subscription_manager
@@ -4,19 +4,17 @@ from typing import Dict, List, Optional, Set, Tuple
4
4
 
5
5
  import pandas as pd
6
6
 
7
- import ccxt.pro as cxp
8
-
9
7
  # CCXT exceptions are now handled in ConnectionManager
10
- from ccxt.pro import Exchange
11
8
  from qubx import logger
12
- from qubx.core.basics import CtrlChannel, DataType, Instrument, ITimeProvider
9
+ from qubx.core.basics import CtrlChannel, DataType, Instrument, ITimeProvider, dt_64
13
10
  from qubx.core.helpers import BasicScheduler
14
- from qubx.core.interfaces import IDataProvider, IHealthMonitor
11
+ from qubx.core.interfaces import IDataArrivalListener, IDataProvider, IHealthMonitor
15
12
  from qubx.core.series import Bar, Quote
16
13
  from qubx.health import DummyHealthMonitor
17
14
  from qubx.utils.misc import AsyncThreadLoop
18
15
 
19
16
  from .connection_manager import ConnectionManager
17
+ from .exchange_manager import ExchangeManager
20
18
  from .handlers import DataTypeHandlerFactory
21
19
  from .handlers.ohlc import OhlcDataHandler
22
20
  from .subscription_config import SubscriptionConfiguration
@@ -27,42 +25,46 @@ from .warmup_service import WarmupService
27
25
 
28
26
  class CcxtDataProvider(IDataProvider):
29
27
  time_provider: ITimeProvider
30
- _exchange: Exchange
28
+ _exchange_manager: ExchangeManager
31
29
  _scheduler: BasicScheduler | None = None
32
30
 
33
31
  # Core state - still needed
34
32
  _last_quotes: dict[Instrument, Optional[Quote]]
35
- _loop: AsyncThreadLoop
36
33
  _warmup_timeout: int
37
34
 
38
35
  def __init__(
39
36
  self,
40
- exchange: cxp.Exchange,
37
+ exchange_manager: ExchangeManager,
41
38
  time_provider: ITimeProvider,
42
39
  channel: CtrlChannel,
43
40
  max_ws_retries: int = 10,
44
41
  warmup_timeout: int = 120,
45
42
  health_monitor: IHealthMonitor | None = None,
46
43
  ):
47
- self._exchange_id = str(exchange.name)
44
+ # Store the exchange manager (always ExchangeManager now)
45
+ self._exchange_manager = exchange_manager
46
+
48
47
  self.time_provider = time_provider
49
48
  self.channel = channel
50
49
  self.max_ws_retries = max_ws_retries
51
50
  self._warmup_timeout = warmup_timeout
52
51
  self._health_monitor = health_monitor or DummyHealthMonitor()
53
52
 
54
- # Core components
55
- self._exchange = exchange
56
- self._loop = AsyncThreadLoop(self._exchange.asyncio_loop)
53
+ self._data_arrival_listeners: List[IDataArrivalListener] = [
54
+ self._health_monitor,
55
+ self._exchange_manager
56
+ ]
57
+
58
+ logger.debug(f"Registered {len(self._data_arrival_listeners)} data arrival listeners")
57
59
 
58
- # Set up global exception handler for unretrieved CCXT futures
59
- self._setup_ccxt_exception_handler()
60
+ # Core components - access exchange directly via exchange_manager.exchange
61
+ self._exchange_id = str(self._exchange_manager.exchange.name)
60
62
 
61
63
  # Initialize composed components
62
64
  self._subscription_manager = SubscriptionManager()
63
65
  self._connection_manager = ConnectionManager(
64
66
  exchange_id=self._exchange_id,
65
- loop=self._loop,
67
+ exchange_manager=self._exchange_manager,
66
68
  max_ws_retries=max_ws_retries,
67
69
  subscription_manager=self._subscription_manager,
68
70
  )
@@ -70,13 +72,13 @@ class CcxtDataProvider(IDataProvider):
70
72
  exchange_id=self._exchange_id,
71
73
  subscription_manager=self._subscription_manager,
72
74
  connection_manager=self._connection_manager,
73
- loop=self._loop,
75
+ exchange_manager=self._exchange_manager,
74
76
  )
75
77
 
76
78
  # Data type handler factory for clean separation of data processing logic
77
79
  self._data_type_handler_factory = DataTypeHandlerFactory(
78
80
  data_provider=self,
79
- exchange=self._exchange,
81
+ exchange_manager=self._exchange_manager,
80
82
  exchange_id=self._exchange_id,
81
83
  )
82
84
 
@@ -85,47 +87,42 @@ class CcxtDataProvider(IDataProvider):
85
87
  handler_factory=self._data_type_handler_factory,
86
88
  channel=channel,
87
89
  exchange_id=self._exchange_id,
88
- async_loop=self._loop,
90
+ exchange_manager=self._exchange_manager,
89
91
  warmup_timeout=warmup_timeout,
90
92
  )
91
93
 
92
94
  # Quote caching for synthetic quote generation
93
95
  self._last_quotes = defaultdict(lambda: None)
94
96
 
95
- logger.info(f"<yellow>{self._exchange_id}</yellow> Initialized")
97
+ # Start ExchangeManager monitoring
98
+ self._exchange_manager.start_monitoring()
96
99
 
97
- def _setup_ccxt_exception_handler(self) -> None:
98
- """
99
- Set up global exception handler for the CCXT async loop to handle unretrieved futures.
100
+ # Register recreation callback for automatic resubscription
101
+ self._exchange_manager.register_recreation_callback(self._handle_exchange_recreation)
100
102
 
101
- This prevents 'Future exception was never retrieved' warnings from CCXT's internal
102
- per-symbol futures that complete with UnsubscribeError during resubscription.
103
- """
104
- asyncio_loop = self._exchange.asyncio_loop
103
+ logger.info(f"<yellow>{self._exchange_id}</yellow> Initialized")
105
104
 
106
- def handle_ccxt_exception(loop, context):
107
- """Handle unretrieved exceptions from CCXT futures."""
108
- exception = context.get("exception")
105
+ @property
106
+ def _loop(self) -> AsyncThreadLoop:
107
+ """Get current AsyncThreadLoop for the exchange."""
108
+ return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
109
+
110
+ def notify_data_arrival(self, event_type: str, event_time: dt_64) -> None:
111
+ """Notify all registered listeners about data arrival.
112
+
113
+ Args:
114
+ event_type: Type of data event (e.g., "ohlcv:BTC/USDT:1m")
115
+ event_time: Timestamp of the data event
116
+ """
117
+ for listener in self._data_arrival_listeners:
118
+ try:
119
+ listener.on_data_arrival(event_type, event_time)
120
+ except Exception as e:
121
+ logger.error(f"Error notifying data arrival listener {type(listener).__name__}: {e}")
109
122
 
110
- # Handle expected CCXT UnsubscribeError during resubscription
111
- if exception and "UnsubscribeError" in str(type(exception)):
112
- return
113
123
 
114
- # Handle other CCXT-related exceptions quietly if they're in our exchange context
115
- if exception and any(
116
- keyword in str(exception) for keyword in [self._exchange.id, "ohlcv", "orderbook", "ticker"]
117
- ):
118
- return
119
124
 
120
- # For all other exceptions, use the default handler
121
- if hasattr(loop, "default_exception_handler"):
122
- loop.default_exception_handler(context)
123
- else:
124
- # Fallback logging if no default handler
125
- logger.warning(f"Unhandled asyncio exception: {context}")
126
125
 
127
- # Set the custom exception handler on the CCXT loop
128
- asyncio_loop.set_exception_handler(handle_ccxt_exception)
129
126
 
130
127
  @property
131
128
  def is_simulation(self) -> bool:
@@ -155,7 +152,7 @@ class CcxtDataProvider(IDataProvider):
155
152
  subscription_type=subscription_type,
156
153
  instruments=_updated_instruments,
157
154
  handler=handler,
158
- exchange=self._exchange,
155
+ exchange=self._exchange_manager.exchange,
159
156
  channel=self.channel,
160
157
  **_params,
161
158
  )
@@ -196,7 +193,7 @@ class CcxtDataProvider(IDataProvider):
196
193
  subscription_type=subscription_type,
197
194
  instruments=remaining_instruments,
198
195
  handler=handler,
199
- exchange=self._exchange,
196
+ exchange=self._exchange_manager.exchange,
200
197
  channel=self.channel,
201
198
  **_params,
202
199
  )
@@ -262,19 +259,66 @@ class CcxtDataProvider(IDataProvider):
262
259
  except Exception as e:
263
260
  logger.error(f"Error stopping subscription {subscription_type}: {e}")
264
261
 
262
+ # Stop ExchangeManager monitoring
263
+ self._exchange_manager.stop_monitoring()
264
+
265
265
  # Close exchange connection
266
- if hasattr(self._exchange, "close"):
267
- future = self._loop.submit(self._exchange.close()) # type: ignore
266
+ if hasattr(self._exchange_manager.exchange, "close"):
267
+ future = self._loop.submit(self._exchange_manager.exchange.close()) # type: ignore
268
268
  # Wait for 5 seconds for connection to close
269
269
  future.result(5)
270
270
  else:
271
- del self._exchange
271
+ del self._exchange_manager
272
272
 
273
273
  # Note: AsyncThreadLoop stop is handled by its own lifecycle
274
274
 
275
275
  except Exception as e:
276
276
  logger.error(f"Error during close: {e}")
277
277
 
278
+ def _handle_exchange_recreation(self) -> None:
279
+ """Handle exchange recreation by resubscribing to all active subscriptions."""
280
+ logger.info(f"<yellow>{self._exchange_id}</yellow> Handling exchange recreation - resubscribing to active subscriptions")
281
+
282
+ # Get snapshot of current subscriptions before cleanup
283
+ active_subscriptions = self._subscription_manager.get_subscriptions()
284
+
285
+ resubscription_data = []
286
+ for subscription_type in active_subscriptions:
287
+ instruments = self._subscription_manager.get_subscribed_instruments(subscription_type)
288
+ if instruments:
289
+ resubscription_data.append((subscription_type, instruments))
290
+
291
+ logger.info(f"<yellow>{self._exchange_id}</yellow> Found {len(resubscription_data)} active subscriptions to recreate")
292
+
293
+ # Track success/failure counts for reporting
294
+ successful_resubscriptions = 0
295
+ failed_resubscriptions = 0
296
+
297
+ # Clean resubscription: unsubscribe then subscribe for each subscription type
298
+ for subscription_type, instruments in resubscription_data:
299
+ try:
300
+ logger.info(f"<yellow>{self._exchange_id}</yellow> Resubscribing to {subscription_type} with {len(instruments)} instruments")
301
+
302
+ self.unsubscribe(subscription_type, instruments)
303
+
304
+ # Resubscribe with reset=True to ensure clean state
305
+ self.subscribe(subscription_type, instruments, reset=True)
306
+
307
+ successful_resubscriptions += 1
308
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Successfully resubscribed to {subscription_type}")
309
+
310
+ except Exception as e:
311
+ failed_resubscriptions += 1
312
+ logger.error(f"<yellow>{self._exchange_id}</yellow> Failed to resubscribe to {subscription_type}: {e}")
313
+ # Continue with other subscriptions even if one fails
314
+
315
+ # Report final status
316
+ total_subscriptions = len(resubscription_data)
317
+ if failed_resubscriptions == 0:
318
+ logger.info(f"<yellow>{self._exchange_id}</yellow> Exchange recreation resubscription completed successfully ({total_subscriptions}/{total_subscriptions})")
319
+ else:
320
+ logger.warning(f"<yellow>{self._exchange_id}</yellow> Exchange recreation resubscription completed with errors ({successful_resubscriptions}/{total_subscriptions} successful)")
321
+
278
322
  @property
279
323
  def subscribed_instruments(self) -> Set[Instrument]:
280
324
  """Get all subscribed instruments (delegated to subscription manager)."""
@@ -282,7 +326,7 @@ class CcxtDataProvider(IDataProvider):
282
326
 
283
327
  @property
284
328
  def is_read_only(self) -> bool:
285
- _key = self._exchange.apiKey
329
+ _key = self._exchange_manager.exchange.apiKey
286
330
  return _key is None or _key == ""
287
331
 
288
332
  def _time_msec_nbars_back(self, timeframe: str, nbarsback: int = 1) -> int:
@@ -293,9 +337,9 @@ class CcxtDataProvider(IDataProvider):
293
337
  _t = re.match(r"(\d+)(\w+)", timeframe)
294
338
  timeframe = f"{_t[1]}{_t[2][0].lower()}" if _t and len(_t.groups()) > 1 else timeframe
295
339
 
296
- tframe = self._exchange.find_timeframe(timeframe)
340
+ tframe = self._exchange_manager.exchange.find_timeframe(timeframe)
297
341
  if tframe is None:
298
- raise ValueError(f"timeframe {timeframe} is not supported by {self._exchange.name}")
342
+ raise ValueError(f"timeframe {timeframe} is not supported by {self._exchange_manager.exchange.name}")
299
343
 
300
344
  return tframe
301
345