Qubx 0.6.89__tar.gz → 0.6.91__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 (235) hide show
  1. {qubx-0.6.89 → qubx-0.6.91}/PKG-INFO +1 -1
  2. {qubx-0.6.89 → qubx-0.6.91}/pyproject.toml +1 -1
  3. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/account.py +1 -1
  4. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/broker.py +52 -6
  5. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/cli/commands.py +13 -1
  6. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/account.py +3 -2
  7. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/broker.py +166 -19
  8. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/connection_manager.py +14 -0
  9. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchange_manager.py +6 -37
  10. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchanges/__init__.py +5 -2
  11. qubx-0.6.91/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +7 -0
  12. qubx-0.6.91/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +75 -0
  13. qubx-0.6.91/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +306 -0
  14. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +83 -0
  15. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/factory.py +1 -9
  16. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/base.py +2 -4
  17. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/factory.py +4 -5
  18. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/utils.py +8 -2
  19. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/account.py +54 -5
  20. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/basics.py +62 -2
  21. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/context.py +14 -4
  22. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/helpers.py +71 -4
  23. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/interfaces.py +55 -9
  24. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/mixins/processing.py +6 -1
  25. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/mixins/trading.py +63 -3
  26. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/data/__init__.py +10 -0
  27. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/data/composite.py +87 -60
  28. qubx-0.6.91/src/qubx/data/containers.py +234 -0
  29. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/data/readers.py +3 -0
  30. qubx-0.6.91/src/qubx/data/registry.py +242 -0
  31. qubx-0.6.91/src/qubx/data/storage.py +74 -0
  32. qubx-0.6.91/src/qubx/data/storages/csv.py +273 -0
  33. qubx-0.6.91/src/qubx/data/storages/questdb.py +554 -0
  34. qubx-0.6.91/src/qubx/data/storages/utils.py +115 -0
  35. qubx-0.6.91/src/qubx/data/transformers.py +491 -0
  36. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/formatters/target_position.py +2 -6
  37. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/redis_streams.py +3 -3
  38. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/loggers/csv.py +4 -4
  39. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/marketdata/ccxt.py +38 -6
  40. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/questdb.py +6 -7
  41. qubx-0.6.91/src/qubx/utils/runner/textual/__init__.py +60 -0
  42. qubx-0.6.91/src/qubx/utils/runner/textual/app.py +149 -0
  43. qubx-0.6.91/src/qubx/utils/runner/textual/handlers.py +72 -0
  44. qubx-0.6.91/src/qubx/utils/runner/textual/init_code.py +143 -0
  45. qubx-0.6.91/src/qubx/utils/runner/textual/kernel.py +110 -0
  46. qubx-0.6.91/src/qubx/utils/runner/textual/styles.tcss +100 -0
  47. qubx-0.6.91/src/qubx/utils/runner/textual/widgets/__init__.py +6 -0
  48. qubx-0.6.91/src/qubx/utils/runner/textual/widgets/positions_table.py +89 -0
  49. qubx-0.6.91/src/qubx/utils/runner/textual/widgets/repl_output.py +14 -0
  50. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/time.py +7 -0
  51. qubx-0.6.89/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -6
  52. qubx-0.6.89/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -77
  53. qubx-0.6.89/src/qubx/data/registry.py +0 -124
  54. {qubx-0.6.89 → qubx-0.6.91}/LICENSE +0 -0
  55. {qubx-0.6.89 → qubx-0.6.91}/README.md +0 -0
  56. {qubx-0.6.89 → qubx-0.6.91}/build.py +0 -0
  57. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/__init__.py +0 -0
  58. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/_nb_magic.py +0 -0
  59. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/__init__.py +0 -0
  60. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/data.py +0 -0
  61. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/management.py +0 -0
  62. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/ome.py +0 -0
  63. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/optimization.py +0 -0
  64. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/runner.py +0 -0
  65. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/sentinels.py +0 -0
  66. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/simulated_data.py +0 -0
  67. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/simulated_exchange.py +0 -0
  68. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/simulator.py +0 -0
  69. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/backtester/utils.py +0 -0
  70. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/cli/__init__.py +0 -0
  71. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/cli/deploy.py +0 -0
  72. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/cli/misc.py +0 -0
  73. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/cli/release.py +0 -0
  74. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/cli/tui.py +0 -0
  75. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/__init__.py +0 -0
  76. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  77. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  78. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/data.py +0 -0
  79. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  80. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  81. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  82. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  83. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  84. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  85. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  86. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  87. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  88. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  89. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  90. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  91. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  92. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  93. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  94. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/reader.py +0 -0
  95. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  96. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  97. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  98. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  99. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/tardis/data.py +0 -0
  100. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/connectors/tardis/utils.py +0 -0
  101. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/__init__.py +0 -0
  102. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/deque.py +0 -0
  103. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/errors.py +0 -0
  104. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/exceptions.py +0 -0
  105. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/initializer.py +0 -0
  106. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/loggers.py +0 -0
  107. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/lookups.py +0 -0
  108. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/metrics.py +0 -0
  109. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/mixins/__init__.py +0 -0
  110. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/mixins/market.py +0 -0
  111. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/mixins/subscription.py +0 -0
  112. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/mixins/universe.py +0 -0
  113. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/mixins/utils.py +0 -0
  114. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/series.pxd +0 -0
  115. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/series.pyi +0 -0
  116. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/series.pyx +0 -0
  117. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/stale_data_detector.py +0 -0
  118. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/utils.pyi +0 -0
  119. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/core/utils.pyx +0 -0
  120. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/data/helpers.py +0 -0
  121. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/data/hft.py +0 -0
  122. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/data/tardis.py +0 -0
  123. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/emitters/__init__.py +0 -0
  124. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/emitters/base.py +0 -0
  125. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/emitters/composite.py +0 -0
  126. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/emitters/csv.py +0 -0
  127. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/emitters/indicator.py +0 -0
  128. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/emitters/inmemory.py +0 -0
  129. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/emitters/prometheus.py +0 -0
  130. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/emitters/questdb.py +0 -0
  131. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/__init__.py +0 -0
  132. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/composite.py +0 -0
  133. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/formatters/__init__.py +0 -0
  134. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/formatters/base.py +0 -0
  135. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/formatters/incremental.py +0 -0
  136. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/formatters/slack.py +0 -0
  137. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/exporters/slack.py +0 -0
  138. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/features/__init__.py +0 -0
  139. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/features/core.py +0 -0
  140. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/features/orderbook.py +0 -0
  141. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/features/price.py +0 -0
  142. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/features/trades.py +0 -0
  143. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/features/utils.py +0 -0
  144. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/gathering/simplest.py +0 -0
  145. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/health/__init__.py +0 -0
  146. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/health/base.py +0 -0
  147. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/loggers/__init__.py +0 -0
  148. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/loggers/factory.py +0 -0
  149. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/loggers/inmemory.py +0 -0
  150. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/loggers/mongo.py +0 -0
  151. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/math/__init__.py +0 -0
  152. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/math/stats.py +0 -0
  153. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/notifications/__init__.py +0 -0
  154. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/notifications/composite.py +0 -0
  155. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/notifications/slack.py +0 -0
  156. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/notifications/throttler.py +0 -0
  157. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/pandaz/__init__.py +0 -0
  158. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/pandaz/ta.py +0 -0
  159. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/pandaz/utils.py +0 -0
  160. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/_build.py +0 -0
  161. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/crypto-fees.ini +0 -0
  162. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  163. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  164. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  165. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  166. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  167. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  168. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  169. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  170. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  171. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  172. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  173. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restarts/__init__.py +0 -0
  174. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restarts/state_resolvers.py +0 -0
  175. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restarts/time_finders.py +0 -0
  176. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restorers/__init__.py +0 -0
  177. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restorers/balance.py +0 -0
  178. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restorers/factory.py +0 -0
  179. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restorers/interfaces.py +0 -0
  180. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restorers/position.py +0 -0
  181. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restorers/signal.py +0 -0
  182. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restorers/state.py +0 -0
  183. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/restorers/utils.py +0 -0
  184. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/ta/__init__.py +0 -0
  185. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/ta/indicators.pxd +0 -0
  186. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/ta/indicators.pyi +0 -0
  187. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/ta/indicators.pyx +0 -0
  188. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/__init__.py +0 -0
  189. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/base.py +0 -0
  190. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  191. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/project/config.yml.j2 +0 -0
  192. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  193. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  194. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  195. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  196. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  197. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/project/template.yml +0 -0
  198. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  199. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  200. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/simple/config.yml.j2 +0 -0
  201. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  202. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  203. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  204. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/templates/simple/template.yml +0 -0
  205. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/trackers/__init__.py +0 -0
  206. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/trackers/advanced.py +0 -0
  207. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/trackers/composite.py +0 -0
  208. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/trackers/rebalancers.py +0 -0
  209. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/trackers/riskctrl.py +0 -0
  210. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/trackers/sizers.py +0 -0
  211. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/__init__.py +0 -0
  212. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/_pyxreloader.py +0 -0
  213. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/charting/lookinglass.py +0 -0
  214. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  215. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/charting/orderbook.py +0 -0
  216. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/collections.py +0 -0
  217. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/marketdata/binance.py +0 -0
  218. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/marketdata/dukas.py +0 -0
  219. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/misc.py +0 -0
  220. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/ntp.py +0 -0
  221. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/numbers_utils.py +0 -0
  222. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/orderbook.py +0 -0
  223. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/plotting/__init__.py +0 -0
  224. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/plotting/dashboard.py +0 -0
  225. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/plotting/data.py +0 -0
  226. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/plotting/interfaces.py +0 -0
  227. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  228. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  229. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/runner/__init__.py +0 -0
  230. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  231. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/runner/accounts.py +0 -0
  232. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/runner/configs.py +0 -0
  233. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/runner/factory.py +0 -0
  234. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/runner/runner.py +0 -0
  235. {qubx-0.6.89 → qubx-0.6.91}/src/qubx/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.89
3
+ Version: 0.6.91
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  License-File: LICENSE
6
6
  Author: Dmitry Marienko
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.89"
7
+ version = "0.6.91"
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"
@@ -42,7 +42,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
42
42
  _pos = self.get_position(instrument)
43
43
  _pos.reset_by_position(position)
44
44
 
45
- def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
45
+ def get_orders(self, instrument: Instrument | None = None, exchange: str | None = None) -> dict[str, Order]:
46
46
  return self._exchange.get_open_orders(instrument)
47
47
 
48
48
  def get_position(self, instrument: Instrument) -> Position:
@@ -6,7 +6,7 @@ from qubx.core.basics import (
6
6
  Instrument,
7
7
  Order,
8
8
  )
9
- from qubx.core.exceptions import OrderNotFound
9
+ from qubx.core.exceptions import BadRequest, OrderNotFound
10
10
  from qubx.core.interfaces import IBroker
11
11
 
12
12
  from .account import SimulatedAccountProcessor
@@ -64,20 +64,66 @@ class SimulatedBroker(IBroker):
64
64
  ) -> None:
65
65
  self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
66
66
 
67
- def cancel_order(self, order_id: str) -> Order | None:
67
+ def cancel_order(self, order_id: str) -> bool:
68
+ """Cancel an order synchronously and return success status."""
68
69
  try:
69
70
  self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
70
- return order_update.order if order_update is not None else None
71
+ return order_update is not None
71
72
  except OrderNotFound:
72
73
  # Order was already cancelled or doesn't exist
73
74
  logger.debug(f"Order {order_id} not found")
74
- return None
75
+ return False
76
+
77
+ def cancel_order_async(self, order_id: str) -> None:
78
+ """Cancel an order asynchronously (fire-and-forget)."""
79
+ # For simulation, async is same as sync since it's fast
80
+ self.cancel_order(order_id)
75
81
 
76
82
  def cancel_orders(self, instrument: Instrument) -> None:
77
83
  raise NotImplementedError("Not implemented yet")
78
84
 
79
- def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
80
- raise NotImplementedError("Not implemented yet")
85
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
86
+ """Update an existing limit order using cancel+recreate strategy.
87
+
88
+ Args:
89
+ order_id: The ID of the order to update
90
+ price: New price for the order
91
+ amount: New amount for the order
92
+
93
+ Returns:
94
+ Order: The updated (newly created) order object
95
+
96
+ Raises:
97
+ OrderNotFound: If the order is not found
98
+ BadRequest: If the order is not a limit order
99
+ """
100
+ # Get the existing order from account
101
+ active_orders = self._account.get_orders()
102
+ existing_order = active_orders.get(order_id)
103
+ if not existing_order:
104
+ raise OrderNotFound(f"Order {order_id} not found")
105
+
106
+ # Validate that it's a limit order
107
+ if existing_order.type.lower() != "limit":
108
+ raise BadRequest(
109
+ f"Order {order_id} is not a limit order (type: {existing_order.type}). Only limit orders can be updated."
110
+ )
111
+
112
+ # Cancel the existing order first
113
+ self.cancel_order(order_id)
114
+
115
+ # Create a new order with updated parameters, preserving original properties
116
+ updated_order = self.send_order(
117
+ instrument=existing_order.instrument,
118
+ order_side=existing_order.side,
119
+ order_type="limit",
120
+ amount=abs(amount),
121
+ price=price,
122
+ client_id=existing_order.client_id, # Preserve original client_id for tracking
123
+ time_in_force=existing_order.time_in_force or "gtc",
124
+ )
125
+
126
+ return updated_order
81
127
 
82
128
  def _send_execution_report(self, report: SimulatedExecutionReport | None):
83
129
  if report is None:
@@ -69,11 +69,14 @@ def main(debug: bool, debug_port: int, log_level: str):
69
69
  @click.option(
70
70
  "--jupyter", "-j", is_flag=True, default=False, help="Run strategy in jupyter console.", show_default=True
71
71
  )
72
+ @click.option(
73
+ "--textual", "-t", is_flag=True, default=False, help="Run strategy in textual TUI.", show_default=True
74
+ )
72
75
  @click.option(
73
76
  "--restore", "-r", is_flag=True, default=False, help="Restore strategy state from previous run.", show_default=True
74
77
  )
75
78
  @click.option("--no-color", is_flag=True, default=False, help="Disable colored logging output.", show_default=True)
76
- def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool, restore: bool, no_color: bool):
79
+ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool, textual: bool, restore: bool, no_color: bool):
77
80
  """
78
81
  Starts the strategy with the given configuration file. If paper mode is enabled, account is not required.
79
82
 
@@ -84,12 +87,21 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
84
87
  """
85
88
  from qubx.utils.misc import add_project_to_system_path, logo
86
89
  from qubx.utils.runner.runner import run_strategy_yaml, run_strategy_yaml_in_jupyter
90
+ from qubx.utils.runner.textual import run_strategy_yaml_in_textual
91
+
92
+ # Ensure jupyter and textual are mutually exclusive
93
+ if jupyter and textual:
94
+ click.echo("Error: --jupyter and --textual cannot be used together.", err=True)
95
+ raise click.Abort()
87
96
 
88
97
  add_project_to_system_path()
89
98
  add_project_to_system_path(str(config_file.parent.parent))
90
99
  add_project_to_system_path(str(config_file.parent))
100
+
91
101
  if jupyter:
92
102
  run_strategy_yaml_in_jupyter(config_file, account_file, paper, restore)
103
+ elif textual:
104
+ run_strategy_yaml_in_textual(config_file, account_file, paper, restore)
93
105
  else:
94
106
  logo()
95
107
  run_strategy_yaml(config_file, account_file, paper=paper, restore=restore, blocking=True, no_color=no_color)
@@ -337,7 +337,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
337
337
  current_pos.change_position_by(timestamp, quantity_diff, _current_price)
338
338
 
339
339
  def _get_start_time_in_ms(self, days_before: int) -> int:
340
- return (self.time_provider.time() - days_before * pd.Timedelta("1d")).asm8.item() // 1000000
340
+ return (self.time_provider.time() - days_before * pd.Timedelta("1d")).asm8.item() // 1000000 # type: ignore
341
341
 
342
342
  def _is_our_order(self, order: Order) -> bool:
343
343
  if order.client_id is None:
@@ -365,7 +365,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
365
365
  _fetch_instruments: list[Instrument] = []
366
366
  for instr in instruments:
367
367
  _dt, _ = self._instrument_to_last_price.get(instr, (None, None))
368
- if _dt is None or pd.Timedelta(_current_time - _dt) > pd.Timedelta(self.balance_interval):
368
+ if _dt is None or pd.Timedelta(_current_time - _dt) > pd.Timedelta(self.balance_interval): # type: ignore
369
369
  _fetch_instruments.append(instr)
370
370
 
371
371
  _symbol_to_instrument = {instr.symbol: instr for instr in instruments}
@@ -506,6 +506,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
506
506
  deals: list[Deal] = [ccxt_convert_deal_info(o) for o in deals_data]
507
507
  return sorted(deals, key=lambda x: x.time) if deals else []
508
508
 
509
+ # TODO: this should take the exchange manager instead of cxp.Exchange
509
510
  async def _listen_to_stream(
510
511
  self,
511
512
  subscriber: Callable[[], Awaitable[None]],
@@ -5,7 +5,6 @@ from typing import Any
5
5
  import pandas as pd
6
6
 
7
7
  import ccxt
8
-
9
8
  from ccxt.base.errors import ExchangeError
10
9
  from qubx import logger
11
10
  from qubx.core.basics import (
@@ -15,7 +14,7 @@ from qubx.core.basics import (
15
14
  OrderSide,
16
15
  )
17
16
  from qubx.core.errors import ErrorLevel, OrderCancellationError, OrderCreationError, create_error_event
18
- from qubx.core.exceptions import BadRequest, InvalidOrderParameters
17
+ from qubx.core.exceptions import BadRequest, InvalidOrderParameters, OrderNotFound
19
18
  from qubx.core.interfaces import (
20
19
  IAccountProcessor,
21
20
  IBroker,
@@ -43,6 +42,7 @@ class CcxtBroker(IBroker):
43
42
  max_cancel_retries: int = 10,
44
43
  enable_create_order_ws: bool = False,
45
44
  enable_cancel_order_ws: bool = False,
45
+ enable_edit_order_ws: bool = False,
46
46
  ):
47
47
  self._exchange_manager = exchange_manager
48
48
  self.ccxt_exchange_id = str(self._exchange_manager.exchange.name)
@@ -55,13 +55,13 @@ class CcxtBroker(IBroker):
55
55
  self.max_cancel_retries = max_cancel_retries
56
56
  self.enable_create_order_ws = enable_create_order_ws
57
57
  self.enable_cancel_order_ws = enable_cancel_order_ws
58
+ self.enable_edit_order_ws = enable_edit_order_ws
58
59
 
59
60
  @property
60
61
  def _loop(self) -> AsyncThreadLoop:
61
62
  """Get current AsyncThreadLoop for the exchange."""
62
63
  return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
63
64
 
64
-
65
65
  @property
66
66
  def is_simulated_trading(self) -> bool:
67
67
  return False
@@ -178,7 +178,7 @@ class CcxtBroker(IBroker):
178
178
  client_id: str | None = None,
179
179
  time_in_force: str = "gtc",
180
180
  **options,
181
- ) -> Order | None:
181
+ ) -> Order:
182
182
  """
183
183
  Submit an order and wait for the result. Exceptions will be raised on errors.
184
184
 
@@ -221,22 +221,38 @@ class CcxtBroker(IBroker):
221
221
  self._post_order_error_to_databus(
222
222
  err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
223
223
  )
224
- return None
224
+ raise err
225
225
 
226
- def cancel_order(self, order_id: str) -> Order | None:
226
+ def cancel_order(self, order_id: str) -> bool:
227
+ """Cancel an order synchronously and return success status."""
227
228
  orders = self.account.get_orders()
228
229
  if order_id not in orders:
229
230
  logger.warning(f"Order {order_id} not found in active orders")
230
- return None
231
+ return False
231
232
 
232
233
  order = orders[order_id]
233
- logger.info(f"Canceling order {order_id} ...")
234
+ logger.info(f"Canceling order {order_id} synchronously...")
234
235
 
235
- # Submit the cancellation task to the async loop without waiting for the result
236
- self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
236
+ try:
237
+ # Submit the task and wait for result
238
+ future = self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
239
+ return future.result() # This will block until completion or timeout
240
+ except Exception as e:
241
+ logger.error(f"Error during synchronous order cancellation: {e}")
242
+ return False # Return False on any error for simplicity
243
+
244
+ def cancel_order_async(self, order_id: str) -> None:
245
+ """Cancel an order asynchronously (non blocking)."""
246
+ orders = self.account.get_orders()
247
+ if order_id not in orders:
248
+ logger.warning(f"Order {order_id} not found in active orders")
249
+ return
237
250
 
238
- # Always return None as requested
239
- return None
251
+ order = orders[order_id]
252
+ logger.info(f"Canceling order {order_id} asynchronously...")
253
+
254
+ # Submit the task without waiting for result
255
+ self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
240
256
 
241
257
  async def _create_order(
242
258
  self,
@@ -298,8 +314,22 @@ class CcxtBroker(IBroker):
298
314
  logger.warning(f"[<y>{instrument.symbol}</y>] :: Quote is not available for order creation.")
299
315
  raise BadRequest(f"Quote is not available for order creation for {instrument.symbol}")
300
316
 
301
- # TODO: think about automatically setting reduce only when needed
302
- if not (reduce_only := options.get("reduceOnly", False)):
317
+ # Auto-detect if order reduces existing position
318
+ reduce_only = options.get("reduceOnly", False)
319
+ if not reduce_only:
320
+ positions = self.account.get_positions()
321
+ if instrument in positions:
322
+ position_qty = positions[instrument].quantity
323
+ # Check if order closes position AND doesn't exceed position size (which would flip to opposite side)
324
+ if (position_qty > 0 and order_side == "SELL" and abs(amount) <= abs(position_qty)) or (
325
+ position_qty < 0 and order_side == "BUY" and abs(amount) <= abs(position_qty)
326
+ ):
327
+ reduce_only = True
328
+ logger.debug(
329
+ f"[{instrument.symbol}] Auto-setting reduceOnly=True ({order_side}, position: {position_qty})"
330
+ )
331
+
332
+ if not reduce_only:
303
333
  min_notional = instrument.min_notional
304
334
  if min_notional > 0 and abs(amount) * quote.mid_price() < min_notional:
305
335
  raise InvalidOrderParameters(
@@ -364,9 +394,13 @@ class CcxtBroker(IBroker):
364
394
  while True:
365
395
  try:
366
396
  if self.enable_cancel_order_ws:
367
- await self._exchange_manager.exchange.cancel_order_ws(order_id, symbol=instrument_to_ccxt_symbol(instrument))
397
+ await self._exchange_manager.exchange.cancel_order_ws(
398
+ order_id, symbol=instrument_to_ccxt_symbol(instrument)
399
+ )
368
400
  else:
369
- await self._exchange_manager.exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(instrument))
401
+ await self._exchange_manager.exchange.cancel_order(
402
+ order_id, symbol=instrument_to_ccxt_symbol(instrument)
403
+ )
370
404
  return True
371
405
  except ccxt.OperationRejected as err:
372
406
  err_msg = str(err).lower()
@@ -375,8 +409,12 @@ class CcxtBroker(IBroker):
375
409
  # These errors might be temporary if the order is still being processed, so retry
376
410
  logger.debug(f"[{order_id}] Order not found for cancellation, might retry: {err}")
377
411
  # Continue with the retry logic instead of returning immediately
412
+ # Order cannot be cancelled (e.g., already filled)
413
+ elif "filled" in err_msg or "partially filled" in err_msg:
414
+ logger.debug(f"[{order_id}] Order cannot be cancelled - already executed: {err}")
415
+ return False # FAILURE: Order cannot be cancelled
416
+ # Other operation rejected errors - don't retry
378
417
  else:
379
- # For other operation rejected errors, don't retry
380
418
  logger.debug(f"[{order_id}] Could not cancel order: {err}")
381
419
  return False
382
420
  except (ccxt.NetworkError, ccxt.ExchangeError, ccxt.ExchangeNotAvailable) as e:
@@ -424,8 +462,117 @@ class CcxtBroker(IBroker):
424
462
  for order_id in instrument_orders:
425
463
  self.cancel_order(order_id)
426
464
 
427
- def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
428
- raise NotImplementedError("Not implemented yet")
465
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
466
+ """Update an existing limit order with new price and amount.
467
+
468
+ Args:
469
+ order_id: The ID of the order to update
470
+ price: New price for the order (already adjusted by TradingManager)
471
+ amount: New amount for the order (already adjusted by TradingManager)
472
+
473
+ Returns:
474
+ Order: The updated Order object if successful
475
+
476
+ Raises:
477
+ OrderNotFound: If the order is not found
478
+ BadRequest: If the order is not a limit order
479
+ ExchangeError: If the exchange operation fails
480
+ """
481
+ active_orders = self.account.get_orders()
482
+ if order_id not in active_orders:
483
+ raise OrderNotFound(f"Order {order_id} not found in active orders")
484
+
485
+ existing_order = active_orders[order_id]
486
+
487
+ # Validate that the order can still be updated (not fully filled/closed)
488
+ updatable_statuses = ["OPEN", "NEW", "PENDING"]
489
+ if existing_order.status not in updatable_statuses:
490
+ raise BadRequest(
491
+ f"Order {order_id} with status '{existing_order.status}' cannot be updated. "
492
+ f"Only orders with status {updatable_statuses} can be updated."
493
+ )
494
+
495
+ instrument = existing_order.instrument
496
+
497
+ logger.debug(
498
+ f"[<g>{instrument.symbol}</g>] :: Updating order {order_id}: "
499
+ f"{amount} @ {price} (was: {existing_order.quantity} @ {existing_order.price} ({existing_order.time_in_force}))"
500
+ )
501
+
502
+ try:
503
+ # Check if exchange supports order editing
504
+ if self._exchange_manager.exchange.has.get("editOrder", False):
505
+ return self._update_order_direct(order_id, existing_order, price, amount)
506
+ else:
507
+ return self._update_order_fallback(order_id, existing_order, price, amount)
508
+ except Exception as err:
509
+ logger.error(f"Failed to update order {order_id}: {err}")
510
+ raise
511
+
512
+ def _update_order_direct(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
513
+ """Update order using exchange's native edit functionality."""
514
+ future_result = self._loop.submit(self._edit_order_async(order_id, existing_order, price, amount))
515
+ updated_order, error = future_result.result()
516
+
517
+ if error is not None:
518
+ raise error
519
+
520
+ if updated_order is not None:
521
+ self.account.process_order(updated_order)
522
+ logger.debug(f"[<g>{existing_order.instrument.symbol}</g>] :: Successfully updated order {order_id}")
523
+ return updated_order
524
+ else:
525
+ raise Exception("Order update returned None without error")
526
+
527
+ def _update_order_fallback(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
528
+ """Update order using cancel+recreate strategy for exchanges without editOrder support."""
529
+ success = self.cancel_order(order_id)
530
+ if not success:
531
+ raise Exception(f"Failed to cancel order {order_id} during update")
532
+
533
+ updated_order = self.send_order(
534
+ instrument=existing_order.instrument,
535
+ order_side=existing_order.side,
536
+ order_type=existing_order.type,
537
+ amount=amount,
538
+ price=price,
539
+ client_id=existing_order.client_id, # Preserve original client_id for tracking
540
+ time_in_force=existing_order.time_in_force or "gtc",
541
+ )
542
+
543
+ logger.debug(
544
+ f"[<g>{existing_order.instrument.symbol}</g>] :: Successfully updated order {order_id} -> new order {updated_order.id}"
545
+ )
546
+ return updated_order
547
+
548
+ async def _edit_order_async(
549
+ self, order_id: str, existing_order: Order, price: float, amount: float
550
+ ) -> tuple[Order | None, Exception | None]:
551
+ """Async helper for direct order editing."""
552
+ try:
553
+ ccxt_symbol = instrument_to_ccxt_symbol(existing_order.instrument)
554
+ ccxt_side = "buy" if existing_order.side == "BUY" else "sell"
555
+
556
+ # CCXT requires positive amount (side determines direction)
557
+ abs_amount = abs(amount)
558
+
559
+ # Use WebSocket if enabled, otherwise use REST API
560
+ if self.enable_edit_order_ws:
561
+ result = await self._exchange_manager.exchange.edit_order_ws(
562
+ id=order_id, symbol=ccxt_symbol, type="limit", side=ccxt_side, amount=abs_amount, price=price, params={}
563
+ )
564
+ else:
565
+ result = await self._exchange_manager.exchange.edit_order(
566
+ id=order_id, symbol=ccxt_symbol, type="limit", side=ccxt_side, amount=abs_amount, price=price, params={}
567
+ )
568
+
569
+ # Convert the result back to our Order format
570
+ updated_order = ccxt_convert_order_info(existing_order.instrument, result)
571
+ return updated_order, None
572
+
573
+ except Exception as err:
574
+ logger.error(f"Async edit order failed for {order_id}: {err}")
575
+ return None, err
429
576
 
430
577
  def exchange(self) -> str:
431
578
  """
@@ -13,6 +13,7 @@ from collections import defaultdict
13
13
  from typing import Awaitable, Callable
14
14
 
15
15
  from ccxt import ExchangeClosedByUser, ExchangeError, ExchangeNotAvailable, NetworkError
16
+ from ccxt.async_support.base.ws.client import Client as _WsClient
16
17
  from ccxt.pro import Exchange
17
18
  from qubx import logger
18
19
  from qubx.core.basics import CtrlChannel
@@ -23,6 +24,19 @@ from .exchange_manager import ExchangeManager
23
24
  from .subscription_manager import SubscriptionManager
24
25
 
25
26
 
27
+ def _safe_buffer(self):
28
+ conn = getattr(self.connection, "_conn", None)
29
+ if not conn or not getattr(conn, "protocol", None):
30
+ return b""
31
+ payload = getattr(conn.protocol, "_payload", None)
32
+ buf = getattr(payload, "_buffer", None)
33
+ return buf if buf is not None else b""
34
+
35
+
36
+ # SAFETY PATCH: make ccxt WS buffer access resilient to closed connections
37
+ _WsClient.buffer = property(_safe_buffer) # type: ignore
38
+
39
+
26
40
  class ConnectionManager:
27
41
  """
28
42
  Manages WebSocket connections and stream lifecycle for CCXT data provider.
@@ -18,8 +18,6 @@ from qubx.core.interfaces import IDataArrivalListener
18
18
 
19
19
  # Constants for better maintainability
20
20
  DEFAULT_CHECK_INTERVAL_SECONDS = 60.0
21
- DEFAULT_MAX_RECREATIONS = 5
22
- DEFAULT_RESET_INTERVAL_HOURS = 6.0
23
21
  SECONDS_PER_HOUR = 3600
24
22
 
25
23
  # Custom stall detection thresholds (in seconds)
@@ -30,7 +28,7 @@ STALL_THRESHOLDS = {
30
28
  "trade": 60 * 60, # 60 minutes = 3,600s
31
29
  "liquidation": 7 * 24 * SECONDS_PER_HOUR, # 7 days = 604,800s
32
30
  "ohlc": 5 * 60, # 5 minutes = 300s
33
- "quote": 5 * 60, # 5 minutes = 300s
31
+ "quote": 2 * 60, # 2 minutes = 120s
34
32
  }
35
33
  DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
36
34
 
@@ -45,7 +43,7 @@ class ExchangeManager(IDataArrivalListener):
45
43
  Key Features:
46
44
  - Explicit .exchange property for CCXT access
47
45
  - Self-contained stall detection and recreation triggering
48
- - Circuit breaker protection with recreation limits
46
+ - Automatic recreation without limits when data stalls
49
47
  - Atomic exchange transitions during recreation
50
48
  - Background monitoring thread for stall detection
51
49
  """
@@ -57,8 +55,6 @@ class ExchangeManager(IDataArrivalListener):
57
55
  exchange_name: str,
58
56
  factory_params: dict[str, Any],
59
57
  initial_exchange: Optional[cxp.Exchange] = None,
60
- max_recreations: int = DEFAULT_MAX_RECREATIONS,
61
- reset_interval_hours: float = DEFAULT_RESET_INTERVAL_HOURS,
62
58
  check_interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
63
59
  ):
64
60
  """Initialize ExchangeManager with underlying CCXT exchange.
@@ -67,19 +63,14 @@ class ExchangeManager(IDataArrivalListener):
67
63
  exchange_name: Exchange name for factory (e.g., "binance.um")
68
64
  factory_params: Parameters for get_ccxt_exchange()
69
65
  initial_exchange: Pre-created exchange instance (from factory)
70
- max_recreations: Maximum recreation attempts before giving up
71
- reset_interval_hours: Hours between recreation count resets
72
66
  check_interval_seconds: How often to check for stalls (default: 60.0)
73
67
  """
74
68
  self._exchange_name = exchange_name
75
69
  self._factory_params = factory_params.copy()
76
- self._max_recreations = max_recreations
77
- self._reset_interval_hours = reset_interval_hours
78
70
 
79
71
  # Recreation state
80
- self._recreation_count = 0
81
72
  self._recreation_lock = threading.RLock()
82
- self._last_successful_reset = time.time()
73
+ self._recreation_count = 0 # Track for logging purposes only
83
74
 
84
75
  # Stall detection state
85
76
  self._check_interval = check_interval_seconds
@@ -142,28 +133,19 @@ class ExchangeManager(IDataArrivalListener):
142
133
 
143
134
  def force_recreation(self) -> bool:
144
135
  """
145
- Force recreation due to data stalls (called by BaseHealthMonitor).
136
+ Force recreation due to data stalls.
146
137
 
147
138
  Returns:
148
- True if recreation successful, False if failed/limit exceeded
139
+ True if recreation successful, False if failed
149
140
  """
150
141
  with self._recreation_lock:
151
- # Check recreation limit
152
- if self._recreation_count >= self._max_recreations:
153
- logger.error(
154
- f"Cannot recreate {self._exchange_name}: recreation limit ({self._max_recreations}) exceeded"
155
- )
156
- return False
157
-
158
142
  logger.info(f"Stall-triggered recreation for {self._exchange_name}")
159
143
  return self._recreate_exchange()
160
144
 
161
145
  def _recreate_exchange(self) -> bool:
162
146
  """Recreate the underlying exchange (must be called with _recreation_lock held)."""
163
147
  self._recreation_count += 1
164
- logger.warning(
165
- f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count}/{self._max_recreations})"
166
- )
148
+ logger.warning(f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count})")
167
149
 
168
150
  # Create new exchange
169
151
  try:
@@ -190,18 +172,6 @@ class ExchangeManager(IDataArrivalListener):
190
172
 
191
173
  return True
192
174
 
193
- def reset_recreation_count_if_needed(self) -> None:
194
- """Reset recreation count periodically (called by monitoring loop)."""
195
- reset_interval_seconds = self._reset_interval_hours * SECONDS_PER_HOUR
196
-
197
- current_time = time.time()
198
- time_since_reset = current_time - self._last_successful_reset
199
-
200
- if time_since_reset >= reset_interval_seconds and self._recreation_count > 0:
201
- logger.info(f"Resetting recreation count for {self._exchange_name} (was {self._recreation_count})")
202
- self._recreation_count = 0
203
- self._last_successful_reset = current_time
204
-
205
175
  def on_data_arrival(self, event_type: str, event_time: dt_64) -> None:
206
176
  """Record data arrival for stall detection.
207
177
 
@@ -254,7 +224,6 @@ class ExchangeManager(IDataArrivalListener):
254
224
  while self._monitoring_enabled:
255
225
  try:
256
226
  self._check_and_handle_stalls()
257
- self.reset_recreation_count_if_needed()
258
227
  time.sleep(self._check_interval)
259
228
  except Exception as e:
260
229
  logger.error(f"Error in ExchangeManager stall detection: {e}")
@@ -12,6 +12,7 @@ from .binance.broker import BinanceCcxtBroker
12
12
  from .binance.exchange import BINANCE_UM_MM, BinancePortfolioMargin, BinanceQV, BinanceQVUSDM
13
13
  from .bitfinex.bitfinex import BitfinexF
14
14
  from .bitfinex.bitfinex_account import BitfinexAccountProcessor
15
+ from .hyperliquid.account import HyperliquidAccountProcessor
15
16
  from .hyperliquid.broker import HyperliquidCcxtBroker
16
17
  from .hyperliquid.hyperliquid import Hyperliquid, HyperliquidF
17
18
  from .kraken.kraken import CustomKrakenFutures
@@ -45,12 +46,14 @@ CUSTOM_BROKERS = {
45
46
  "binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
46
47
  "binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
47
48
  "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),
49
+ "hyperliquid": partial(HyperliquidCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False, enable_edit_order_ws=True),
50
+ "hyperliquid.f": partial(HyperliquidCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False, enable_edit_order_ws=True),
50
51
  }
51
52
 
52
53
  CUSTOM_ACCOUNTS = {
53
54
  "bitfinex.f": BitfinexAccountProcessor,
55
+ "hyperliquid": HyperliquidAccountProcessor,
56
+ "hyperliquid.f": HyperliquidAccountProcessor,
54
57
  }
55
58
 
56
59
  READER_CAPABILITIES = {
@@ -0,0 +1,7 @@
1
+ # Hyperliquid exchange overrides
2
+
3
+ from .account import HyperliquidAccountProcessor
4
+ from .broker import HyperliquidCcxtBroker
5
+ from .hyperliquid import Hyperliquid, HyperliquidF
6
+
7
+ __all__ = ["HyperliquidAccountProcessor", "HyperliquidCcxtBroker", "Hyperliquid", "HyperliquidF"]