Qubx 0.6.64__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 (292) hide show
  1. {qubx-0.6.64 → qubx-0.7.25}/PKG-INFO +58 -6
  2. {qubx-0.6.64 → qubx-0.7.25}/README.md +48 -3
  3. {qubx-0.6.64 → qubx-0.7.25}/pyproject.toml +12 -3
  4. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/__init__.py +1 -1
  5. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/account.py +9 -2
  6. qubx-0.7.25/src/qubx/backtester/broker.py +128 -0
  7. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/data.py +64 -11
  8. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/management.py +18 -5
  9. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/ome.py +5 -2
  10. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/runner.py +164 -33
  11. qubx-0.7.25/src/qubx/backtester/sentinels.py +23 -0
  12. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/simulated_data.py +23 -9
  13. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/simulated_exchange.py +29 -3
  14. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/simulator.py +54 -7
  15. qubx-0.7.25/src/qubx/backtester/transfers.py +151 -0
  16. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/utils.py +58 -18
  17. qubx-0.7.25/src/qubx/cli/commands.py +607 -0
  18. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/cli/release.py +351 -26
  19. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/account.py +62 -29
  20. qubx-0.7.25/src/qubx/connectors/ccxt/adapters/__init__.py +7 -0
  21. qubx-0.7.25/src/qubx/connectors/ccxt/adapters/polling_adapter.py +247 -0
  22. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/broker.py +223 -52
  23. qubx-0.7.25/src/qubx/connectors/ccxt/connection_manager.py +319 -0
  24. qubx-0.7.25/src/qubx/connectors/ccxt/data.py +337 -0
  25. qubx-0.7.25/src/qubx/connectors/ccxt/exchange_manager.py +265 -0
  26. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/__init__.py +36 -0
  27. qubx-0.7.25/src/qubx/connectors/ccxt/exchanges/base.py +63 -0
  28. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +359 -127
  29. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +3 -1
  30. qubx-0.7.25/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +7 -0
  31. qubx-0.7.25/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +75 -0
  32. qubx-0.7.25/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +306 -0
  33. qubx-0.7.25/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +602 -0
  34. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +4 -2
  35. qubx-0.7.25/src/qubx/connectors/ccxt/factory.py +177 -0
  36. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/__init__.py +29 -0
  37. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/base.py +93 -0
  38. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/factory.py +123 -0
  39. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/funding_rate.py +213 -0
  40. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/liquidation.py +93 -0
  41. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/ohlc.py +377 -0
  42. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/open_interest.py +201 -0
  43. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/orderbook.py +219 -0
  44. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/quote.py +99 -0
  45. qubx-0.7.25/src/qubx/connectors/ccxt/handlers/trade.py +203 -0
  46. qubx-0.7.25/src/qubx/connectors/ccxt/reader.py +869 -0
  47. qubx-0.7.25/src/qubx/connectors/ccxt/subscription_config.py +87 -0
  48. qubx-0.7.25/src/qubx/connectors/ccxt/subscription_manager.py +337 -0
  49. qubx-0.7.25/src/qubx/connectors/ccxt/subscription_orchestrator.py +370 -0
  50. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/utils.py +129 -23
  51. qubx-0.7.25/src/qubx/connectors/ccxt/warmup_service.py +122 -0
  52. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/tardis/data.py +15 -9
  53. qubx-0.7.25/src/qubx/connectors/xlighter/__init__.py +82 -0
  54. qubx-0.7.25/src/qubx/connectors/xlighter/account.py +557 -0
  55. qubx-0.7.25/src/qubx/connectors/xlighter/broker.py +803 -0
  56. qubx-0.7.25/src/qubx/connectors/xlighter/client.py +414 -0
  57. qubx-0.7.25/src/qubx/connectors/xlighter/constants.py +129 -0
  58. qubx-0.7.25/src/qubx/connectors/xlighter/data.py +725 -0
  59. qubx-0.7.25/src/qubx/connectors/xlighter/extensions.py +248 -0
  60. qubx-0.7.25/src/qubx/connectors/xlighter/factory.py +239 -0
  61. qubx-0.7.25/src/qubx/connectors/xlighter/handlers/__init__.py +13 -0
  62. qubx-0.7.25/src/qubx/connectors/xlighter/handlers/base.py +116 -0
  63. qubx-0.7.25/src/qubx/connectors/xlighter/handlers/orderbook.py +374 -0
  64. qubx-0.7.25/src/qubx/connectors/xlighter/handlers/stats.py +352 -0
  65. qubx-0.7.25/src/qubx/connectors/xlighter/handlers/trades.py +146 -0
  66. qubx-0.7.25/src/qubx/connectors/xlighter/instruments.py +60 -0
  67. qubx-0.7.25/src/qubx/connectors/xlighter/nonce.py +21 -0
  68. qubx-0.7.25/src/qubx/connectors/xlighter/parsers.py +657 -0
  69. qubx-0.7.25/src/qubx/connectors/xlighter/rate_limits.py +96 -0
  70. qubx-0.7.25/src/qubx/connectors/xlighter/reader.py +453 -0
  71. qubx-0.7.25/src/qubx/connectors/xlighter/utils.py +303 -0
  72. qubx-0.7.25/src/qubx/connectors/xlighter/websocket.py +490 -0
  73. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/account.py +310 -55
  74. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/basics.py +302 -45
  75. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/context.py +330 -85
  76. qubx-0.7.25/src/qubx/core/detectors/__init__.py +4 -0
  77. qubx-0.7.25/src/qubx/core/detectors/delisting.py +92 -0
  78. qubx-0.7.25/src/qubx/core/detectors/stale.py +419 -0
  79. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/exceptions.py +4 -0
  80. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/helpers.py +188 -16
  81. qubx-0.7.25/src/qubx/core/initializer.py +249 -0
  82. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/interfaces.py +799 -139
  83. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/loggers.py +16 -10
  84. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/lookups.py +1 -1
  85. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/metrics.py +668 -46
  86. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/mixins/market.py +30 -4
  87. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/mixins/processing.py +281 -62
  88. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/mixins/subscription.py +150 -43
  89. qubx-0.7.25/src/qubx/core/mixins/trading.py +547 -0
  90. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/mixins/universe.py +26 -12
  91. qubx-0.7.25/src/qubx/core/mixins/utils.py +4 -0
  92. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/series.pxd +43 -8
  93. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/series.pyi +95 -3
  94. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/series.pyx +460 -68
  95. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/utils.pyx +21 -2
  96. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/data/__init__.py +12 -1
  97. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/data/composite.py +322 -19
  98. qubx-0.7.25/src/qubx/data/containers.py +234 -0
  99. qubx-0.7.25/src/qubx/data/helpers.py +2080 -0
  100. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/data/readers.py +230 -51
  101. qubx-0.7.25/src/qubx/data/registry.py +242 -0
  102. qubx-0.7.25/src/qubx/data/storage.py +74 -0
  103. qubx-0.7.25/src/qubx/data/storages/csv.py +273 -0
  104. qubx-0.7.25/src/qubx/data/storages/questdb.py +596 -0
  105. qubx-0.7.25/src/qubx/data/storages/utils.py +115 -0
  106. qubx-0.7.25/src/qubx/data/transformers.py +494 -0
  107. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/emitters/base.py +32 -16
  108. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/emitters/composite.py +30 -1
  109. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/emitters/csv.py +2 -1
  110. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/emitters/indicator.py +16 -17
  111. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/emitters/inmemory.py +5 -4
  112. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/emitters/prometheus.py +2 -2
  113. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/emitters/questdb.py +136 -35
  114. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/exporters/formatters/__init__.py +8 -1
  115. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/exporters/formatters/base.py +0 -1
  116. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/exporters/formatters/incremental.py +8 -8
  117. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/exporters/formatters/slack.py +6 -4
  118. qubx-0.7.25/src/qubx/exporters/formatters/target_position.py +76 -0
  119. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/exporters/redis_streams.py +27 -7
  120. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/exporters/slack.py +59 -74
  121. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/gathering/simplest.py +5 -3
  122. qubx-0.7.25/src/qubx/health/__init__.py +4 -0
  123. qubx-0.7.25/src/qubx/health/base.py +543 -0
  124. qubx-0.7.25/src/qubx/health/dummy.py +99 -0
  125. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/loggers/csv.py +4 -4
  126. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/notifications/__init__.py +5 -5
  127. qubx-0.7.25/src/qubx/notifications/composite.py +83 -0
  128. qubx-0.7.25/src/qubx/notifications/slack.py +189 -0
  129. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/pandaz/ta.py +235 -34
  130. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/pandaz/utils.py +1 -1
  131. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/crypto-fees.ini +8 -1
  132. qubx-0.7.25/src/qubx/resources/instruments/hyperliquid-spot.json +4204 -0
  133. qubx-0.7.25/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +4424 -0
  134. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restarts/state_resolvers.py +10 -5
  135. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restorers/balance.py +36 -38
  136. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restorers/interfaces.py +2 -2
  137. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restorers/position.py +25 -24
  138. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restorers/signal.py +2 -2
  139. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/ta/indicators.pxd +123 -3
  140. qubx-0.7.25/src/qubx/ta/indicators.pyi +97 -0
  141. qubx-0.7.25/src/qubx/ta/indicators.pyx +1866 -0
  142. qubx-0.7.25/src/qubx/templates/__init__.py +5 -0
  143. qubx-0.7.25/src/qubx/templates/base.py +166 -0
  144. qubx-0.7.25/src/qubx/templates/project/accounts.toml.j2 +22 -0
  145. qubx-0.7.25/src/qubx/templates/project/config.yml.j2 +33 -0
  146. qubx-0.7.25/src/qubx/templates/project/jlive.sh.j2 +43 -0
  147. qubx-0.7.25/src/qubx/templates/project/jpaper.sh.j2 +6 -0
  148. qubx-0.7.25/src/qubx/templates/project/pyproject.toml.j2 +18 -0
  149. qubx-0.7.25/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +5 -0
  150. qubx-0.7.25/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +170 -0
  151. qubx-0.7.25/src/qubx/templates/project/template.yml +20 -0
  152. qubx-0.7.25/src/qubx/templates/simple/__init__.py.j2 +5 -0
  153. qubx-0.7.25/src/qubx/templates/simple/accounts.toml.j2 +22 -0
  154. qubx-0.7.25/src/qubx/templates/simple/config.yml.j2 +30 -0
  155. qubx-0.7.25/src/qubx/templates/simple/jlive.sh.j2 +43 -0
  156. qubx-0.7.25/src/qubx/templates/simple/jpaper.sh.j2 +6 -0
  157. qubx-0.7.25/src/qubx/templates/simple/strategy.py.j2 +95 -0
  158. qubx-0.7.25/src/qubx/templates/simple/template.yml +20 -0
  159. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/trackers/__init__.py +2 -0
  160. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/trackers/advanced.py +41 -4
  161. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/trackers/riskctrl.py +494 -4
  162. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/trackers/sizers.py +65 -2
  163. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/charting/lookinglass.py +145 -15
  164. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/charting/mpl_helpers.py +128 -0
  165. qubx-0.7.25/src/qubx/utils/charting/orderbook.py +314 -0
  166. qubx-0.7.25/src/qubx/utils/hft/__init__.py +5 -0
  167. qubx-0.7.25/src/qubx/utils/hft/numba_utils.py +12 -0
  168. qubx-0.7.25/src/qubx/utils/hft/orderbook.pyi +177 -0
  169. qubx-0.7.25/src/qubx/utils/hft/orderbook.pyx +416 -0
  170. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/marketdata/ccxt.py +44 -7
  171. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/misc.py +52 -8
  172. qubx-0.7.25/src/qubx/utils/nonce.py +53 -0
  173. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/orderbook.py +1 -1
  174. qubx-0.7.25/src/qubx/utils/questdb.py +141 -0
  175. qubx-0.7.25/src/qubx/utils/rate_limiter.py +222 -0
  176. qubx-0.7.25/src/qubx/utils/ringbuffer.pxd +17 -0
  177. qubx-0.7.25/src/qubx/utils/ringbuffer.pyi +197 -0
  178. qubx-0.7.25/src/qubx/utils/ringbuffer.pyx +253 -0
  179. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/runner/_jupyter_runner.pyt +23 -12
  180. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/runner/accounts.py +30 -1
  181. qubx-0.7.25/src/qubx/utils/runner/configs.py +318 -0
  182. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/runner/factory.py +46 -14
  183. qubx-0.7.25/src/qubx/utils/runner/kernel_service.py +195 -0
  184. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/runner/runner.py +327 -39
  185. qubx-0.7.25/src/qubx/utils/runner/textual/__init__.py +177 -0
  186. qubx-0.7.25/src/qubx/utils/runner/textual/app.py +412 -0
  187. qubx-0.7.25/src/qubx/utils/runner/textual/handlers.py +101 -0
  188. qubx-0.7.25/src/qubx/utils/runner/textual/init_code.py +301 -0
  189. qubx-0.7.25/src/qubx/utils/runner/textual/kernel.py +269 -0
  190. qubx-0.7.25/src/qubx/utils/runner/textual/styles.tcss +134 -0
  191. qubx-0.7.25/src/qubx/utils/runner/textual/widgets/__init__.py +10 -0
  192. qubx-0.7.25/src/qubx/utils/runner/textual/widgets/command_input.py +105 -0
  193. qubx-0.7.25/src/qubx/utils/runner/textual/widgets/debug_log.py +97 -0
  194. qubx-0.7.25/src/qubx/utils/runner/textual/widgets/orders_table.py +242 -0
  195. qubx-0.7.25/src/qubx/utils/runner/textual/widgets/positions_table.py +330 -0
  196. qubx-0.7.25/src/qubx/utils/runner/textual/widgets/quotes_table.py +225 -0
  197. qubx-0.7.25/src/qubx/utils/runner/textual/widgets/repl_output.py +111 -0
  198. qubx-0.7.25/src/qubx/utils/slack.py +339 -0
  199. qubx-0.7.25/src/qubx/utils/throttler.py +136 -0
  200. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/time.py +168 -1
  201. qubx-0.7.25/src/qubx/utils/websocket_manager.py +450 -0
  202. qubx-0.6.64/src/qubx/backtester/broker.py +0 -84
  203. qubx-0.6.64/src/qubx/cli/commands.py +0 -279
  204. qubx-0.6.64/src/qubx/connectors/ccxt/data.py +0 -823
  205. qubx-0.6.64/src/qubx/connectors/ccxt/factory.py +0 -104
  206. qubx-0.6.64/src/qubx/connectors/ccxt/reader.py +0 -237
  207. qubx-0.6.64/src/qubx/core/deque.py +0 -182
  208. qubx-0.6.64/src/qubx/core/initializer.py +0 -115
  209. qubx-0.6.64/src/qubx/core/mixins/trading.py +0 -201
  210. qubx-0.6.64/src/qubx/data/helpers.py +0 -443
  211. qubx-0.6.64/src/qubx/data/registry.py +0 -124
  212. qubx-0.6.64/src/qubx/features/__init__.py +0 -14
  213. qubx-0.6.64/src/qubx/features/core.py +0 -254
  214. qubx-0.6.64/src/qubx/features/orderbook.py +0 -42
  215. qubx-0.6.64/src/qubx/features/price.py +0 -20
  216. qubx-0.6.64/src/qubx/features/trades.py +0 -105
  217. qubx-0.6.64/src/qubx/features/utils.py +0 -10
  218. qubx-0.6.64/src/qubx/health/__init__.py +0 -3
  219. qubx-0.6.64/src/qubx/health/base.py +0 -668
  220. qubx-0.6.64/src/qubx/notifications/composite.py +0 -71
  221. qubx-0.6.64/src/qubx/notifications/slack.py +0 -213
  222. qubx-0.6.64/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -1
  223. qubx-0.6.64/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -1
  224. qubx-0.6.64/src/qubx/ta/indicators.pyi +0 -41
  225. qubx-0.6.64/src/qubx/ta/indicators.pyx +0 -784
  226. qubx-0.6.64/src/qubx/utils/questdb.py +0 -79
  227. qubx-0.6.64/src/qubx/utils/runner/configs.py +0 -137
  228. {qubx-0.6.64 → qubx-0.7.25}/LICENSE +0 -0
  229. {qubx-0.6.64 → qubx-0.7.25}/build.py +0 -0
  230. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/_nb_magic.py +0 -0
  231. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/__init__.py +0 -0
  232. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/backtester/optimization.py +0 -0
  233. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/cli/__init__.py +0 -0
  234. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/cli/deploy.py +0 -0
  235. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/cli/misc.py +0 -0
  236. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/cli/tui.py +0 -0
  237. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/__init__.py +0 -0
  238. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  239. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  240. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  241. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/connectors/tardis/utils.py +0 -0
  242. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/__init__.py +0 -0
  243. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/errors.py +0 -0
  244. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/mixins/__init__.py +0 -0
  245. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/core/utils.pyi +0 -0
  246. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/data/hft.py +0 -0
  247. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/data/tardis.py +0 -0
  248. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/emitters/__init__.py +0 -0
  249. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/exporters/__init__.py +0 -0
  250. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/exporters/composite.py +0 -0
  251. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/loggers/__init__.py +0 -0
  252. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/loggers/factory.py +0 -0
  253. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/loggers/inmemory.py +0 -0
  254. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/loggers/mongo.py +0 -0
  255. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/math/__init__.py +0 -0
  256. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/math/stats.py +0 -0
  257. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/notifications/throttler.py +0 -0
  258. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/pandaz/__init__.py +0 -0
  259. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/_build.py +0 -0
  260. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  261. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  262. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  263. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  264. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  265. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  266. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  267. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  268. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  269. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restarts/__init__.py +0 -0
  270. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restarts/time_finders.py +0 -0
  271. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restorers/__init__.py +0 -0
  272. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restorers/factory.py +0 -0
  273. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restorers/state.py +0 -0
  274. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/restorers/utils.py +0 -0
  275. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/ta/__init__.py +0 -0
  276. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/trackers/composite.py +0 -0
  277. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/trackers/rebalancers.py +0 -0
  278. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/__init__.py +0 -0
  279. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/_pyxreloader.py +0 -0
  280. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/collections.py +0 -0
  281. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/marketdata/binance.py +0 -0
  282. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/marketdata/dukas.py +0 -0
  283. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/ntp.py +0 -0
  284. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/numbers_utils.py +0 -0
  285. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/plotting/__init__.py +0 -0
  286. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/plotting/dashboard.py +0 -0
  287. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/plotting/data.py +0 -0
  288. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/plotting/interfaces.py +0 -0
  289. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  290. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  291. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/runner/__init__.py +0 -0
  292. {qubx-0.6.64 → qubx-0.7.25}/src/qubx/utils/version.py +0 -0
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.64
3
+ Version: 0.7.25
4
4
  Summary: Qubx - Quantitative Trading Framework
5
+ License-File: LICENSE
5
6
  Author: Dmitry Marienko
6
7
  Author-email: dmitry.marienko@xlydian.com
7
8
  Requires-Python: >=3.10,<4.0
@@ -10,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
10
11
  Classifier: Programming Language :: Python :: 3.11
11
12
  Classifier: Programming Language :: Python :: 3.12
12
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
13
15
  Requires-Dist: aiohttp (>=3.10.11,<3.11.0)
14
16
  Requires-Dist: ccxt (>=4.2.68,<5.0.0)
15
17
  Requires-Dist: croniter (>=2.0.5,<3.0.0)
@@ -19,6 +21,7 @@ Requires-Dist: dash-bootstrap-components (>=1.6.0,<2.0.0)
19
21
  Requires-Dist: gitpython (>=3.1.44,<4.0.0)
20
22
  Requires-Dist: importlib-metadata
21
23
  Requires-Dist: ipywidgets (>=8.1.5,<9.0.0)
24
+ Requires-Dist: jinja2 (>=3.1.0,<4.0.0)
22
25
  Requires-Dist: jupyter (>=1.1.1,<2.0.0)
23
26
  Requires-Dist: jupyter-console (>=6.6.3,<7.0.0)
24
27
  Requires-Dist: loguru (>=0.7.2,<0.8.0)
@@ -40,6 +43,7 @@ Requires-Dist: python-binance (>=1.0.19,<2.0.0)
40
43
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
41
44
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
42
45
  Requires-Dist: qubx-bitfinex-api (>=3.0.7,<4.0.0)
46
+ Requires-Dist: qubx-lighter-api (>=0.1.4,<0.2.0)
43
47
  Requires-Dist: questdb (>=2.0.3,<3.0.0)
44
48
  Requires-Dist: redis (>=5.2.1,<6.0.0)
45
49
  Requires-Dist: rich (>=13.9.4,<14.0.0)
@@ -49,9 +53,12 @@ Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
49
53
  Requires-Dist: stackprinter (>=0.2.10,<0.3.0)
50
54
  Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
51
55
  Requires-Dist: tabulate (>=0.9.0,<0.10.0)
52
- Requires-Dist: textual (>=0.88.0,<0.89.0)
56
+ Requires-Dist: textual-autocomplete (>=4.0.0,<5.0.0)
57
+ Requires-Dist: textual-serve (>=1.0.0,<2.0.0)
58
+ Requires-Dist: textual[syntax] (>=6.0.0,<7.0.0)
53
59
  Requires-Dist: toml (>=0.10.2,<0.11.0)
54
60
  Requires-Dist: tqdm
61
+ Requires-Dist: uvloop (>=0.22.1,<0.23.0)
55
62
  Requires-Dist: websockets (==15.0.1)
56
63
  Project-URL: Repository, https://github.com/xLydianSoftware/Qubx
57
64
  Description-Content-Type: text/markdown
@@ -69,6 +76,49 @@ Description-Content-Type: text/markdown
69
76
 
70
77
  Qubx is a next-generation quantitative trading framework designed for efficient backtesting and live trading. Built with Python, it offers a robust environment for developing, testing, and deploying trading strategies.
71
78
 
79
+ ## Quick Start
80
+
81
+ ### 1. Install Dependencies
82
+ ```bash
83
+ poetry install
84
+ ```
85
+
86
+ ### 2. Create a Strategy
87
+ ```bash
88
+ # Create a simple strategy template (default)
89
+ poetry run qubx init
90
+
91
+ # Or specify a name and symbols
92
+ poetry run qubx init --name my_strategy --symbols BTCUSDT,ETHUSDT
93
+ ```
94
+
95
+ ### 3. Run Your Strategy
96
+ ```bash
97
+ cd my_strategy
98
+
99
+ # Run in paper trading mode
100
+ poetry run qubx run config.yml --paper
101
+
102
+ # Or run in Jupyter mode for interactive development
103
+ ./jpaper.sh
104
+ ```
105
+
106
+ ### Available Templates
107
+ ```bash
108
+ # List available strategy templates
109
+ poetry run qubx init --list-templates
110
+
111
+ # Create strategy with full project structure and MACD example
112
+ poetry run qubx init --template project --name my_project
113
+ ```
114
+
115
+ ### Strategy Development Workflow
116
+ 1. **Initialize**: `poetry run qubx init` - Create strategy from template
117
+ 2. **Develop**: Edit `strategy.py` to implement your trading logic
118
+ 3. **Test**: `poetry run qubx run config.yml --paper` - Run in paper mode
119
+ 4. **Debug**: `./jpaper.sh` - Use Jupyter for interactive development
120
+ 5. **Deploy**: Configure for live trading when ready
121
+
72
122
  ## Features
73
123
 
74
124
  - 🚀 High-performance backtesting engine
@@ -119,11 +169,13 @@ qubx --help # Show all available commands
119
169
 
120
170
  Available commands:
121
171
 
122
- - `qubx deploy` - Deploy a strategy from a zip file
123
- - `qubx ls` - List all strategies in a directory
124
- - `qubx release` - Package a strategy into a zip file
172
+ - `qubx init` - Create a new strategy from template
125
173
  - `qubx run` - Start a strategy with given configuration
126
174
  - `qubx simulate` - Run strategy simulation
175
+ - `qubx ls` - List all strategies in a directory
176
+ - `qubx release` - Package a strategy into a zip file
177
+ - `qubx deploy` - Deploy a strategy from a zip file
178
+ - `qubx browse` - Browse backtest results using interactive TUI
127
179
 
128
180
  ## Development
129
181
 
@@ -11,6 +11,49 @@
11
11
 
12
12
  Qubx is a next-generation quantitative trading framework designed for efficient backtesting and live trading. Built with Python, it offers a robust environment for developing, testing, and deploying trading strategies.
13
13
 
14
+ ## Quick Start
15
+
16
+ ### 1. Install Dependencies
17
+ ```bash
18
+ poetry install
19
+ ```
20
+
21
+ ### 2. Create a Strategy
22
+ ```bash
23
+ # Create a simple strategy template (default)
24
+ poetry run qubx init
25
+
26
+ # Or specify a name and symbols
27
+ poetry run qubx init --name my_strategy --symbols BTCUSDT,ETHUSDT
28
+ ```
29
+
30
+ ### 3. Run Your Strategy
31
+ ```bash
32
+ cd my_strategy
33
+
34
+ # Run in paper trading mode
35
+ poetry run qubx run config.yml --paper
36
+
37
+ # Or run in Jupyter mode for interactive development
38
+ ./jpaper.sh
39
+ ```
40
+
41
+ ### Available Templates
42
+ ```bash
43
+ # List available strategy templates
44
+ poetry run qubx init --list-templates
45
+
46
+ # Create strategy with full project structure and MACD example
47
+ poetry run qubx init --template project --name my_project
48
+ ```
49
+
50
+ ### Strategy Development Workflow
51
+ 1. **Initialize**: `poetry run qubx init` - Create strategy from template
52
+ 2. **Develop**: Edit `strategy.py` to implement your trading logic
53
+ 3. **Test**: `poetry run qubx run config.yml --paper` - Run in paper mode
54
+ 4. **Debug**: `./jpaper.sh` - Use Jupyter for interactive development
55
+ 5. **Deploy**: Configure for live trading when ready
56
+
14
57
  ## Features
15
58
 
16
59
  - 🚀 High-performance backtesting engine
@@ -61,11 +104,13 @@ qubx --help # Show all available commands
61
104
 
62
105
  Available commands:
63
106
 
64
- - `qubx deploy` - Deploy a strategy from a zip file
65
- - `qubx ls` - List all strategies in a directory
66
- - `qubx release` - Package a strategy into a zip file
107
+ - `qubx init` - Create a new strategy from template
67
108
  - `qubx run` - Start a strategy with given configuration
68
109
  - `qubx simulate` - Run strategy simulation
110
+ - `qubx ls` - List all strategies in a directory
111
+ - `qubx release` - Package a strategy into a zip file
112
+ - `qubx deploy` - Deploy a strategy from a zip file
113
+ - `qubx browse` - Browse backtest results using interactive TUI
69
114
 
70
115
  ## Development
71
116
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.64"
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"
@@ -74,8 +74,12 @@ orjson = "^3.10.15"
74
74
  aiohttp = "~3.10.11"
75
75
  websockets = "15.0.1"
76
76
  qubx-bitfinex-api = "^3.0.7"
77
- textual = "^0.88.0"
77
+ textual-autocomplete = "^4.0.0"
78
+ textual-serve = "^1.0.0"
78
79
  rich = "^13.9.4"
80
+ jinja2 = "^3.1.0"
81
+ qubx-lighter-api = "^0.1.4"
82
+ uvloop = "^0.22.1"
79
83
 
80
84
  [tool.ruff.lint]
81
85
  extend-select = [ "I",]
@@ -87,7 +91,11 @@ asyncio_default_fixture_loop_scope = "function"
87
91
  pythonpath = [ "src",]
88
92
  markers = [ "integration: mark test as requiring external services like Redis", "e2e: mark test as requiring external exchange connections and API credentials",]
89
93
  addopts = "--disable-warnings"
90
- filterwarnings = [ "ignore:.*Jupyter is migrating.*:DeprecationWarning",]
94
+ filterwarnings = [ "ignore:.*Jupyter is migrating.*:DeprecationWarning", "ignore:coroutine.*AsyncMockMixin._execute_mock_call.*was never awaited:RuntimeWarning",]
95
+
96
+ [tool.poetry.dependencies.textual]
97
+ extras = [ "syntax",]
98
+ version = "^6.0.0"
91
99
 
92
100
  [tool.ruff.lint.extend-per-file-ignores]
93
101
  "*.ipynb" = [ "F405", "F401", "E701", "E402", "F403", "E401", "E702", "I001",]
@@ -119,6 +127,7 @@ pytest-mock = "^3.12.0"
119
127
  pytest-lazy-fixture = "^0.6.3"
120
128
  pytest-cov = "^4.1.0"
121
129
  mongomock = "^4.3.0"
130
+ pytest-textual-snapshot = "^1.1.0"
122
131
 
123
132
  [tool.poetry.group.k8.dependencies]
124
133
  prometheus-client = "^0.21.1"
@@ -186,7 +186,7 @@ if runtime_env() in ["notebook", "shell"]:
186
186
  return
187
187
 
188
188
  ipy = get_ipython()
189
- for a in [x for x in re.split(r"[\ ,;]", line.strip()) if x]:
189
+ for a in [x for x in re.split(r"[\s,;]", line.strip()) if x]:
190
190
  ipy.push({a: self._get_manager().Value(None, None)})
191
191
 
192
192
  # code to run
@@ -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,12 +42,14 @@ 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)
44
51
 
45
- def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
52
+ def get_orders(self, instrument: Instrument | None = None, exchange: str | None = None) -> dict[str, Order]:
46
53
  return self._exchange.get_open_orders(instrument)
47
54
 
48
55
  def get_position(self, instrument: Instrument) -> Position:
@@ -0,0 +1,128 @@
1
+ from qubx import logger
2
+ from qubx.backtester.ome import SimulatedExecutionReport
3
+ from qubx.backtester.simulated_exchange import ISimulatedExchange
4
+ from qubx.core.basics import (
5
+ CtrlChannel,
6
+ Instrument,
7
+ Order,
8
+ OrderRequest,
9
+ )
10
+ from qubx.core.exceptions import BadRequest, OrderNotFound
11
+ from qubx.core.interfaces import IBroker
12
+
13
+ from .account import SimulatedAccountProcessor
14
+
15
+
16
+ class SimulatedBroker(IBroker):
17
+ channel: CtrlChannel
18
+
19
+ _account: SimulatedAccountProcessor
20
+ _exchange: ISimulatedExchange
21
+
22
+ def __init__(
23
+ self,
24
+ channel: CtrlChannel,
25
+ account: SimulatedAccountProcessor,
26
+ simulated_exchange: ISimulatedExchange,
27
+ ) -> None:
28
+ self.channel = channel
29
+ self._account = account
30
+ self._exchange = simulated_exchange
31
+
32
+ @property
33
+ def is_simulated_trading(self) -> bool:
34
+ return True
35
+
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
+ self._send_execution_report(
48
+ report := self._exchange.place_order(
49
+ instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
50
+ )
51
+ )
52
+ return report.order
53
+
54
+ def send_order_async(self, request: OrderRequest) -> None:
55
+ """Submit order asynchronously (same as sync in simulation)."""
56
+ self.send_order(request)
57
+
58
+ def cancel_order(self, order_id: str) -> bool:
59
+ """Cancel an order synchronously and return success status."""
60
+ try:
61
+ self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
62
+ return order_update is not None
63
+ except OrderNotFound:
64
+ # Order was already cancelled or doesn't exist
65
+ logger.debug(f"Order {order_id} not found")
66
+ return False
67
+
68
+ def cancel_order_async(self, order_id: str) -> None:
69
+ """Cancel an order asynchronously (fire-and-forget)."""
70
+ # For simulation, async is same as sync since it's fast
71
+ self.cancel_order(order_id)
72
+
73
+ def cancel_orders(self, instrument: Instrument) -> None:
74
+ raise NotImplementedError("Not implemented yet")
75
+
76
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
77
+ """Update an existing limit order using cancel+recreate strategy.
78
+
79
+ Args:
80
+ order_id: The ID of the order to update
81
+ price: New price for the order
82
+ amount: New amount for the order
83
+
84
+ Returns:
85
+ Order: The updated (newly created) order object
86
+
87
+ Raises:
88
+ OrderNotFound: If the order is not found
89
+ BadRequest: If the order is not a limit order
90
+ """
91
+ # Get the existing order from account
92
+ active_orders = self._account.get_orders()
93
+ existing_order = active_orders.get(order_id)
94
+ if not existing_order:
95
+ raise OrderNotFound(f"Order {order_id} not found")
96
+
97
+ # Validate that it's a limit order
98
+ if existing_order.type != "LIMIT":
99
+ raise BadRequest(
100
+ f"Order {order_id} is not a limit order (type: {existing_order.type}). Only limit orders can be updated."
101
+ )
102
+
103
+ self.cancel_order(order_id)
104
+
105
+ request = OrderRequest(
106
+ instrument=existing_order.instrument,
107
+ quantity=abs(amount),
108
+ price=price,
109
+ order_type="LIMIT",
110
+ side=existing_order.side,
111
+ time_in_force=existing_order.time_in_force or "gtc",
112
+ options={},
113
+ )
114
+
115
+ updated_order = self.send_order(request)
116
+
117
+ return updated_order
118
+
119
+ def _send_execution_report(self, report: SimulatedExecutionReport | None):
120
+ if report is None:
121
+ return
122
+
123
+ self.channel.send((report.instrument, "order", report.order, False))
124
+ if report.exec is not None:
125
+ self.channel.send((report.instrument, "deals", [report.exec], False))
126
+
127
+ def exchange(self) -> str:
128
+ return self._exchange.exchange_id
@@ -1,4 +1,5 @@
1
1
  from collections import defaultdict
2
+ from typing import TypeVar
2
3
 
3
4
  import pandas as pd
4
5
 
@@ -19,6 +20,17 @@ from qubx.utils.time import infer_series_frequency
19
20
  from .account import SimulatedAccountProcessor
20
21
  from .utils import SimulatedTimeProvider
21
22
 
23
+ T = TypeVar("T")
24
+
25
+
26
+ def _get_first_existing(data: dict, keys: list, default: T = None) -> T:
27
+ data_get = data.get # Cache method lookup
28
+ sentinel = object()
29
+ for key in keys:
30
+ if (value := data_get(key, sentinel)) is not sentinel and value is not None:
31
+ return value
32
+ return default
33
+
22
34
 
23
35
  class SimulatedDataProvider(IDataProvider):
24
36
  time_provider: SimulatedTimeProvider
@@ -62,16 +74,35 @@ class SimulatedDataProvider(IDataProvider):
62
74
  def is_simulation(self) -> bool:
63
75
  return True
64
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
+
65
88
  def subscribe(self, subscription_type: str, instruments: set[Instrument], reset: bool) -> None:
66
89
  _new_instr = [i for i in instruments if not self.has_subscription(i, subscription_type)]
90
+
67
91
  self._data_source.add_instruments_for_subscription(subscription_type, list(instruments))
68
92
 
69
93
  # - provide historical data and last quote for subscribed instruments
70
94
  for i in _new_instr:
71
- # Check if the instrument was actually subscribed (not filtered out)
95
+ # - check if the instrument was actually subscribed (not filtered out)
72
96
  if not self.has_subscription(i, subscription_type):
73
97
  continue
74
-
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
75
106
  h_data = self._data_source.peek_historical_data(i, subscription_type)
76
107
  if h_data:
77
108
  # _s_type = DataType.from_str(subscription_type)[0]
@@ -91,9 +122,16 @@ class SimulatedDataProvider(IDataProvider):
91
122
  def unsubscribe(self, subscription_type: str, instruments: set[Instrument] | Instrument | None = None) -> None:
92
123
  # logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
93
124
  if instruments is not None:
94
- self._data_source.remove_instruments_from_subscription(
95
- subscription_type, [instruments] if isinstance(instruments, Instrument) else list(instruments)
96
- )
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)
97
135
 
98
136
  def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
99
137
  return self._data_source.has_subscription(instrument, subscription_type)
@@ -157,15 +195,30 @@ class SimulatedDataProvider(IDataProvider):
157
195
  if _b_ts_0 <= cut_time_ns and cut_time_ns < _b_ts_1:
158
196
  break
159
197
 
198
+ # Handle None values in OHLC data
199
+ open_price = r.data["open"]
200
+ high_price = r.data["high"]
201
+ low_price = r.data["low"]
202
+ close_price = r.data["close"]
203
+
204
+ # Skip this record if any OHLC value is None
205
+ if open_price is None or high_price is None or low_price is None or close_price is None:
206
+ continue
207
+
160
208
  bars.append(
161
209
  Bar(
162
210
  _b_ts_0,
163
- r.data["open"],
164
- r.data["high"],
165
- r.data["low"],
166
- r.data["close"],
167
- r.data.get("volume", 0),
168
- r.data.get("bought_volume", 0),
211
+ open_price,
212
+ high_price,
213
+ low_price,
214
+ close_price,
215
+ volume=r.data.get("volume", 0) or 0, # Handle None volume
216
+ bought_volume=_get_first_existing(r.data, ["taker_buy_volume", "bought_volume"], 0),
217
+ volume_quote=_get_first_existing(r.data, ["quote_volume", "volume_quote"], 0),
218
+ bought_volume_quote=_get_first_existing(
219
+ r.data, ["taker_buy_quote_volume", "bought_volume_quote"], 0
220
+ ),
221
+ trade_count=_get_first_existing(r.data, ["count", "trade_count"], 0),
169
222
  )
170
223
  )
171
224
 
@@ -1,11 +1,13 @@
1
1
  import re
2
2
  import zipfile
3
3
  from collections import defaultdict
4
+ from os.path import expanduser
4
5
  from pathlib import Path
5
6
 
6
7
  import numpy as np
7
8
  import pandas as pd
8
9
  import yaml
10
+ from tqdm.auto import tqdm
9
11
 
10
12
  from qubx.core.metrics import TradingSessionResult
11
13
  from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
@@ -38,7 +40,7 @@ class BacktestsResultsManager:
38
40
  """
39
41
 
40
42
  def __init__(self, path: str):
41
- self.path = path
43
+ self.path = expanduser(path)
42
44
  self.reload()
43
45
 
44
46
  def reload(self) -> "BacktestsResultsManager":
@@ -326,10 +328,11 @@ class BacktestsResultsManager:
326
328
  if not as_table:
327
329
  print(_s)
328
330
 
331
+ dd_column = "max_dd_pct" if "max_dd_pct" in metrics else "mdd_pct"
329
332
  if with_metrics:
330
333
  _m_repr = (
331
334
  pd.DataFrame.from_dict(metrics, orient="index")
332
- .T[["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]]
335
+ .T[["gain", "cagr", "sharpe", "qr", dd_column, "mdd_usd", "fees", "execs"]]
333
336
  .astype(float)
334
337
  )
335
338
  _m_repr = _m_repr.round(3).to_string(index=False)
@@ -344,7 +347,7 @@ class BacktestsResultsManager:
344
347
  metrics = {
345
348
  m: round(v, 3)
346
349
  for m, v in metrics.items()
347
- if m in ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
350
+ if m in ["gain", "cagr", "sharpe", "qr", dd_column, "mdd_usd", "fees", "execs"]
348
351
  }
349
352
  _t_rep.append(
350
353
  {"Index": info.get("idx", ""), "Strategy": name}
@@ -379,7 +382,7 @@ class BacktestsResultsManager:
379
382
  _mtrx[_nm] = v.get("performance", {})
380
383
 
381
384
  _m_repr = pd.DataFrame.from_dict(_mtrx, orient="index")[
382
- ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
385
+ ["gain", "cagr", "sharpe", "qr", "mdd_pct", "mdd_usd", "fees", "execs"]
383
386
  ].astype(float)
384
387
  _m_repr = _m_repr.round(3)
385
388
  _m_repr = _m_repr.sort_values(by=sort_by, ascending=ascending) if sort_by else _m_repr
@@ -420,8 +423,10 @@ class BacktestsResultsManager:
420
423
  Returns:
421
424
  plotly.graph_objects.Figure: The plot of the variation.
422
425
  """
423
- import plotly.express as px
424
426
  from itertools import cycle
427
+
428
+ import plotly.express as px
429
+
425
430
  from qubx.utils.misc import string_shortener
426
431
 
427
432
  _vars = self.variations.get(variation_idx)
@@ -507,3 +512,11 @@ class BacktestsResultsManager:
507
512
  )
508
513
  )
509
514
  return figure
515
+
516
+ def export_backtests_to_markdown(self, path: str, tags: tuple[str] | None = None):
517
+ """
518
+ Export backtests to markdown format
519
+ """
520
+ for n, v in tqdm(self.results.items()):
521
+ r = TradingSessionResult.from_file(v.get("path"))
522
+ r.to_markdown(path, list(tags) if tags else None)
@@ -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")