Qubx 0.6.94__tar.gz → 0.6.95__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 (261) hide show
  1. {qubx-0.6.94 → qubx-0.6.95}/PKG-INFO +1 -1
  2. {qubx-0.6.94 → qubx-0.6.95}/pyproject.toml +1 -1
  3. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/constants.py +4 -2
  4. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/data.py +30 -1
  5. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/base.py +12 -0
  6. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/orderbook.py +9 -1
  7. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/websocket.py +36 -17
  8. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/basics.py +5 -1
  9. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/metrics.py +12 -6
  10. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/misc.py +6 -1
  11. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/orderbook.py +193 -18
  12. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/configs.py +14 -7
  13. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/runner.py +3 -0
  14. qubx-0.6.95/src/qubx/utils/websocket_manager.py +445 -0
  15. qubx-0.6.94/src/qubx/utils/websocket_manager.py +0 -592
  16. {qubx-0.6.94 → qubx-0.6.95}/LICENSE +0 -0
  17. {qubx-0.6.94 → qubx-0.6.95}/README.md +0 -0
  18. {qubx-0.6.94 → qubx-0.6.95}/build.py +0 -0
  19. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/__init__.py +0 -0
  20. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/_nb_magic.py +0 -0
  21. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/__init__.py +0 -0
  22. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/account.py +0 -0
  23. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/broker.py +0 -0
  24. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/data.py +0 -0
  25. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/management.py +0 -0
  26. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/ome.py +0 -0
  27. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/optimization.py +0 -0
  28. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/runner.py +0 -0
  29. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/sentinels.py +0 -0
  30. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/simulated_data.py +0 -0
  31. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/simulated_exchange.py +0 -0
  32. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/simulator.py +0 -0
  33. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/transfers.py +0 -0
  34. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/backtester/utils.py +0 -0
  35. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/__init__.py +0 -0
  36. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/commands.py +0 -0
  37. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/deploy.py +0 -0
  38. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/misc.py +0 -0
  39. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/release.py +0 -0
  40. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/cli/tui.py +0 -0
  41. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/__init__.py +0 -0
  42. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/account.py +0 -0
  43. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  44. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  45. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/broker.py +0 -0
  46. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  47. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/data.py +0 -0
  48. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  49. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  50. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  51. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  52. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  53. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  54. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  55. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  56. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  57. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  58. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  59. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  60. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  61. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/factory.py +0 -0
  62. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  63. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  64. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  65. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  66. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  67. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  68. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  69. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  70. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  71. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  72. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/reader.py +0 -0
  73. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  74. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  75. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  76. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/utils.py +0 -0
  77. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  78. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/tardis/data.py +0 -0
  79. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/tardis/utils.py +0 -0
  80. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/__init__.py +0 -0
  81. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/account.py +0 -0
  82. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/broker.py +0 -0
  83. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/client.py +0 -0
  84. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/extensions.py +0 -0
  85. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/factory.py +0 -0
  86. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/__init__.py +0 -0
  87. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/quote.py +0 -0
  88. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/stats.py +0 -0
  89. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/trades.py +0 -0
  90. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/instruments.py +0 -0
  91. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/parsers.py +0 -0
  92. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/reader.py +0 -0
  93. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/connectors/xlighter/utils.py +0 -0
  94. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/__init__.py +0 -0
  95. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/account.py +0 -0
  96. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/context.py +0 -0
  97. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/deque.py +0 -0
  98. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/detectors/__init__.py +0 -0
  99. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/detectors/delisting.py +0 -0
  100. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/detectors/stale.py +0 -0
  101. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/errors.py +0 -0
  102. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/exceptions.py +0 -0
  103. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/helpers.py +0 -0
  104. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/initializer.py +0 -0
  105. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/interfaces.py +0 -0
  106. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/loggers.py +0 -0
  107. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/lookups.py +0 -0
  108. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/__init__.py +0 -0
  109. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/market.py +0 -0
  110. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/processing.py +0 -0
  111. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/subscription.py +0 -0
  112. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/trading.py +0 -0
  113. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/universe.py +0 -0
  114. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/mixins/utils.py +0 -0
  115. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/series.pxd +0 -0
  116. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/series.pyi +0 -0
  117. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/series.pyx +0 -0
  118. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/utils.pyi +0 -0
  119. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/core/utils.pyx +0 -0
  120. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/__init__.py +0 -0
  121. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/composite.py +0 -0
  122. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/containers.py +0 -0
  123. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/helpers.py +0 -0
  124. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/hft.py +0 -0
  125. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/readers.py +0 -0
  126. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/registry.py +0 -0
  127. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/storage.py +0 -0
  128. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/storages/csv.py +0 -0
  129. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/storages/questdb.py +0 -0
  130. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/storages/utils.py +0 -0
  131. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/tardis.py +0 -0
  132. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/data/transformers.py +0 -0
  133. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/__init__.py +0 -0
  134. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/base.py +0 -0
  135. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/composite.py +0 -0
  136. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/csv.py +0 -0
  137. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/indicator.py +0 -0
  138. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/inmemory.py +0 -0
  139. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/prometheus.py +0 -0
  140. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/emitters/questdb.py +0 -0
  141. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/__init__.py +0 -0
  142. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/composite.py +0 -0
  143. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/__init__.py +0 -0
  144. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/base.py +0 -0
  145. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/incremental.py +0 -0
  146. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/slack.py +0 -0
  147. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/formatters/target_position.py +0 -0
  148. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/redis_streams.py +0 -0
  149. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/exporters/slack.py +0 -0
  150. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/__init__.py +0 -0
  151. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/core.py +0 -0
  152. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/orderbook.py +0 -0
  153. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/price.py +0 -0
  154. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/trades.py +0 -0
  155. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/features/utils.py +0 -0
  156. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/gathering/simplest.py +0 -0
  157. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/health/__init__.py +0 -0
  158. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/health/base.py +0 -0
  159. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/__init__.py +0 -0
  160. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/csv.py +0 -0
  161. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/factory.py +0 -0
  162. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/inmemory.py +0 -0
  163. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/loggers/mongo.py +0 -0
  164. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/math/__init__.py +0 -0
  165. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/math/stats.py +0 -0
  166. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/notifications/__init__.py +0 -0
  167. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/notifications/composite.py +0 -0
  168. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/notifications/slack.py +0 -0
  169. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/notifications/throttler.py +0 -0
  170. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/pandaz/__init__.py +0 -0
  171. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/pandaz/ta.py +0 -0
  172. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/pandaz/utils.py +0 -0
  173. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/_build.py +0 -0
  174. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/crypto-fees.ini +0 -0
  175. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  176. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  177. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  178. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  179. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  180. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  181. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  182. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  183. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  184. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  185. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  186. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restarts/__init__.py +0 -0
  187. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restarts/state_resolvers.py +0 -0
  188. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restarts/time_finders.py +0 -0
  189. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/__init__.py +0 -0
  190. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/balance.py +0 -0
  191. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/factory.py +0 -0
  192. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/interfaces.py +0 -0
  193. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/position.py +0 -0
  194. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/signal.py +0 -0
  195. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/state.py +0 -0
  196. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/restorers/utils.py +0 -0
  197. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/ta/__init__.py +0 -0
  198. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/ta/indicators.pxd +0 -0
  199. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/ta/indicators.pyi +0 -0
  200. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/ta/indicators.pyx +0 -0
  201. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/__init__.py +0 -0
  202. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/base.py +0 -0
  203. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  204. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/config.yml.j2 +0 -0
  205. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  206. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  207. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  208. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  209. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  210. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/project/template.yml +0 -0
  211. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  212. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  213. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/config.yml.j2 +0 -0
  214. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  215. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  216. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  217. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/templates/simple/template.yml +0 -0
  218. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/__init__.py +0 -0
  219. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/advanced.py +0 -0
  220. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/composite.py +0 -0
  221. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/rebalancers.py +0 -0
  222. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/riskctrl.py +0 -0
  223. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/trackers/sizers.py +0 -0
  224. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/__init__.py +0 -0
  225. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/_pyxreloader.py +0 -0
  226. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/charting/lookinglass.py +0 -0
  227. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  228. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/charting/orderbook.py +0 -0
  229. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/collections.py +0 -0
  230. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/marketdata/binance.py +0 -0
  231. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/marketdata/ccxt.py +0 -0
  232. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/marketdata/dukas.py +0 -0
  233. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/ntp.py +0 -0
  234. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/numbers_utils.py +0 -0
  235. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/__init__.py +0 -0
  236. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/dashboard.py +0 -0
  237. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/data.py +0 -0
  238. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/interfaces.py +0 -0
  239. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  240. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  241. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/questdb.py +0 -0
  242. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/__init__.py +0 -0
  243. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  244. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/accounts.py +0 -0
  245. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/factory.py +0 -0
  246. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/kernel_service.py +0 -0
  247. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/__init__.py +0 -0
  248. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/app.py +0 -0
  249. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/handlers.py +0 -0
  250. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/init_code.py +0 -0
  251. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/kernel.py +0 -0
  252. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  253. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  254. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  255. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  256. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  257. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  258. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  259. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  260. {qubx-0.6.94 → qubx-0.6.95}/src/qubx/utils/time.py +0 -0
  261. {qubx-0.6.94 → qubx-0.6.95}/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.94
3
+ Version: 0.6.95
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.94"
7
+ version = "0.6.95"
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"
@@ -70,9 +70,11 @@ API_BASE_TESTNET = "https://testnet.zklighter.elliot.ai"
70
70
  WS_BASE_MAINNET = "wss://mainnet.zklighter.elliot.ai/stream"
71
71
  WS_BASE_TESTNET = "wss://testnet.zklighter.elliot.ai/stream"
72
72
 
73
- DEFAULT_PING_INTERVAL = 200
74
- DEFAULT_PING_TIMEOUT = 30
73
+ DEFAULT_PING_INTERVAL = None
74
+ DEFAULT_PING_TIMEOUT = None
75
75
  DEFAULT_MAX_RETRIES = 10
76
+ DEFAULT_MAX_SIZE = None
77
+ DEFAULT_MAX_QUEUE = 5000
76
78
 
77
79
 
78
80
  # Enums for type safety (kept for backward compatibility)
@@ -82,6 +82,9 @@ class LighterDataProvider(IDataProvider):
82
82
  # Track if market_stats:all is subscribed (single subscription for all instruments)
83
83
  self._market_stats_subscribed: bool = False
84
84
 
85
+ # Track if reconnection callback has been registered
86
+ self._reconnection_callback_registered: bool = False
87
+
85
88
  logger.info("LighterDataProvider initialized")
86
89
 
87
90
  @property
@@ -160,6 +163,25 @@ class LighterDataProvider(IDataProvider):
160
163
  f"Subscribed to {subscription_type} for {len(instruments)} instruments: {[i.symbol for i in instruments]}"
161
164
  )
162
165
 
166
+ async def _on_reconnected(self) -> None:
167
+ """
168
+ Callback invoked after WebSocket reconnection.
169
+
170
+ Resets all handler states to ensure clean state after reconnection.
171
+ This is particularly important for stateful handlers like OrderbookHandler
172
+ which maintain incremental state that becomes invalid after disconnection.
173
+ """
174
+ logger.info("WebSocket reconnected, resetting all handler states")
175
+
176
+ # Reset all handlers (stateless handlers have empty reset() implementation)
177
+ for handler in self._handlers.values():
178
+ try:
179
+ handler.reset()
180
+ except Exception as e:
181
+ logger.error(f"Error resetting handler {handler.__class__.__name__}: {e}")
182
+
183
+ logger.debug(f"Reset {len(self._handlers)} handlers after reconnection")
184
+
163
185
  def _ensure_websocket_connected(self, timeout: float = 5.0) -> None:
164
186
  """
165
187
  Ensure WebSocket is connected, wait if necessary.
@@ -192,6 +214,12 @@ class LighterDataProvider(IDataProvider):
192
214
  await self._ws_manager.connect()
193
215
  self._ws_connected = True
194
216
 
217
+ # Register reconnection callback (one-time setup)
218
+ if not self._reconnection_callback_registered:
219
+ self._ws_manager.on_reconnected(self._on_reconnected)
220
+ self._reconnection_callback_registered = True
221
+ logger.debug("Registered reconnection callback for handler state reset")
222
+
195
223
  # Submit and WAIT for connection
196
224
  future = self._async_loop.submit(_connect())
197
225
  try:
@@ -302,7 +330,8 @@ class LighterDataProvider(IDataProvider):
302
330
  handler = cast(OrderbookHandler, self._handlers.get(handler_key))
303
331
 
304
332
  if handler and handler.can_handle(message):
305
- orderbook = cast(OrderBook, handler.handle(message))
333
+ orderbook = handler.handle(message)
334
+ orderbook = cast(OrderBook, orderbook)
306
335
  if orderbook:
307
336
  # Send to channel
308
337
  self.channel.send((instrument, "orderbook", orderbook, False))
@@ -102,3 +102,15 @@ class BaseHandler(ABC, Generic[T]):
102
102
  """Reset statistics counters"""
103
103
  self._message_count = 0
104
104
  self._error_count = 0
105
+
106
+ def reset(self):
107
+ """
108
+ Reset handler internal state.
109
+
110
+ This method is called when the WebSocket connection is reestablished
111
+ to ensure handlers start with clean state. Handlers with stateful
112
+ components should override this method to reset their state.
113
+
114
+ The default implementation does nothing (stateless handlers).
115
+ """
116
+ pass
@@ -58,7 +58,11 @@ class OrderbookHandler(BaseHandler[OrderBook]):
58
58
  self.max_levels = max_levels
59
59
  self.tick_size_pct = tick_size_pct
60
60
  self.instrument = instrument
61
- self._state_manager = OrderBookStateManager()
61
+
62
+ # Initialize state manager with sufficient buffer size
63
+ # Use larger buffer to accommodate raw orderbook before aggregation
64
+ state_manager_max_levels = max(1000, 2 * max_levels)
65
+ self._state_manager = OrderBookStateManager(max_levels=state_manager_max_levels)
62
66
 
63
67
  def can_handle(self, message: dict[str, Any]) -> bool:
64
68
  channel = message.get("channel", "")
@@ -96,6 +100,10 @@ class OrderbookHandler(BaseHandler[OrderBook]):
96
100
  logger.warning("Missing order_book in message")
97
101
  return None
98
102
 
103
+ is_update = message.get("type") == "update/order_book"
104
+ if not is_update:
105
+ self._state_manager.reset()
106
+
99
107
  bids = book.get("bids", [])
100
108
  asks = book.get("asks", [])
101
109
  bids = [(float(bid["price"]), float(bid["size"])) for bid in bids]
@@ -13,7 +13,6 @@ from qubx.utils.websocket_manager import BaseWebSocketManager, ReconnectionConfi
13
13
  from .client import LighterClient
14
14
  from .constants import (
15
15
  DEFAULT_MAX_RETRIES,
16
- DEFAULT_PING_INTERVAL,
17
16
  DEFAULT_PING_TIMEOUT,
18
17
  WS_BASE_MAINNET,
19
18
  WS_BASE_TESTNET,
@@ -43,8 +42,9 @@ class LighterWebSocketManager(BaseWebSocketManager):
43
42
  client: LighterClient,
44
43
  testnet: bool = False,
45
44
  reconnection_config: Optional[ReconnectionConfig] = None,
46
- ping_interval: float | None = DEFAULT_PING_INTERVAL,
45
+ ping_interval: float | None = None,
47
46
  ping_timeout: float | None = DEFAULT_PING_TIMEOUT,
47
+ application_ping_interval: float | None = 20.0,
48
48
  ):
49
49
  """
50
50
  Initialize Lighter WebSocket manager.
@@ -52,19 +52,25 @@ class LighterWebSocketManager(BaseWebSocketManager):
52
52
  Args:
53
53
  testnet: If True, connect to testnet. Otherwise mainnet.
54
54
  reconnection_config: Configuration for reconnection behavior
55
- ping_interval: Interval between pings (seconds)
55
+ ping_interval: Interval for protocol-level pings (seconds), or None to disable (default: None)
56
56
  ping_timeout: Timeout for ping response (seconds)
57
+ application_ping_interval: Interval for application-level pings (seconds), or None to disable (default: 20.0)
57
58
  """
58
- url = WS_BASE_TESTNET if testnet else WS_BASE_MAINNET
59
-
60
59
  if reconnection_config is None:
61
60
  reconnection_config = ReconnectionConfig(max_retries=DEFAULT_MAX_RETRIES)
62
61
 
63
62
  super().__init__(
64
- url=url,
65
- reconnection_config=reconnection_config,
66
- ping_interval=ping_interval,
67
- ping_timeout=ping_timeout,
63
+ url=WS_BASE_TESTNET if testnet else WS_BASE_MAINNET,
64
+ reconnection=reconnection_config,
65
+ ping_interval=None, # use app-level pings
66
+ ping_timeout=None,
67
+ max_size=16 * 1024 * 1024,
68
+ max_queue=None,
69
+ compression=None,
70
+ inbox_size=5000,
71
+ workers=1,
72
+ app_ping_interval=20.0, # send {"type":"ping"} every 20s
73
+ no_rx_reconnect_after=60.0,
68
74
  )
69
75
 
70
76
  self.testnet = testnet
@@ -334,11 +340,14 @@ class LighterWebSocketManager(BaseWebSocketManager):
334
340
  Returns:
335
341
  Channel identifier or None
336
342
  """
337
- channel = message.get("channel")
338
- if channel:
339
- # Convert "order_book:0" back to "order_book/0"
340
- channel = channel.replace(":", "/")
341
- return channel
343
+ try:
344
+ channel = message.get("channel", None)
345
+ if channel:
346
+ # Convert "order_book:0" back to "order_book/0"
347
+ channel = channel.replace(":", "/")
348
+ return channel
349
+ except Exception:
350
+ return None
342
351
 
343
352
  async def _handle_error_message(self, error: dict) -> None:
344
353
  """
@@ -352,13 +361,16 @@ class LighterWebSocketManager(BaseWebSocketManager):
352
361
  """
353
362
  error_code = error.get("code", -1)
354
363
  error_message = error.get("message", "Unknown error")
355
- logger.error(f"Lighter WebSocket error [{error_code}] {error_message}")
356
364
  match error_code:
357
365
  case 20013:
358
366
  # expired token
367
+ logger.warning("Auth token expired, generating new one")
359
368
  self._update_auth_token()
369
+ case 30003:
370
+ # Alread subscribed
371
+ logger.debug(f"Already subscribed to {error['channel']}")
360
372
  case _:
361
- pass
373
+ logger.warning(f"Lighter WebSocket error [{error_code}] {error_message}")
362
374
 
363
375
  def _update_auth_token(self):
364
376
  """
@@ -423,6 +435,9 @@ class LighterWebSocketManager(BaseWebSocketManager):
423
435
  if self._on_connected_callback:
424
436
  await self._on_connected_callback()
425
437
 
438
+ elif msg_type == "pong":
439
+ pass
440
+
426
441
  elif msg_type == "ping":
427
442
  # Application-level ping - must respond with pong
428
443
  logger.debug("Received application-level ping, sending pong")
@@ -440,4 +455,8 @@ class LighterWebSocketManager(BaseWebSocketManager):
440
455
 
441
456
  else:
442
457
  # Log unknown messages for debugging
443
- logger.debug(f"Unhandled Lighter message: {message}")
458
+ # logger.debug(f"Unhandled Lighter message: {message}")
459
+ pass
460
+
461
+ def _app_ping_payload(self) -> Optional[dict]:
462
+ return {"type": "ping"}
@@ -295,7 +295,7 @@ class MarketType(StrEnum):
295
295
  OPTION = "OPTION"
296
296
 
297
297
 
298
- @dataclass
298
+ @dataclass(order=True)
299
299
  class Instrument:
300
300
  """
301
301
  Instrument class.
@@ -326,6 +326,10 @@ class Instrument:
326
326
  delist_date: datetime | None = None # date when instrument is delisted
327
327
  inverse: bool = False # if true, then the future is inverse
328
328
 
329
+ def __post_init__(self):
330
+ # define how ordering works
331
+ object.__setattr__(self, "sort_index", f"{self.exchange}:{self.market_type}:{self.symbol}")
332
+
329
333
  @property
330
334
  def price_precision(self):
331
335
  if not hasattr(self, "_price_precision"):
@@ -980,7 +980,8 @@ class TradingSessionResult:
980
980
  perf = {
981
981
  "cagr": 0.0,
982
982
  "sharpe": 0.0,
983
- "max_dd_pct": 0.0,
983
+ "mdd_pct": 0.0,
984
+ "daily_turnover": 0.0,
984
985
  "gain": 0.0,
985
986
  "calmar": 0.0,
986
987
  "sortino": 0.0,
@@ -1000,7 +1001,8 @@ class TradingSessionResult:
1000
1001
  "performance": {
1001
1002
  "cagr": perf.get("cagr", 0.0),
1002
1003
  "sharpe": perf.get("sharpe", 0.0),
1003
- "max_dd_pct": perf.get("max_dd_pct", 0.0),
1004
+ "mdd_pct": perf.get("mdd_pct", 0.0),
1005
+ "daily_turnover": perf.get("daily_turnover", 0.0),
1004
1006
  "total_return": perf.get("gain", 0.0) / self.get_total_capital() * 100
1005
1007
  if self.get_total_capital() > 0
1006
1008
  else 0.0,
@@ -1139,7 +1141,7 @@ class TradingSessionResult:
1139
1141
  params = info["parameters"]
1140
1142
 
1141
1143
  # - performance extracting
1142
- _dd_mtrx = ["max_dd_pct", "mdd_usd", "mdd_start", "mdd_peak", "mdd_recover"]
1144
+ _dd_mtrx = ["mdd_pct", "mdd_usd", "mdd_start", "mdd_peak", "mdd_recover"]
1143
1145
  perf_main = {"".join(list(map(str.capitalize, c.split("_")))): v for c, v in perf.items() if c not in _dd_mtrx}
1144
1146
  perf_dd = {"".join(list(map(str.capitalize, c.split("_")))): v for c, v in perf.items() if c in _dd_mtrx}
1145
1147
  perf_dd["MddStart"] = pd.Timestamp(perf_dd["MddStart"]).strftime("%Y-%m-%d %H:%M:%S")
@@ -1233,7 +1235,11 @@ description: {_desc}
1233
1235
  executions = pd.DataFrame()
1234
1236
  try:
1235
1237
  signals = pd.read_csv(
1236
- zip_ref.open("signals.csv"), index_col=["timestamp"], parse_dates=["timestamp"], date_format="mixed"
1238
+ zip_ref.open("signals.csv"),
1239
+ index_col=["timestamp"],
1240
+ parse_dates=["timestamp"],
1241
+ date_format="mixed",
1242
+ low_memory=False,
1237
1243
  )
1238
1244
  except:
1239
1245
  signals = pd.DataFrame()
@@ -1491,7 +1497,7 @@ def portfolio_metrics(
1491
1497
  sheet["drawdown_pct"] = mdd_pct
1492
1498
  sheet["drawdown_usd"] = dd_data
1493
1499
  # 25-May-2019: MDE fixed Max DD pct calculations
1494
- # sheet["max_dd_pct_on_init"] = 100 * mdd / init_cash
1500
+ # sheet["mdd_pct_on_init"] = 100 * mdd / init_cash
1495
1501
  sheet["mdd_start"] = equity.index[ddstart]
1496
1502
  sheet["mdd_peak"] = equity.index[ddpeak]
1497
1503
  sheet["mdd_recover"] = equity.index[ddrecover]
@@ -1672,7 +1678,7 @@ def tearsheet(
1672
1678
  report = report.set_index("id", append=True).swaplevel()
1673
1679
  if sort_by:
1674
1680
  report = report.sort_values(by=sort_by, ascending=sort_ascending)
1675
- return report
1681
+ return report.round(2)
1676
1682
 
1677
1683
  else:
1678
1684
  return _tearsheet_single(
@@ -447,7 +447,12 @@ class AsyncThreadLoop:
447
447
  self.loop = loop
448
448
 
449
449
  def submit(self, coro: Awaitable) -> concurrent.futures.Future:
450
- return asyncio.run_coroutine_threadsafe(coro, self.loop)
450
+ return asyncio.run_coroutine_threadsafe(coro, self.loop) # type: ignore
451
+
452
+ async def run_in_executor(
453
+ self, executor: concurrent.futures.Executor, func: Callable, *args, **kwargs
454
+ ) -> concurrent.futures.Future:
455
+ return await self.loop.run_in_executor(executor, func, *args, **kwargs)
451
456
 
452
457
 
453
458
  def synchronized(func: Callable):
@@ -13,6 +13,7 @@ import numpy as np
13
13
  import pandas as pd
14
14
  from numba import njit, types
15
15
  from numba.typed import Dict
16
+ from sortedcontainers import SortedDict
16
17
  from tqdm.auto import tqdm
17
18
 
18
19
  from qubx import logger
@@ -573,32 +574,120 @@ class OrderBookState:
573
574
 
574
575
 
575
576
  class OrderBookStateManager:
576
- def __init__(self):
577
+ """
578
+ Manages orderbook state with efficient updates and lookups.
579
+
580
+ Uses SortedDict for maintaining price levels in sorted order without
581
+ explicit sorting on each access. Pre-allocates numpy buffers to avoid
582
+ allocation overhead on frequent orderbook generation.
583
+
584
+ Args:
585
+ max_levels: Maximum number of orderbook levels to support (default: 1000)
586
+ Used for buffer pre-allocation to avoid repeated allocations
587
+ """
588
+
589
+ def __init__(self, max_levels: int = 1000):
590
+ self.time = None
591
+ self.bids: SortedDict = SortedDict() # Price -> Size (maintained in sorted order)
592
+ self.asks: SortedDict = SortedDict() # Price -> Size (maintained in sorted order)
593
+ self.max_levels = max_levels
594
+
595
+ # Pre-allocate buffers to avoid allocation overhead
596
+ # These are reused across get_orderbook() calls
597
+ self._bids_buffer = np.zeros(max_levels, dtype=np.float64)
598
+ self._asks_buffer = np.zeros(max_levels, dtype=np.float64)
599
+
600
+ def reset(self):
601
+ """
602
+ Reset orderbook state to initial empty state.
603
+
604
+ This method clears all bid/ask price levels and resets the timestamp.
605
+ Should be called when the WebSocket connection is reestablished to ensure
606
+ clean state before processing new updates.
607
+ """
608
+ # Recreate SortedDict instances to ensure clean state
609
+ self.bids = SortedDict()
610
+ self.asks = SortedDict()
577
611
  self.time = None
578
- self.bids = {}
579
- self.asks = {}
580
612
 
581
613
  def get_state(self):
582
- bids = sorted(self.bids.items(), key=lambda x: x[0])
583
- asks = sorted(self.asks.items(), key=lambda x: x[0])
614
+ """
615
+ Get current orderbook state as OrderBookState object.
616
+
617
+ Returns sorted price levels from SortedDict (no explicit sorting needed).
618
+ """
619
+ # SortedDict maintains sorted order, just convert to list
620
+ bids = list(self.bids.items())
621
+ asks = list(self.asks.items())
584
622
  return OrderBookState(bids=bids, asks=asks)
585
623
 
586
624
  def get_orderbook(self, tick_size: float, levels: int) -> OrderBook | None:
625
+ """
626
+ Generate OrderBook from current state with aggregation.
627
+
628
+ Args:
629
+ tick_size: Price tick size for aggregation
630
+ levels: Number of price levels to include
631
+
632
+ Returns:
633
+ OrderBook object, or None if state is empty or invalid
634
+
635
+ Note:
636
+ Detects crossed orderbook (bid >= ask) which indicates corrupted state
637
+ from missed or out-of-order updates. Automatically resets state on detection.
638
+ """
587
639
  # Check if we have bids and asks
588
640
  if not self.bids or not self.asks:
589
641
  return None
590
642
 
591
- # Sort bids descending (highest first) and asks ascending (lowest first)
592
- sorted_bids = sorted(self.bids.items(), key=lambda x: x[0], reverse=True)
593
- sorted_asks = sorted(self.asks.items(), key=lambda x: x[0])
643
+ # Validate levels parameter
644
+ if levels > self.max_levels:
645
+ logger.warning(
646
+ f"Requested levels ({levels}) exceeds max_levels ({self.max_levels}). "
647
+ f"Using max_levels={self.max_levels}"
648
+ )
649
+ levels = self.max_levels
650
+
651
+ # Get best bid/ask for crossed orderbook detection
652
+ # SortedDict: keys are in ascending order
653
+ best_bid = self.bids.keys()[-1] # Highest bid (last key)
654
+ best_ask = self.asks.keys()[0] # Lowest ask (first key)
655
+
656
+ # Detect crossed orderbook (invalid state)
657
+ # This should rarely happen now with proactive cleaning in update_state()
658
+ if best_bid >= best_ask:
659
+ logger.error(
660
+ f"Crossed orderbook detected AFTER proactive cleaning: "
661
+ f"best_bid={best_bid:.2f} >= best_ask={best_ask:.2f}. "
662
+ f"This indicates extreme message disorder or logic bug. Resetting state."
663
+ )
664
+ self.reset()
665
+ return None
666
+
667
+ # Get bids descending (highest first) and asks ascending (lowest first)
668
+ # SortedDict maintains ascending order, so reverse bids
669
+ sorted_bids = list(reversed(self.bids.items()))[:levels]
670
+ sorted_asks = list(self.asks.items())[:levels]
671
+
672
+ if levels == 1:
673
+ return OrderBook(
674
+ time=time_as_nsec(self.time),
675
+ top_bid=best_bid,
676
+ top_ask=best_ask,
677
+ tick_size=tick_size,
678
+ bids=np.array([sorted_bids[0][1]]),
679
+ asks=np.array([sorted_asks[0][1]]),
680
+ )
594
681
 
595
- bids_buffer = np.zeros(levels, dtype=np.float64)
596
- asks_buffer = np.zeros(levels, dtype=np.float64)
682
+ # Clear pre-allocated buffers for requested levels
683
+ self._bids_buffer[:levels].fill(0.0)
684
+ self._asks_buffer[:levels].fill(0.0)
597
685
 
598
686
  # Apply accumulation to aggregate into uniform grid
687
+ # Use slices of pre-allocated buffers to avoid allocation
599
688
  top_bid_agg, bids_accumulated = accumulate_orderbook_levels(
600
689
  np.array(sorted_bids, dtype=np.float64),
601
- bids_buffer,
690
+ self._bids_buffer[:levels],
602
691
  tick_size,
603
692
  True,
604
693
  levels,
@@ -606,12 +695,13 @@ class OrderBookStateManager:
606
695
  )
607
696
  top_ask_agg, asks_accumulated = accumulate_orderbook_levels(
608
697
  np.array(sorted_asks, dtype=np.float64),
609
- asks_buffer,
698
+ self._asks_buffer[:levels],
610
699
  tick_size,
611
700
  False,
612
701
  levels,
613
702
  False, # is_bid=False, sizes_in_quoted=False
614
703
  )
704
+
615
705
  return OrderBook(
616
706
  time=time_as_nsec(self.time),
617
707
  top_bid=top_bid_agg,
@@ -622,13 +712,98 @@ class OrderBookStateManager:
622
712
  )
623
713
 
624
714
  def update_state(self, time: dt_64, bids: list[tuple[float, float]], asks: list[tuple[float, float]]):
715
+ """
716
+ Update orderbook state and proactively clean crossed levels.
717
+
718
+ Uses extreme prices from the UPDATE data (not state) for cross-checking:
719
+ - Highest bid in updates is the fresh price to check against asks
720
+ - Lowest ask in updates is the fresh price to check against bids
721
+
722
+ This ensures we only clean based on fresh data, not potentially stale state.
723
+
724
+ Args:
725
+ time: Timestamp of update
726
+ bids: List of (price, size) tuples for bid updates
727
+ asks: List of (price, size) tuples for ask updates
728
+ """
625
729
  self.time = time
626
- self._update_state(self.bids, bids)
627
- self._update_state(self.asks, asks)
628
730
 
629
- def _update_state(self, state: dict[float, float], updates: list[tuple[float, float]]):
630
- for price, size in updates:
731
+ # Find highest bid price in updates (excluding zero sizes)
732
+ highest_bid_update = None
733
+ if bids:
734
+ non_zero_bids = [price for price, size in bids if size > 0]
735
+ if non_zero_bids:
736
+ highest_bid_update = max(non_zero_bids)
737
+
738
+ # Apply all bid updates
739
+ for price, size in bids:
631
740
  if size == 0:
632
- state.pop(price, None)
741
+ self.bids.pop(price, None)
633
742
  else:
634
- state[price] = size
743
+ self.bids[price] = size
744
+
745
+ # Clean crossed asks using highest bid from UPDATE (not state)
746
+ if highest_bid_update is not None:
747
+ self._clean_crossed_asks(highest_bid_update)
748
+
749
+ # Find lowest ask price in updates (excluding zero sizes)
750
+ lowest_ask_update = None
751
+ if asks:
752
+ non_zero_asks = [price for price, size in asks if size > 0]
753
+ if non_zero_asks:
754
+ lowest_ask_update = min(non_zero_asks)
755
+
756
+ # Apply all ask updates
757
+ for price, size in asks:
758
+ if size == 0:
759
+ self.asks.pop(price, None)
760
+ else:
761
+ self.asks[price] = size
762
+
763
+ # Clean crossed bids using lowest ask from UPDATE (not state)
764
+ if lowest_ask_update is not None:
765
+ self._clean_crossed_bids(lowest_ask_update)
766
+
767
+ def _clean_crossed_asks(self, bid_price: float) -> int:
768
+ """
769
+ Remove all asks at or below the given bid price.
770
+
771
+ Args:
772
+ bid_price: Bid price from fresh update that may cross asks
773
+
774
+ Returns:
775
+ Number of crossed asks removed
776
+ """
777
+ if not self.asks:
778
+ return 0
779
+
780
+ # Use irange() to efficiently find asks <= bid_price
781
+ crossed_keys = list(self.asks.irange(maximum=bid_price, inclusive=(True, True)))
782
+
783
+ if crossed_keys:
784
+ for key in crossed_keys:
785
+ del self.asks[key]
786
+
787
+ return len(crossed_keys)
788
+
789
+ def _clean_crossed_bids(self, ask_price: float) -> int:
790
+ """
791
+ Remove all bids at or above the given ask price.
792
+
793
+ Args:
794
+ ask_price: Ask price from fresh update that may cross bids
795
+
796
+ Returns:
797
+ Number of crossed bids removed
798
+ """
799
+ if not self.bids:
800
+ return 0
801
+
802
+ # Use irange() to efficiently find bids >= ask_price
803
+ crossed_keys = list(self.bids.irange(minimum=ask_price, inclusive=(True, True)))
804
+
805
+ if crossed_keys:
806
+ for key in crossed_keys:
807
+ del self.bids[key]
808
+
809
+ return len(crossed_keys)