Qubx 0.7.8__tar.gz → 0.7.25__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.
Files changed (278) hide show
  1. {qubx-0.7.8 → qubx-0.7.25}/PKG-INFO +2 -1
  2. {qubx-0.7.8 → qubx-0.7.25}/pyproject.toml +2 -1
  3. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/account.py +8 -1
  4. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/broker.py +23 -32
  5. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/data.py +33 -7
  6. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/management.py +1 -1
  7. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/ome.py +5 -2
  8. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/runner.py +23 -11
  9. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/simulated_exchange.py +23 -0
  10. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/transfers.py +13 -8
  11. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/utils.py +9 -2
  12. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/commands.py +52 -25
  13. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/account.py +22 -10
  14. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/broker.py +51 -34
  15. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/data.py +17 -33
  16. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchange_manager.py +33 -79
  17. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +121 -108
  18. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +1 -1
  19. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/factory.py +6 -2
  20. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -4
  21. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/liquidation.py +7 -6
  22. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -12
  23. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -6
  24. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/orderbook.py +1 -3
  25. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/quote.py +1 -5
  26. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/trade.py +0 -4
  27. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/reader.py +361 -165
  28. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/utils.py +30 -41
  29. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/tardis/data.py +9 -3
  30. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/__init__.py +3 -4
  31. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/account.py +179 -98
  32. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/broker.py +201 -93
  33. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/client.py +13 -27
  34. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/constants.py +1 -1
  35. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/data.py +261 -326
  36. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/factory.py +12 -89
  37. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/__init__.py +0 -2
  38. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/orderbook.py +67 -16
  39. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/stats.py +6 -5
  40. qubx-0.7.25/src/qubx/connectors/xlighter/instruments.py +60 -0
  41. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/parsers.py +8 -2
  42. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/rate_limits.py +2 -2
  43. qubx-0.7.25/src/qubx/connectors/xlighter/reader.py +453 -0
  44. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/utils.py +22 -0
  45. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/websocket.py +2 -2
  46. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/account.py +220 -39
  47. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/basics.py +116 -12
  48. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/context.py +39 -39
  49. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/detectors/delisting.py +16 -5
  50. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/helpers.py +67 -4
  51. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/initializer.py +2 -1
  52. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/interfaces.py +236 -124
  53. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/loggers.py +14 -10
  54. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/metrics.py +42 -18
  55. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/processing.py +107 -51
  56. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/subscription.py +124 -37
  57. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/trading.py +69 -17
  58. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/universe.py +18 -22
  59. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/series.pyx +27 -6
  60. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/utils.pyx +21 -2
  61. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/composite.py +45 -15
  62. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/helpers.py +9 -5
  63. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/readers.py +9 -6
  64. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/storages/questdb.py +18 -2
  65. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/transformers.py +4 -36
  66. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/base.py +8 -1
  67. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/indicator.py +2 -10
  68. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/questdb.py +42 -13
  69. qubx-0.7.25/src/qubx/health/__init__.py +4 -0
  70. qubx-0.7.25/src/qubx/health/base.py +543 -0
  71. qubx-0.7.25/src/qubx/health/dummy.py +99 -0
  72. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/balance.py +36 -38
  73. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/interfaces.py +2 -2
  74. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/position.py +25 -24
  75. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/ta/indicators.pxd +44 -1
  76. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/ta/indicators.pyi +11 -0
  77. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/ta/indicators.pyx +335 -25
  78. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/misc.py +15 -0
  79. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/rate_limiter.py +3 -6
  80. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/_jupyter_runner.pyt +1 -1
  81. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/configs.py +20 -3
  82. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/runner.py +146 -19
  83. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/app.py +13 -1
  84. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/init_code.py +2 -2
  85. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/repl_output.py +9 -0
  86. qubx-0.7.25/src/qubx/utils/throttler.py +136 -0
  87. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/time.py +10 -2
  88. qubx-0.7.8/src/qubx/connectors/xlighter/handlers/quote.py +0 -158
  89. qubx-0.7.8/src/qubx/connectors/xlighter/instruments.py +0 -253
  90. qubx-0.7.8/src/qubx/connectors/xlighter/reader.py +0 -490
  91. qubx-0.7.8/src/qubx/core/deque.py +0 -182
  92. qubx-0.7.8/src/qubx/features/__init__.py +0 -14
  93. qubx-0.7.8/src/qubx/features/core.py +0 -254
  94. qubx-0.7.8/src/qubx/features/orderbook.py +0 -42
  95. qubx-0.7.8/src/qubx/features/price.py +0 -20
  96. qubx-0.7.8/src/qubx/features/trades.py +0 -105
  97. qubx-0.7.8/src/qubx/features/utils.py +0 -10
  98. qubx-0.7.8/src/qubx/health/__init__.py +0 -3
  99. qubx-0.7.8/src/qubx/health/base.py +0 -672
  100. {qubx-0.7.8 → qubx-0.7.25}/LICENSE +0 -0
  101. {qubx-0.7.8 → qubx-0.7.25}/README.md +0 -0
  102. {qubx-0.7.8 → qubx-0.7.25}/build.py +0 -0
  103. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/__init__.py +0 -0
  104. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/_nb_magic.py +0 -0
  105. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/__init__.py +0 -0
  106. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/optimization.py +0 -0
  107. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/sentinels.py +0 -0
  108. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/simulated_data.py +0 -0
  109. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/simulator.py +0 -0
  110. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/__init__.py +0 -0
  111. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/deploy.py +0 -0
  112. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/misc.py +0 -0
  113. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/release.py +0 -0
  114. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/tui.py +0 -0
  115. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/__init__.py +0 -0
  116. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  117. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  118. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  119. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  120. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  121. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  122. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  123. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  124. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  125. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  126. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  127. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  128. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  129. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  130. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  131. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  132. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  133. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  134. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  135. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  136. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/tardis/utils.py +0 -0
  137. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/extensions.py +0 -0
  138. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/base.py +0 -0
  139. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/trades.py +0 -0
  140. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/nonce.py +0 -0
  141. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/__init__.py +0 -0
  142. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/detectors/__init__.py +0 -0
  143. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/detectors/stale.py +0 -0
  144. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/errors.py +0 -0
  145. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/exceptions.py +0 -0
  146. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/lookups.py +0 -0
  147. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/__init__.py +0 -0
  148. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/market.py +0 -0
  149. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/utils.py +0 -0
  150. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/series.pxd +0 -0
  151. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/series.pyi +0 -0
  152. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/utils.pyi +0 -0
  153. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/__init__.py +0 -0
  154. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/containers.py +0 -0
  155. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/hft.py +0 -0
  156. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/registry.py +0 -0
  157. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/storage.py +0 -0
  158. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/storages/csv.py +0 -0
  159. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/storages/utils.py +0 -0
  160. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/tardis.py +0 -0
  161. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/__init__.py +0 -0
  162. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/composite.py +0 -0
  163. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/csv.py +0 -0
  164. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/inmemory.py +0 -0
  165. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/prometheus.py +0 -0
  166. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/__init__.py +0 -0
  167. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/composite.py +0 -0
  168. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/__init__.py +0 -0
  169. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/base.py +0 -0
  170. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/incremental.py +0 -0
  171. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/slack.py +0 -0
  172. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/target_position.py +0 -0
  173. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/redis_streams.py +0 -0
  174. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/slack.py +0 -0
  175. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/gathering/simplest.py +0 -0
  176. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/__init__.py +0 -0
  177. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/csv.py +0 -0
  178. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/factory.py +0 -0
  179. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/inmemory.py +0 -0
  180. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/mongo.py +0 -0
  181. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/math/__init__.py +0 -0
  182. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/math/stats.py +0 -0
  183. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/notifications/__init__.py +0 -0
  184. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/notifications/composite.py +0 -0
  185. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/notifications/slack.py +0 -0
  186. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/notifications/throttler.py +0 -0
  187. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/pandaz/__init__.py +0 -0
  188. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/pandaz/ta.py +0 -0
  189. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/pandaz/utils.py +0 -0
  190. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/_build.py +0 -0
  191. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/crypto-fees.ini +0 -0
  192. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  193. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  194. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  195. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  196. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  197. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  198. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  199. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  200. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  201. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  202. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  203. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restarts/__init__.py +0 -0
  204. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restarts/state_resolvers.py +0 -0
  205. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restarts/time_finders.py +0 -0
  206. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/__init__.py +0 -0
  207. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/factory.py +0 -0
  208. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/signal.py +0 -0
  209. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/state.py +0 -0
  210. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/utils.py +0 -0
  211. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/ta/__init__.py +0 -0
  212. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/__init__.py +0 -0
  213. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/base.py +0 -0
  214. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  215. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/config.yml.j2 +0 -0
  216. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  217. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  218. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  219. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  220. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  221. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/template.yml +0 -0
  222. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  223. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  224. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/config.yml.j2 +0 -0
  225. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  226. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  227. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  228. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/template.yml +0 -0
  229. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/__init__.py +0 -0
  230. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/advanced.py +0 -0
  231. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/composite.py +0 -0
  232. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/rebalancers.py +0 -0
  233. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/riskctrl.py +0 -0
  234. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/sizers.py +0 -0
  235. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/__init__.py +0 -0
  236. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/_pyxreloader.py +0 -0
  237. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/charting/lookinglass.py +0 -0
  238. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  239. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/charting/orderbook.py +0 -0
  240. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/collections.py +0 -0
  241. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/hft/__init__.py +0 -0
  242. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/hft/numba_utils.py +0 -0
  243. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/hft/orderbook.pyi +0 -0
  244. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/hft/orderbook.pyx +0 -0
  245. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/marketdata/binance.py +0 -0
  246. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/marketdata/ccxt.py +0 -0
  247. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/marketdata/dukas.py +0 -0
  248. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/nonce.py +0 -0
  249. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/ntp.py +0 -0
  250. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/numbers_utils.py +0 -0
  251. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/orderbook.py +0 -0
  252. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/__init__.py +0 -0
  253. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/dashboard.py +0 -0
  254. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/data.py +0 -0
  255. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/interfaces.py +0 -0
  256. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  257. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  258. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/questdb.py +0 -0
  259. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/ringbuffer.pxd +0 -0
  260. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/ringbuffer.pyi +0 -0
  261. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/ringbuffer.pyx +0 -0
  262. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/__init__.py +0 -0
  263. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/accounts.py +0 -0
  264. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/factory.py +0 -0
  265. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/kernel_service.py +0 -0
  266. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/__init__.py +0 -0
  267. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/handlers.py +0 -0
  268. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/kernel.py +0 -0
  269. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  270. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  271. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  272. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  273. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  274. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  275. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  276. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/slack.py +0 -0
  277. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/version.py +0 -0
  278. {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/websocket_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.7.8
3
+ Version: 0.7.25
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  License-File: LICENSE
6
6
  Author: Dmitry Marienko
@@ -58,6 +58,7 @@ Requires-Dist: textual-serve (>=1.0.0,<2.0.0)
58
58
  Requires-Dist: textual[syntax] (>=6.0.0,<7.0.0)
59
59
  Requires-Dist: toml (>=0.10.2,<0.11.0)
60
60
  Requires-Dist: tqdm
61
+ Requires-Dist: uvloop (>=0.22.1,<0.23.0)
61
62
  Requires-Dist: websockets (==15.0.1)
62
63
  Project-URL: Repository, https://github.com/xLydianSoftware/Qubx
63
64
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.7.8"
7
+ version = "0.7.25"
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"
@@ -79,6 +79,7 @@ textual-serve = "^1.0.0"
79
79
  rich = "^13.9.4"
80
80
  jinja2 = "^3.1.0"
81
81
  qubx-lighter-api = "^0.1.4"
82
+ uvloop = "^0.22.1"
82
83
 
83
84
  [tool.ruff.lint]
84
85
  extend-select = [ "I",]
@@ -8,6 +8,7 @@ from qubx.core.basics import (
8
8
  Timestamped,
9
9
  dt_64,
10
10
  )
11
+ from qubx.core.interfaces import IHealthMonitor
11
12
  from qubx.core.series import OrderBook, Quote, Trade, TradeArray
12
13
  from qubx.restorers import RestoredState
13
14
 
@@ -21,7 +22,9 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
21
22
  account_id: str,
22
23
  exchange: ISimulatedExchange,
23
24
  channel: CtrlChannel,
25
+ health_monitor: IHealthMonitor,
24
26
  base_currency: str,
27
+ exchange_name: str,
25
28
  initial_capital: float,
26
29
  restored_state: RestoredState | None = None,
27
30
  ) -> None:
@@ -29,6 +32,8 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
29
32
  account_id=account_id,
30
33
  time_provider=exchange.get_time_provider(),
31
34
  base_currency=base_currency,
35
+ health_monitor=health_monitor,
36
+ exchange=exchange_name,
32
37
  tcc=exchange.get_transaction_costs_calculator(),
33
38
  initial_capital=initial_capital,
34
39
  )
@@ -37,7 +42,9 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
37
42
  self._channel = channel
38
43
 
39
44
  if restored_state is not None:
40
- self._balances.update(restored_state.balances)
45
+ # Convert list of AssetBalance to dict for internal storage
46
+ for balance in restored_state.balances:
47
+ self._balances[balance.currency] = balance
41
48
  for instrument, position in restored_state.positions.items():
42
49
  _pos = self.get_position(instrument)
43
50
  _pos.reset_by_position(position)
@@ -5,6 +5,7 @@ from qubx.core.basics import (
5
5
  CtrlChannel,
6
6
  Instrument,
7
7
  Order,
8
+ OrderRequest,
8
9
  )
9
10
  from qubx.core.exceptions import BadRequest, OrderNotFound
10
11
  from qubx.core.interfaces import IBroker
@@ -32,18 +33,17 @@ class SimulatedBroker(IBroker):
32
33
  def is_simulated_trading(self) -> bool:
33
34
  return True
34
35
 
35
- def send_order(
36
- self,
37
- instrument: Instrument,
38
- order_side: str,
39
- order_type: str,
40
- amount: float,
41
- price: float | None = None,
42
- client_id: str | None = None,
43
- time_in_force: str = "gtc",
44
- **options,
45
- ) -> Order:
46
- # - place order at exchange and send exec report to data channel
36
+ def send_order(self, request: OrderRequest) -> Order:
37
+ """Submit order synchronously in simulation."""
38
+ instrument = request.instrument
39
+ order_side = request.side
40
+ order_type = request.order_type
41
+ amount = request.quantity
42
+ price = request.price
43
+ client_id = request.client_id
44
+ time_in_force = request.time_in_force
45
+ options = request.options
46
+
47
47
  self._send_execution_report(
48
48
  report := self._exchange.place_order(
49
49
  instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
@@ -51,18 +51,9 @@ class SimulatedBroker(IBroker):
51
51
  )
52
52
  return report.order
53
53
 
54
- def send_order_async(
55
- self,
56
- instrument: Instrument,
57
- order_side: str,
58
- order_type: str,
59
- amount: float,
60
- price: float | None = None,
61
- client_id: str | None = None,
62
- time_in_force: str = "gtc",
63
- **optional,
64
- ) -> None:
65
- self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
54
+ def send_order_async(self, request: OrderRequest) -> None:
55
+ """Submit order asynchronously (same as sync in simulation)."""
56
+ self.send_order(request)
66
57
 
67
58
  def cancel_order(self, order_id: str) -> bool:
68
59
  """Cancel an order synchronously and return success status."""
@@ -104,25 +95,25 @@ class SimulatedBroker(IBroker):
104
95
  raise OrderNotFound(f"Order {order_id} not found")
105
96
 
106
97
  # Validate that it's a limit order
107
- if existing_order.type.lower() != "limit":
98
+ if existing_order.type != "LIMIT":
108
99
  raise BadRequest(
109
100
  f"Order {order_id} is not a limit order (type: {existing_order.type}). Only limit orders can be updated."
110
101
  )
111
102
 
112
- # Cancel the existing order first
113
103
  self.cancel_order(order_id)
114
104
 
115
- # Create a new order with updated parameters, preserving original properties
116
- updated_order = self.send_order(
105
+ request = OrderRequest(
117
106
  instrument=existing_order.instrument,
118
- order_side=existing_order.side,
119
- order_type="limit",
120
- amount=abs(amount),
107
+ quantity=abs(amount),
121
108
  price=price,
122
- client_id=existing_order.client_id, # Preserve original client_id for tracking
109
+ order_type="LIMIT",
110
+ side=existing_order.side,
123
111
  time_in_force=existing_order.time_in_force or "gtc",
112
+ options={},
124
113
  )
125
114
 
115
+ updated_order = self.send_order(request)
116
+
126
117
  return updated_order
127
118
 
128
119
  def _send_execution_report(self, report: SimulatedExecutionReport | None):
@@ -1,5 +1,5 @@
1
1
  from collections import defaultdict
2
- from typing import Any, TypeVar
2
+ from typing import TypeVar
3
3
 
4
4
  import pandas as pd
5
5
 
@@ -74,16 +74,35 @@ class SimulatedDataProvider(IDataProvider):
74
74
  def is_simulation(self) -> bool:
75
75
  return True
76
76
 
77
+ def is_connected(self) -> bool:
78
+ """
79
+ Check if the data provider is currently connected to the exchange.
80
+
81
+ For simulated data provider, always returns True since data is loaded from files.
82
+
83
+ Returns:
84
+ bool: Always True for simulated data
85
+ """
86
+ return True
87
+
77
88
  def subscribe(self, subscription_type: str, instruments: set[Instrument], reset: bool) -> None:
78
89
  _new_instr = [i for i in instruments if not self.has_subscription(i, subscription_type)]
90
+
79
91
  self._data_source.add_instruments_for_subscription(subscription_type, list(instruments))
80
92
 
81
93
  # - provide historical data and last quote for subscribed instruments
82
94
  for i in _new_instr:
83
- # Check if the instrument was actually subscribed (not filtered out)
95
+ # - check if the instrument was actually subscribed (not filtered out)
84
96
  if not self.has_subscription(i, subscription_type):
85
97
  continue
86
98
 
99
+ # - notify simulating exchange that instrument is subscribed
100
+ self._account._exchange.on_subscribe(i)
101
+
102
+ # - we need to clear last quote as it can be staled
103
+ self._last_quotes.pop(i, None)
104
+
105
+ # - try to peek most recent market data
87
106
  h_data = self._data_source.peek_historical_data(i, subscription_type)
88
107
  if h_data:
89
108
  # _s_type = DataType.from_str(subscription_type)[0]
@@ -103,9 +122,16 @@ class SimulatedDataProvider(IDataProvider):
103
122
  def unsubscribe(self, subscription_type: str, instruments: set[Instrument] | Instrument | None = None) -> None:
104
123
  # logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
105
124
  if instruments is not None:
106
- self._data_source.remove_instruments_from_subscription(
107
- subscription_type, [instruments] if isinstance(instruments, Instrument) else list(instruments)
108
- )
125
+ _instruments = [instruments] if isinstance(instruments, Instrument) else list(instruments)
126
+ self._data_source.remove_instruments_from_subscription(subscription_type, _instruments)
127
+
128
+ # - Clear last quotes for unsubscribed instruments
129
+ for instr in _instruments:
130
+ # - clear last quote
131
+ self._last_quotes.pop(instr, None)
132
+
133
+ # - Notify simulating exchange that instrument is unsubscribed
134
+ self._account._exchange.on_unsubscribe(instr)
109
135
 
110
136
  def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
111
137
  return self._data_source.has_subscription(instrument, subscription_type)
@@ -174,11 +200,11 @@ class SimulatedDataProvider(IDataProvider):
174
200
  high_price = r.data["high"]
175
201
  low_price = r.data["low"]
176
202
  close_price = r.data["close"]
177
-
203
+
178
204
  # Skip this record if any OHLC value is None
179
205
  if open_price is None or high_price is None or low_price is None or close_price is None:
180
206
  continue
181
-
207
+
182
208
  bars.append(
183
209
  Bar(
184
210
  _b_ts_0,
@@ -382,7 +382,7 @@ class BacktestsResultsManager:
382
382
  _mtrx[_nm] = v.get("performance", {})
383
383
 
384
384
  _m_repr = pd.DataFrame.from_dict(_mtrx, orient="index")[
385
- ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
385
+ ["gain", "cagr", "sharpe", "qr", "mdd_pct", "mdd_usd", "fees", "execs"]
386
386
  ].astype(float)
387
387
  _m_repr = _m_repr.round(3)
388
388
  _m_repr = _m_repr.sort_values(by=sort_by, ascending=ascending) if sort_by else _m_repr
@@ -79,8 +79,11 @@ class OrdersManagementEngine:
79
79
  self.active_orders = dict()
80
80
  self.stop_orders = dict()
81
81
  self.bbo = None
82
- self.__order_id = 100000
83
- self.__trade_id = 100000
82
+ # - 2025-12-09: OME always started with fixed order_id / trade_id
83
+ # executions stopped to be accounted in portfolio when OME re-created
84
+ _start_id = int(time_provider.time().view("i8")) + 1000
85
+ self.__order_id = _start_id
86
+ self.__trade_id = _start_id
84
87
  self._fill_stops_at_price = fill_stop_order_at_price
85
88
  self._tick_size = instrument.tick_size
86
89
  self._last_update_time = np.datetime64(0, "ns")
@@ -16,6 +16,7 @@ from qubx.core.initializer import BasicStrategyInitializer
16
16
  from qubx.core.interfaces import (
17
17
  CtrlChannel,
18
18
  IDataProvider,
19
+ IHealthMonitor,
19
20
  IMetricEmitter,
20
21
  IStrategy,
21
22
  IStrategyContext,
@@ -26,6 +27,7 @@ from qubx.core.interfaces import (
26
27
  from qubx.core.loggers import StrategyLogging
27
28
  from qubx.core.lookups import lookup
28
29
  from qubx.data.helpers import CachedPrefetchReader
30
+ from qubx.health import DummyHealthMonitor
29
31
  from qubx.loggers.inmemory import InMemoryLogsWriter
30
32
  from qubx.pandaz.utils import _frame_to_str
31
33
  from qubx.utils.time import now_ns
@@ -140,18 +142,20 @@ class SimulationRunner:
140
142
 
141
143
  self._prefetch_aux_data()
142
144
 
143
- # Start the context
145
+ # - Apply warmup periods before the start
146
+ # - merge default warmups with strategy warmups (strategy warmups take precedence)
147
+ _merged_warmups = {
148
+ **(self.data_config.default_warmups or {}),
149
+ **self.ctx.initializer.get_subscription_warmup(),
150
+ }
151
+ if _merged_warmups:
152
+ logger.debug(f"[<y>SimulationRunner</y>] :: Setting warmups: {_merged_warmups}")
153
+ self.ctx.set_warmup(_merged_warmups)
154
+
155
+ # - Start the context
144
156
  self.ctx.start()
145
157
 
146
- # Apply default warmup periods if strategy didn't set them
147
- for s in self.ctx.get_subscriptions():
148
- if not self.ctx.get_warmup(s) and (_d_wt := self.data_config.default_warmups.get(s)):
149
- logger.debug(
150
- f"[<y>SimulationRunner</y>] :: Strategy didn't set warmup period for <c>{s}</c> so default <c>{_d_wt}</c> will be used"
151
- )
152
- self.ctx.set_warmup({s: _d_wt})
153
-
154
- # Subscribe to any custom data types if needed
158
+ # - Subscribe to any custom data types if needed
155
159
  def _is_known_type(t: str):
156
160
  try:
157
161
  DataType(t)
@@ -387,9 +391,14 @@ class SimulationRunner:
387
391
 
388
392
  channel = SimulatedCtrlChannel("databus", sentinel=(None, None, None, None))
389
393
  simulated_clock = SimulatedTimeProvider(np.datetime64(self.start, "ns"))
394
+ health_monitor = DummyHealthMonitor()
390
395
 
391
396
  account = self._construct_account_processor(
392
- self.setup.exchanges, self.setup.commissions, simulated_clock, channel
397
+ self.setup.exchanges,
398
+ self.setup.commissions,
399
+ simulated_clock,
400
+ channel,
401
+ health_monitor,
393
402
  )
394
403
 
395
404
  scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
@@ -528,6 +537,7 @@ class SimulationRunner:
528
537
  commissions: str | dict[str, str | None] | None,
529
538
  time_provider: ITimeProvider,
530
539
  channel: CtrlChannel,
540
+ health_monitor: IHealthMonitor,
531
541
  ) -> CompositeAccountProcessor:
532
542
  _exchange_to_tcc = self._construct_tcc(exchanges, commissions)
533
543
  for tcc in _exchange_to_tcc.values():
@@ -555,7 +565,9 @@ class SimulationRunner:
555
565
  account_id=self.account_id,
556
566
  exchange=_exchange_to_simulated_exchange[exchange],
557
567
  channel=channel,
568
+ health_monitor=health_monitor,
558
569
  base_currency=self.setup.base_currency,
570
+ exchange_name=exchange,
559
571
  initial_capital=_initial_capital,
560
572
  )
561
573
 
@@ -47,6 +47,10 @@ class ISimulatedExchange:
47
47
 
48
48
  def get_open_orders(self, instrument: Instrument | None = None) -> dict[str, Order]: ...
49
49
 
50
+ def on_unsubscribe(self, instrument: Instrument) -> None: ...
51
+
52
+ def on_subscribe(self, instrument: Instrument) -> None: ...
53
+
50
54
  def process_market_data(
51
55
  self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray
52
56
  ) -> Generator[SimulatedExecutionReport]: ...
@@ -93,6 +97,7 @@ class BasicSimulatedExchange(ISimulatedExchange):
93
97
  """
94
98
 
95
99
  _ome: dict[Instrument, OrdersManagementEngine]
100
+ _half_tick_size: dict[Instrument, float]
96
101
  _order_to_instrument: dict[str, Instrument]
97
102
  _fill_stop_order_at_price: bool
98
103
  _time_provider: ITimeProvider
@@ -112,6 +117,7 @@ class BasicSimulatedExchange(ISimulatedExchange):
112
117
  self._fill_stop_order_at_price = accurate_stop_orders_execution
113
118
  self._time_provider = time_provider
114
119
  self._tcc = tcc
120
+
115
121
  if self._fill_stop_order_at_price:
116
122
  logger.info(
117
123
  f"[<y>{self.__class__.__name__}</y>] :: emulation of stop orders executions at exact price is ON"
@@ -193,6 +199,23 @@ class BasicSimulatedExchange(ISimulatedExchange):
193
199
 
194
200
  return {o.id: o for ome in self._ome.values() for o in ome.get_open_orders()}
195
201
 
202
+ def on_unsubscribe(self, instrument: Instrument) -> None:
203
+ """
204
+ Called when an instrument is unsubscribed.
205
+ """
206
+ # - clears the OME to remove stale BBO data.
207
+ self._ome.pop(instrument, None)
208
+ self._half_tick_size.pop(instrument, None)
209
+
210
+ def on_subscribe(self, instrument: Instrument) -> None:
211
+ """
212
+ Called when new instrument is subscribed.
213
+ """
214
+ # - just for sanity: remove OME for this instrument if it wasn't removed in on_unsubscribe call
215
+ if instrument in self._ome:
216
+ self._ome.pop(instrument, None)
217
+ self._half_tick_size.pop(instrument, None)
218
+
196
219
  def _get_ome(self, instrument: Instrument) -> OrdersManagementEngine:
197
220
  if (ome := self._ome.get(instrument)) is None:
198
221
  self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
@@ -63,11 +63,13 @@ class SimulationTransferManager(ITransferManager):
63
63
  raise ValueError(f"Exchange not found: {e}")
64
64
 
65
65
  # Validate sufficient funds
66
- from_balances = from_processor.get_balances()
67
- if currency not in from_balances:
66
+ from_balances_list = from_processor.get_balances()
67
+ from_balance = next((b for b in from_balances_list if b.currency == currency), None)
68
+
69
+ if from_balance is None:
68
70
  raise ValueError(f"Currency '{currency}' not found in {from_exchange}")
69
71
 
70
- available = from_balances[currency].free
72
+ available = from_balance.free
71
73
  if available < amount:
72
74
  raise ValueError(
73
75
  f"Insufficient funds in {from_exchange}: "
@@ -75,12 +77,15 @@ class SimulationTransferManager(ITransferManager):
75
77
  )
76
78
 
77
79
  # Execute transfer (instant balance manipulation)
78
- from_balances[currency].total -= amount
79
- from_balances[currency].free -= amount
80
+ from_balance.total -= amount
81
+ from_balance.free -= amount
82
+
83
+ to_balances_list = to_processor.get_balances()
84
+ to_balance = next((b for b in to_balances_list if b.currency == currency), None)
80
85
 
81
- to_balances = to_processor.get_balances()
82
- to_balances[currency].total += amount
83
- to_balances[currency].free += amount
86
+ total_amount = to_balance.total + amount if to_balance is not None else amount
87
+ locked_amount = to_balance.locked if to_balance is not None else 0
88
+ to_processor.update_balance(currency, total_amount, locked_amount, to_exchange)
84
89
 
85
90
  # Record transfer
86
91
  transfer_record = {
@@ -746,8 +746,15 @@ def _detect_defaults_from_subscriptions(
746
746
  _out_tf_tdelta, open_close_time_indent_secs
747
747
  )
748
748
 
749
- # - default warmups
750
- _warmups = {str(_base_subscr): time_delta_to_str(_get_default_warmup_period(_base_subscr, _in_base_tf).asm8.item())}
749
+ # - default warmups - populate for all subscription types in readers
750
+ _warmups = {}
751
+ for _sub_type in _t_readers.keys():
752
+ _sub_tf = _tf(_sub_type) # - extract timeframe if exists (None for quote/trade/orderbook)
753
+ _warmups[str(_sub_type)] = time_delta_to_str(_get_default_warmup_period(str(_sub_type), _sub_tf).asm8.item())
754
+
755
+ # - ensure base subscription has warmup (in case it's not in readers)
756
+ if str(_base_subscr) not in _warmups:
757
+ _warmups[str(_base_subscr)] = time_delta_to_str(_get_default_warmup_period(_base_subscr, _in_base_tf).asm8.item())
751
758
 
752
759
  return SimulationDataConfig(
753
760
  _default_trigger_schedule,
@@ -34,8 +34,9 @@ def main(debug: bool, debug_port: int, log_level: str):
34
34
  """
35
35
  # Suppress syntax warnings from AST parsing during import resolution
36
36
  import warnings
37
+
37
38
  warnings.filterwarnings("ignore", category=SyntaxWarning)
38
-
39
+
39
40
  os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
40
41
  log_level = log_level.upper() if not debug else "DEBUG"
41
42
 
@@ -69,23 +70,29 @@ def main(debug: bool, debug_port: int, log_level: str):
69
70
  @click.option(
70
71
  "--jupyter", "-j", is_flag=True, default=False, help="Run strategy in jupyter console.", show_default=True
71
72
  )
73
+ @click.option("--textual", "-t", is_flag=True, default=False, help="Run strategy in textual TUI.", show_default=True)
72
74
  @click.option(
73
- "--textual", "-t", is_flag=True, default=False, help="Run strategy in textual TUI.", show_default=True
74
- )
75
- @click.option(
76
- "--textual-dev", is_flag=True, default=False, help="Enable Textual dev mode (use with 'textual console').", show_default=True
77
- )
78
- @click.option(
79
- "--textual-web", is_flag=True, default=False, help="Serve Textual app in web browser.", show_default=True
80
- )
81
- @click.option(
82
- "--textual-port", type=int, default=None, help="Port for Textual (web server: 8000, devtools: 8081).", show_default=False
75
+ "--textual-dev",
76
+ is_flag=True,
77
+ default=False,
78
+ help="Enable Textual dev mode (use with 'textual console').",
79
+ show_default=True,
83
80
  )
81
+ @click.option("--textual-web", is_flag=True, default=False, help="Serve Textual app in web browser.", show_default=True)
84
82
  @click.option(
85
- "--textual-host", type=str, default="0.0.0.0", help="Host for Textual web server.", show_default=True
83
+ "--textual-port",
84
+ type=int,
85
+ default=None,
86
+ help="Port for Textual (web server: 8000, devtools: 8081).",
87
+ show_default=False,
86
88
  )
89
+ @click.option("--textual-host", type=str, default="0.0.0.0", help="Host for Textual web server.", show_default=True)
87
90
  @click.option(
88
- "--kernel-only", is_flag=True, default=False, help="Start kernel without UI (returns connection file).", show_default=True
91
+ "--kernel-only",
92
+ is_flag=True,
93
+ default=False,
94
+ help="Start kernel without UI (returns connection file).",
95
+ show_default=True,
89
96
  )
90
97
  @click.option(
91
98
  "--connect", type=Path, default=None, help="Connect to existing kernel via connection file.", show_default=False
@@ -94,7 +101,21 @@ def main(debug: bool, debug_port: int, log_level: str):
94
101
  "--restore", "-r", is_flag=True, default=False, help="Restore strategy state from previous run.", show_default=True
95
102
  )
96
103
  @click.option("--no-color", is_flag=True, default=False, help="Disable colored logging output.", show_default=True)
97
- def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool, textual: bool, textual_dev: bool, textual_web: bool, textual_port: int | None, textual_host: str, kernel_only: bool, connect: Path | None, restore: bool, no_color: bool):
104
+ def run(
105
+ config_file: Path,
106
+ account_file: Path | None,
107
+ paper: bool,
108
+ jupyter: bool,
109
+ textual: bool,
110
+ textual_dev: bool,
111
+ textual_web: bool,
112
+ textual_port: int | None,
113
+ textual_host: str,
114
+ kernel_only: bool,
115
+ connect: Path | None,
116
+ restore: bool,
117
+ no_color: bool,
118
+ ):
98
119
  """
99
120
  Starts the strategy with the given configuration file. If paper mode is enabled, account is not required.
100
121
 
@@ -138,6 +159,7 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
138
159
  # Keep the process alive until interrupted
139
160
  try:
140
161
  import signal
162
+
141
163
  signal.pause()
142
164
  except KeyboardInterrupt:
143
165
  click.echo("\nShutting down kernel...")
@@ -152,7 +174,9 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
152
174
  if jupyter:
153
175
  run_strategy_yaml_in_jupyter(config_file, account_file, paper, restore)
154
176
  elif textual:
155
- run_strategy_yaml_in_textual(config_file, account_file, paper, restore, textual_dev, textual_web, textual_port, textual_host, connect)
177
+ run_strategy_yaml_in_textual(
178
+ config_file, account_file, paper, restore, textual_dev, textual_web, textual_port, textual_host, connect
179
+ )
156
180
  else:
157
181
  logo()
158
182
  run_strategy_yaml(config_file, account_file, paper=paper, restore=restore, blocking=True, no_color=no_color)
@@ -169,7 +193,10 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
169
193
  @click.option(
170
194
  "--output", "-o", default="results", type=str, help="Output directory for simulation results.", show_default=True
171
195
  )
172
- def simulate(config_file: Path, start: str | None, end: str | None, output: str | None):
196
+ @click.option(
197
+ "--report", "-r", default=None, type=str, help="Output directory for simulation reports.", show_default=True
198
+ )
199
+ def simulate(config_file: Path, start: str | None, end: str | None, output: str | None, report: str | None):
173
200
  """
174
201
  Simulates the strategy with the given configuration file.
175
202
  """
@@ -179,7 +206,7 @@ def simulate(config_file: Path, start: str | None, end: str | None, output: str
179
206
  add_project_to_system_path()
180
207
  add_project_to_system_path(str(config_file.parent))
181
208
  logo()
182
- simulate_strategy(config_file, output, start, end)
209
+ simulate_strategy(config_file, output, start, end, report)
183
210
 
184
211
 
185
212
  @main.command()
@@ -457,32 +484,32 @@ def init(
457
484
  ):
458
485
  """
459
486
  Create a new strategy from a template.
460
-
487
+
461
488
  This command generates a complete strategy project structure with:
462
489
  - Strategy class implementing IStrategy interface
463
490
  - Configuration file for qubx run command
464
491
  - Package structure for proper imports
465
-
492
+
466
493
  The generated strategy can be run immediately with:
467
494
  poetry run qubx run --config config.yml --paper
468
495
  """
469
496
  from qubx.templates import TemplateError, TemplateManager
470
-
497
+
471
498
  try:
472
499
  manager = TemplateManager()
473
-
500
+
474
501
  if list_templates:
475
502
  templates = manager.list_templates()
476
503
  if not templates:
477
504
  click.echo("No templates available.")
478
505
  return
479
-
506
+
480
507
  click.echo("Available templates:")
481
508
  for template_name, metadata in templates.items():
482
509
  description = metadata.get("description", "No description")
483
510
  click.echo(f" {template_name:<15} - {description}")
484
511
  return
485
-
512
+
486
513
  # Generate strategy
487
514
  strategy_path = manager.generate_strategy(
488
515
  template_name=template if not template_path else None,
@@ -493,7 +520,7 @@ def init(
493
520
  symbols=symbols,
494
521
  timeframe=timeframe,
495
522
  )
496
-
523
+
497
524
  click.echo(f"✅ Strategy '{name}' created successfully!")
498
525
  click.echo(f"📁 Location: {strategy_path}")
499
526
  click.echo()
@@ -503,7 +530,7 @@ def init(
503
530
  click.echo()
504
531
  click.echo("To run in Jupyter mode:")
505
532
  click.echo(" ./jpaper.sh")
506
-
533
+
507
534
  except TemplateError as e:
508
535
  click.echo(f"❌ Template error: {e}", err=True)
509
536
  raise click.Abort()