Qubx 0.6.89__tar.gz → 0.6.90__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Qubx might be problematic. Click here for more details.

Files changed (216) hide show
  1. {qubx-0.6.89 → qubx-0.6.90}/PKG-INFO +1 -1
  2. {qubx-0.6.89 → qubx-0.6.90}/pyproject.toml +1 -1
  3. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/broker.py +52 -6
  4. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/broker.py +159 -19
  5. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchange_manager.py +6 -37
  6. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/factory.py +1 -9
  7. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/context.py +12 -2
  8. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/helpers.py +11 -3
  9. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/interfaces.py +51 -7
  10. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/mixins/trading.py +69 -3
  11. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/formatters/target_position.py +2 -6
  12. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/loggers/csv.py +4 -4
  13. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/marketdata/ccxt.py +38 -6
  14. {qubx-0.6.89 → qubx-0.6.90}/LICENSE +0 -0
  15. {qubx-0.6.89 → qubx-0.6.90}/README.md +0 -0
  16. {qubx-0.6.89 → qubx-0.6.90}/build.py +0 -0
  17. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/__init__.py +0 -0
  18. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/_nb_magic.py +0 -0
  19. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/__init__.py +0 -0
  20. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/account.py +0 -0
  21. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/data.py +0 -0
  22. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/management.py +0 -0
  23. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/ome.py +0 -0
  24. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/optimization.py +0 -0
  25. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/runner.py +0 -0
  26. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/sentinels.py +0 -0
  27. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/simulated_data.py +0 -0
  28. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/simulated_exchange.py +0 -0
  29. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/simulator.py +0 -0
  30. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/backtester/utils.py +0 -0
  31. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/cli/__init__.py +0 -0
  32. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/cli/commands.py +0 -0
  33. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/cli/deploy.py +0 -0
  34. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/cli/misc.py +0 -0
  35. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/cli/release.py +0 -0
  36. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/cli/tui.py +0 -0
  37. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/__init__.py +0 -0
  38. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/account.py +0 -0
  39. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  40. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  41. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  42. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/data.py +0 -0
  43. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  44. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  45. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  46. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  47. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  48. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  49. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  50. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  51. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  52. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  53. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  54. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  55. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  56. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  57. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  58. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  59. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  60. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  61. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  62. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  63. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  64. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/reader.py +0 -0
  65. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  66. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  67. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  68. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/utils.py +0 -0
  69. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  70. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/tardis/data.py +0 -0
  71. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/connectors/tardis/utils.py +0 -0
  72. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/__init__.py +0 -0
  73. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/account.py +0 -0
  74. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/basics.py +0 -0
  75. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/deque.py +0 -0
  76. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/errors.py +0 -0
  77. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/exceptions.py +0 -0
  78. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/initializer.py +0 -0
  79. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/loggers.py +0 -0
  80. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/lookups.py +0 -0
  81. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/metrics.py +0 -0
  82. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/mixins/__init__.py +0 -0
  83. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/mixins/market.py +0 -0
  84. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/mixins/processing.py +0 -0
  85. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/mixins/subscription.py +0 -0
  86. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/mixins/universe.py +0 -0
  87. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/mixins/utils.py +0 -0
  88. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/series.pxd +0 -0
  89. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/series.pyi +0 -0
  90. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/series.pyx +0 -0
  91. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/stale_data_detector.py +0 -0
  92. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/utils.pyi +0 -0
  93. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/core/utils.pyx +0 -0
  94. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/data/__init__.py +0 -0
  95. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/data/composite.py +0 -0
  96. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/data/helpers.py +0 -0
  97. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/data/hft.py +0 -0
  98. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/data/readers.py +0 -0
  99. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/data/registry.py +0 -0
  100. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/data/tardis.py +0 -0
  101. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/emitters/__init__.py +0 -0
  102. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/emitters/base.py +0 -0
  103. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/emitters/composite.py +0 -0
  104. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/emitters/csv.py +0 -0
  105. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/emitters/indicator.py +0 -0
  106. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/emitters/inmemory.py +0 -0
  107. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/emitters/prometheus.py +0 -0
  108. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/emitters/questdb.py +0 -0
  109. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/__init__.py +0 -0
  110. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/composite.py +0 -0
  111. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/formatters/__init__.py +0 -0
  112. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/formatters/base.py +0 -0
  113. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/formatters/incremental.py +0 -0
  114. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/formatters/slack.py +0 -0
  115. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/redis_streams.py +0 -0
  116. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/exporters/slack.py +0 -0
  117. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/features/__init__.py +0 -0
  118. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/features/core.py +0 -0
  119. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/features/orderbook.py +0 -0
  120. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/features/price.py +0 -0
  121. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/features/trades.py +0 -0
  122. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/features/utils.py +0 -0
  123. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/gathering/simplest.py +0 -0
  124. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/health/__init__.py +0 -0
  125. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/health/base.py +0 -0
  126. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/loggers/__init__.py +0 -0
  127. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/loggers/factory.py +0 -0
  128. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/loggers/inmemory.py +0 -0
  129. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/loggers/mongo.py +0 -0
  130. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/math/__init__.py +0 -0
  131. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/math/stats.py +0 -0
  132. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/notifications/__init__.py +0 -0
  133. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/notifications/composite.py +0 -0
  134. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/notifications/slack.py +0 -0
  135. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/notifications/throttler.py +0 -0
  136. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/pandaz/__init__.py +0 -0
  137. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/pandaz/ta.py +0 -0
  138. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/pandaz/utils.py +0 -0
  139. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/_build.py +0 -0
  140. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/crypto-fees.ini +0 -0
  141. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  142. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  143. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  144. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  145. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  146. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  147. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  148. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  149. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  150. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  151. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  152. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restarts/__init__.py +0 -0
  153. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restarts/state_resolvers.py +0 -0
  154. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restarts/time_finders.py +0 -0
  155. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restorers/__init__.py +0 -0
  156. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restorers/balance.py +0 -0
  157. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restorers/factory.py +0 -0
  158. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restorers/interfaces.py +0 -0
  159. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restorers/position.py +0 -0
  160. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restorers/signal.py +0 -0
  161. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restorers/state.py +0 -0
  162. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/restorers/utils.py +0 -0
  163. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/ta/__init__.py +0 -0
  164. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/ta/indicators.pxd +0 -0
  165. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/ta/indicators.pyi +0 -0
  166. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/ta/indicators.pyx +0 -0
  167. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/__init__.py +0 -0
  168. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/base.py +0 -0
  169. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  170. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/project/config.yml.j2 +0 -0
  171. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  172. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  173. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  174. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  175. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  176. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/project/template.yml +0 -0
  177. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  178. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  179. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/simple/config.yml.j2 +0 -0
  180. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  181. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  182. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  183. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/templates/simple/template.yml +0 -0
  184. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/trackers/__init__.py +0 -0
  185. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/trackers/advanced.py +0 -0
  186. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/trackers/composite.py +0 -0
  187. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/trackers/rebalancers.py +0 -0
  188. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/trackers/riskctrl.py +0 -0
  189. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/trackers/sizers.py +0 -0
  190. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/__init__.py +0 -0
  191. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/_pyxreloader.py +0 -0
  192. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/charting/lookinglass.py +0 -0
  193. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  194. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/charting/orderbook.py +0 -0
  195. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/collections.py +0 -0
  196. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/marketdata/binance.py +0 -0
  197. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/marketdata/dukas.py +0 -0
  198. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/misc.py +0 -0
  199. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/ntp.py +0 -0
  200. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/numbers_utils.py +0 -0
  201. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/orderbook.py +0 -0
  202. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/plotting/__init__.py +0 -0
  203. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/plotting/dashboard.py +0 -0
  204. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/plotting/data.py +0 -0
  205. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/plotting/interfaces.py +0 -0
  206. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  207. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  208. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/questdb.py +0 -0
  209. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/runner/__init__.py +0 -0
  210. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  211. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/runner/accounts.py +0 -0
  212. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/runner/configs.py +0 -0
  213. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/runner/factory.py +0 -0
  214. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/runner/runner.py +0 -0
  215. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/time.py +0 -0
  216. {qubx-0.6.89 → qubx-0.6.90}/src/qubx/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.89
3
+ Version: 0.6.90
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  License-File: LICENSE
6
6
  Author: Dmitry Marienko
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.89"
7
+ version = "0.6.90"
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"
@@ -6,7 +6,7 @@ from qubx.core.basics import (
6
6
  Instrument,
7
7
  Order,
8
8
  )
9
- from qubx.core.exceptions import OrderNotFound
9
+ from qubx.core.exceptions import BadRequest, OrderNotFound
10
10
  from qubx.core.interfaces import IBroker
11
11
 
12
12
  from .account import SimulatedAccountProcessor
@@ -64,20 +64,66 @@ class SimulatedBroker(IBroker):
64
64
  ) -> None:
65
65
  self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
66
66
 
67
- def cancel_order(self, order_id: str) -> Order | None:
67
+ def cancel_order(self, order_id: str) -> bool:
68
+ """Cancel an order synchronously and return success status."""
68
69
  try:
69
70
  self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
70
- return order_update.order if order_update is not None else None
71
+ return order_update is not None
71
72
  except OrderNotFound:
72
73
  # Order was already cancelled or doesn't exist
73
74
  logger.debug(f"Order {order_id} not found")
74
- return None
75
+ return False
76
+
77
+ def cancel_order_async(self, order_id: str) -> None:
78
+ """Cancel an order asynchronously (fire-and-forget)."""
79
+ # For simulation, async is same as sync since it's fast
80
+ self.cancel_order(order_id)
75
81
 
76
82
  def cancel_orders(self, instrument: Instrument) -> None:
77
83
  raise NotImplementedError("Not implemented yet")
78
84
 
79
- def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
80
- raise NotImplementedError("Not implemented yet")
85
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
86
+ """Update an existing limit order using cancel+recreate strategy.
87
+
88
+ Args:
89
+ order_id: The ID of the order to update
90
+ price: New price for the order
91
+ amount: New amount for the order
92
+
93
+ Returns:
94
+ Order: The updated (newly created) order object
95
+
96
+ Raises:
97
+ OrderNotFound: If the order is not found
98
+ BadRequest: If the order is not a limit order
99
+ """
100
+ # Get the existing order from account
101
+ active_orders = self._account.get_orders()
102
+ existing_order = active_orders.get(order_id)
103
+ if not existing_order:
104
+ raise OrderNotFound(f"Order {order_id} not found")
105
+
106
+ # Validate that it's a limit order
107
+ if existing_order.type.lower() != "limit":
108
+ raise BadRequest(
109
+ f"Order {order_id} is not a limit order (type: {existing_order.type}). Only limit orders can be updated."
110
+ )
111
+
112
+ # Cancel the existing order first
113
+ self.cancel_order(order_id)
114
+
115
+ # Create a new order with updated parameters, preserving original properties
116
+ updated_order = self.send_order(
117
+ instrument=existing_order.instrument,
118
+ order_side=existing_order.side,
119
+ order_type="limit",
120
+ amount=abs(amount),
121
+ price=price,
122
+ client_id=existing_order.client_id, # Preserve original client_id for tracking
123
+ time_in_force=existing_order.time_in_force or "gtc",
124
+ )
125
+
126
+ return updated_order
81
127
 
82
128
  def _send_execution_report(self, report: SimulatedExecutionReport | None):
83
129
  if report is None:
@@ -5,7 +5,6 @@ from typing import Any
5
5
  import pandas as pd
6
6
 
7
7
  import ccxt
8
-
9
8
  from ccxt.base.errors import ExchangeError
10
9
  from qubx import logger
11
10
  from qubx.core.basics import (
@@ -15,7 +14,7 @@ from qubx.core.basics import (
15
14
  OrderSide,
16
15
  )
17
16
  from qubx.core.errors import ErrorLevel, OrderCancellationError, OrderCreationError, create_error_event
18
- from qubx.core.exceptions import BadRequest, InvalidOrderParameters
17
+ from qubx.core.exceptions import BadRequest, InvalidOrderParameters, OrderNotFound
19
18
  from qubx.core.interfaces import (
20
19
  IAccountProcessor,
21
20
  IBroker,
@@ -61,7 +60,6 @@ class CcxtBroker(IBroker):
61
60
  """Get current AsyncThreadLoop for the exchange."""
62
61
  return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
63
62
 
64
-
65
63
  @property
66
64
  def is_simulated_trading(self) -> bool:
67
65
  return False
@@ -178,7 +176,7 @@ class CcxtBroker(IBroker):
178
176
  client_id: str | None = None,
179
177
  time_in_force: str = "gtc",
180
178
  **options,
181
- ) -> Order | None:
179
+ ) -> Order:
182
180
  """
183
181
  Submit an order and wait for the result. Exceptions will be raised on errors.
184
182
 
@@ -221,22 +219,38 @@ class CcxtBroker(IBroker):
221
219
  self._post_order_error_to_databus(
222
220
  err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
223
221
  )
224
- return None
222
+ raise err
225
223
 
226
- def cancel_order(self, order_id: str) -> Order | None:
224
+ def cancel_order(self, order_id: str) -> bool:
225
+ """Cancel an order synchronously and return success status."""
227
226
  orders = self.account.get_orders()
228
227
  if order_id not in orders:
229
228
  logger.warning(f"Order {order_id} not found in active orders")
230
- return None
229
+ return False
231
230
 
232
231
  order = orders[order_id]
233
- logger.info(f"Canceling order {order_id} ...")
232
+ logger.info(f"Canceling order {order_id} synchronously...")
234
233
 
235
- # Submit the cancellation task to the async loop without waiting for the result
236
- self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
234
+ try:
235
+ # Submit the task and wait for result
236
+ future = self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
237
+ return future.result() # This will block until completion or timeout
238
+ except Exception as e:
239
+ logger.error(f"Error during synchronous order cancellation: {e}")
240
+ return False # Return False on any error for simplicity
241
+
242
+ def cancel_order_async(self, order_id: str) -> None:
243
+ """Cancel an order asynchronously (non blocking)."""
244
+ orders = self.account.get_orders()
245
+ if order_id not in orders:
246
+ logger.warning(f"Order {order_id} not found in active orders")
247
+ return
237
248
 
238
- # Always return None as requested
239
- return None
249
+ order = orders[order_id]
250
+ logger.info(f"Canceling order {order_id} asynchronously...")
251
+
252
+ # Submit the task without waiting for result
253
+ self._loop.submit(self._cancel_order_with_retry(order_id, order.instrument))
240
254
 
241
255
  async def _create_order(
242
256
  self,
@@ -298,8 +312,22 @@ class CcxtBroker(IBroker):
298
312
  logger.warning(f"[<y>{instrument.symbol}</y>] :: Quote is not available for order creation.")
299
313
  raise BadRequest(f"Quote is not available for order creation for {instrument.symbol}")
300
314
 
301
- # TODO: think about automatically setting reduce only when needed
302
- if not (reduce_only := options.get("reduceOnly", False)):
315
+ # Auto-detect if order reduces existing position
316
+ reduce_only = options.get("reduceOnly", False)
317
+ if not reduce_only:
318
+ positions = self.account.get_positions()
319
+ if instrument in positions:
320
+ position_qty = positions[instrument].quantity
321
+ # Check if order closes position AND doesn't exceed position size (which would flip to opposite side)
322
+ if (position_qty > 0 and order_side == "SELL" and abs(amount) <= abs(position_qty)) or (
323
+ position_qty < 0 and order_side == "BUY" and abs(amount) <= abs(position_qty)
324
+ ):
325
+ reduce_only = True
326
+ logger.debug(
327
+ f"[{instrument.symbol}] Auto-setting reduceOnly=True ({order_side}, position: {position_qty})"
328
+ )
329
+
330
+ if not reduce_only:
303
331
  min_notional = instrument.min_notional
304
332
  if min_notional > 0 and abs(amount) * quote.mid_price() < min_notional:
305
333
  raise InvalidOrderParameters(
@@ -364,9 +392,13 @@ class CcxtBroker(IBroker):
364
392
  while True:
365
393
  try:
366
394
  if self.enable_cancel_order_ws:
367
- await self._exchange_manager.exchange.cancel_order_ws(order_id, symbol=instrument_to_ccxt_symbol(instrument))
395
+ await self._exchange_manager.exchange.cancel_order_ws(
396
+ order_id, symbol=instrument_to_ccxt_symbol(instrument)
397
+ )
368
398
  else:
369
- await self._exchange_manager.exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(instrument))
399
+ await self._exchange_manager.exchange.cancel_order(
400
+ order_id, symbol=instrument_to_ccxt_symbol(instrument)
401
+ )
370
402
  return True
371
403
  except ccxt.OperationRejected as err:
372
404
  err_msg = str(err).lower()
@@ -375,8 +407,12 @@ class CcxtBroker(IBroker):
375
407
  # These errors might be temporary if the order is still being processed, so retry
376
408
  logger.debug(f"[{order_id}] Order not found for cancellation, might retry: {err}")
377
409
  # Continue with the retry logic instead of returning immediately
410
+ # Order cannot be cancelled (e.g., already filled)
411
+ elif "filled" in err_msg or "partially filled" in err_msg:
412
+ logger.debug(f"[{order_id}] Order cannot be cancelled - already executed: {err}")
413
+ return False # FAILURE: Order cannot be cancelled
414
+ # Other operation rejected errors - don't retry
378
415
  else:
379
- # For other operation rejected errors, don't retry
380
416
  logger.debug(f"[{order_id}] Could not cancel order: {err}")
381
417
  return False
382
418
  except (ccxt.NetworkError, ccxt.ExchangeError, ccxt.ExchangeNotAvailable) as e:
@@ -424,8 +460,112 @@ class CcxtBroker(IBroker):
424
460
  for order_id in instrument_orders:
425
461
  self.cancel_order(order_id)
426
462
 
427
- def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
428
- raise NotImplementedError("Not implemented yet")
463
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
464
+ """Update an existing limit order with new price and amount.
465
+
466
+ Args:
467
+ order_id: The ID of the order to update
468
+ price: New price for the order (already adjusted by TradingManager)
469
+ amount: New amount for the order (already adjusted by TradingManager)
470
+
471
+ Returns:
472
+ Order: The updated Order object if successful
473
+
474
+ Raises:
475
+ OrderNotFound: If the order is not found
476
+ BadRequest: If the order is not a limit order
477
+ ExchangeError: If the exchange operation fails
478
+ """
479
+ logger.debug(f"Updating order {order_id} with price={price}, amount={amount}")
480
+
481
+ active_orders = self.account.get_orders()
482
+ if order_id not in active_orders:
483
+ raise OrderNotFound(f"Order {order_id} not found in active orders")
484
+
485
+ existing_order = active_orders[order_id]
486
+
487
+ # Validate that the order can still be updated (not fully filled/closed)
488
+ updatable_statuses = ["OPEN", "NEW", "PENDING"]
489
+ if existing_order.status not in updatable_statuses:
490
+ raise BadRequest(
491
+ f"Order {order_id} with status '{existing_order.status}' cannot be updated. "
492
+ f"Only orders with status {updatable_statuses} can be updated."
493
+ )
494
+
495
+ instrument = existing_order.instrument
496
+
497
+ logger.debug(
498
+ f"[<g>{instrument.symbol}</g>] :: Updating order {order_id}: "
499
+ f"{amount} @ {price} (was: {existing_order.quantity} @ {existing_order.price})"
500
+ )
501
+
502
+ try:
503
+ # Check if exchange supports order editing
504
+ if self._exchange_manager.exchange.has.get("editOrder", False):
505
+ return self._update_order_direct(order_id, existing_order, price, amount)
506
+ else:
507
+ return self._update_order_fallback(order_id, existing_order, price, amount)
508
+ except Exception as err:
509
+ logger.error(f"Failed to update order {order_id}: {err}")
510
+ raise
511
+
512
+ def _update_order_direct(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
513
+ """Update order using exchange's native edit functionality."""
514
+ logger.debug(f"Using direct order update for {order_id}")
515
+
516
+ future_result = self._loop.submit(self._edit_order_async(order_id, existing_order, price, amount))
517
+ updated_order, error = future_result.result()
518
+
519
+ if error is not None:
520
+ raise error
521
+
522
+ if updated_order is not None:
523
+ self.account.process_order(updated_order)
524
+ logger.debug(f"Direct update successful for order {order_id}")
525
+ return updated_order
526
+ else:
527
+ raise Exception("Order update returned None without error")
528
+
529
+ def _update_order_fallback(self, order_id: str, existing_order: Order, price: float, amount: float) -> Order:
530
+ """Update order using cancel+recreate strategy for exchanges without editOrder support."""
531
+ logger.debug(f"Using fallback (cancel+recreate) strategy for order {order_id}")
532
+
533
+ success = self.cancel_order(order_id)
534
+ if not success:
535
+ raise Exception(f"Failed to cancel order {order_id} during update")
536
+
537
+ updated_order = self.send_order(
538
+ instrument=existing_order.instrument,
539
+ order_side=existing_order.side,
540
+ order_type=existing_order.type,
541
+ amount=amount,
542
+ price=price,
543
+ client_id=existing_order.client_id, # Preserve original client_id for tracking
544
+ time_in_force=existing_order.time_in_force or "gtc",
545
+ )
546
+
547
+ logger.debug(f"Fallback update successful for order {order_id} -> new order {updated_order.id}")
548
+ return updated_order
549
+
550
+ async def _edit_order_async(
551
+ self, order_id: str, existing_order: Order, price: float, amount: float
552
+ ) -> tuple[Order | None, Exception | None]:
553
+ """Async helper for direct order editing."""
554
+ try:
555
+ ccxt_symbol = instrument_to_ccxt_symbol(existing_order.instrument)
556
+ ccxt_side = "buy" if existing_order.side == "BUY" else "sell"
557
+
558
+ result = await self._exchange_manager.exchange.edit_order(
559
+ id=order_id, symbol=ccxt_symbol, type="limit", side=ccxt_side, amount=amount, price=price, params={}
560
+ )
561
+
562
+ # Convert the result back to our Order format
563
+ updated_order = ccxt_convert_order_info(existing_order.instrument, result)
564
+ return updated_order, None
565
+
566
+ except Exception as err:
567
+ logger.error(f"Async edit order failed for {order_id}: {err}")
568
+ return None, err
429
569
 
430
570
  def exchange(self) -> str:
431
571
  """
@@ -18,8 +18,6 @@ from qubx.core.interfaces import IDataArrivalListener
18
18
 
19
19
  # Constants for better maintainability
20
20
  DEFAULT_CHECK_INTERVAL_SECONDS = 60.0
21
- DEFAULT_MAX_RECREATIONS = 5
22
- DEFAULT_RESET_INTERVAL_HOURS = 6.0
23
21
  SECONDS_PER_HOUR = 3600
24
22
 
25
23
  # Custom stall detection thresholds (in seconds)
@@ -30,7 +28,7 @@ STALL_THRESHOLDS = {
30
28
  "trade": 60 * 60, # 60 minutes = 3,600s
31
29
  "liquidation": 7 * 24 * SECONDS_PER_HOUR, # 7 days = 604,800s
32
30
  "ohlc": 5 * 60, # 5 minutes = 300s
33
- "quote": 5 * 60, # 5 minutes = 300s
31
+ "quote": 2 * 60, # 2 minutes = 120s
34
32
  }
35
33
  DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
36
34
 
@@ -45,7 +43,7 @@ class ExchangeManager(IDataArrivalListener):
45
43
  Key Features:
46
44
  - Explicit .exchange property for CCXT access
47
45
  - Self-contained stall detection and recreation triggering
48
- - Circuit breaker protection with recreation limits
46
+ - Automatic recreation without limits when data stalls
49
47
  - Atomic exchange transitions during recreation
50
48
  - Background monitoring thread for stall detection
51
49
  """
@@ -57,8 +55,6 @@ class ExchangeManager(IDataArrivalListener):
57
55
  exchange_name: str,
58
56
  factory_params: dict[str, Any],
59
57
  initial_exchange: Optional[cxp.Exchange] = None,
60
- max_recreations: int = DEFAULT_MAX_RECREATIONS,
61
- reset_interval_hours: float = DEFAULT_RESET_INTERVAL_HOURS,
62
58
  check_interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
63
59
  ):
64
60
  """Initialize ExchangeManager with underlying CCXT exchange.
@@ -67,19 +63,14 @@ class ExchangeManager(IDataArrivalListener):
67
63
  exchange_name: Exchange name for factory (e.g., "binance.um")
68
64
  factory_params: Parameters for get_ccxt_exchange()
69
65
  initial_exchange: Pre-created exchange instance (from factory)
70
- max_recreations: Maximum recreation attempts before giving up
71
- reset_interval_hours: Hours between recreation count resets
72
66
  check_interval_seconds: How often to check for stalls (default: 60.0)
73
67
  """
74
68
  self._exchange_name = exchange_name
75
69
  self._factory_params = factory_params.copy()
76
- self._max_recreations = max_recreations
77
- self._reset_interval_hours = reset_interval_hours
78
70
 
79
71
  # Recreation state
80
- self._recreation_count = 0
81
72
  self._recreation_lock = threading.RLock()
82
- self._last_successful_reset = time.time()
73
+ self._recreation_count = 0 # Track for logging purposes only
83
74
 
84
75
  # Stall detection state
85
76
  self._check_interval = check_interval_seconds
@@ -142,28 +133,19 @@ class ExchangeManager(IDataArrivalListener):
142
133
 
143
134
  def force_recreation(self) -> bool:
144
135
  """
145
- Force recreation due to data stalls (called by BaseHealthMonitor).
136
+ Force recreation due to data stalls.
146
137
 
147
138
  Returns:
148
- True if recreation successful, False if failed/limit exceeded
139
+ True if recreation successful, False if failed
149
140
  """
150
141
  with self._recreation_lock:
151
- # Check recreation limit
152
- if self._recreation_count >= self._max_recreations:
153
- logger.error(
154
- f"Cannot recreate {self._exchange_name}: recreation limit ({self._max_recreations}) exceeded"
155
- )
156
- return False
157
-
158
142
  logger.info(f"Stall-triggered recreation for {self._exchange_name}")
159
143
  return self._recreate_exchange()
160
144
 
161
145
  def _recreate_exchange(self) -> bool:
162
146
  """Recreate the underlying exchange (must be called with _recreation_lock held)."""
163
147
  self._recreation_count += 1
164
- logger.warning(
165
- f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count}/{self._max_recreations})"
166
- )
148
+ logger.warning(f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count})")
167
149
 
168
150
  # Create new exchange
169
151
  try:
@@ -190,18 +172,6 @@ class ExchangeManager(IDataArrivalListener):
190
172
 
191
173
  return True
192
174
 
193
- def reset_recreation_count_if_needed(self) -> None:
194
- """Reset recreation count periodically (called by monitoring loop)."""
195
- reset_interval_seconds = self._reset_interval_hours * SECONDS_PER_HOUR
196
-
197
- current_time = time.time()
198
- time_since_reset = current_time - self._last_successful_reset
199
-
200
- if time_since_reset >= reset_interval_seconds and self._recreation_count > 0:
201
- logger.info(f"Resetting recreation count for {self._exchange_name} (was {self._recreation_count})")
202
- self._recreation_count = 0
203
- self._last_successful_reset = current_time
204
-
205
175
  def on_data_arrival(self, event_type: str, event_time: dt_64) -> None:
206
176
  """Record data arrival for stall detection.
207
177
 
@@ -254,7 +224,6 @@ class ExchangeManager(IDataArrivalListener):
254
224
  while self._monitoring_enabled:
255
225
  try:
256
226
  self._check_and_handle_stalls()
257
- self.reset_recreation_count_if_needed()
258
227
  time.sleep(self._check_interval)
259
228
  except Exception as e:
260
229
  logger.error(f"Error in ExchangeManager stall detection: {e}")
@@ -85,8 +85,6 @@ def get_ccxt_exchange_manager(
85
85
  secret: str | None = None,
86
86
  loop: asyncio.AbstractEventLoop | None = None,
87
87
  use_testnet: bool = False,
88
- max_recreations: int = 3,
89
- reset_interval_hours: float = 24.0,
90
88
  check_interval_seconds: float = 30.0,
91
89
  **kwargs,
92
90
  ) -> ExchangeManager:
@@ -102,8 +100,6 @@ def get_ccxt_exchange_manager(
102
100
  secret (str, optional): The API secret. Default is None.
103
101
  loop (asyncio.AbstractEventLoop, optional): Event loop. Default is None.
104
102
  use_testnet (bool): Use testnet/sandbox mode. Default is False.
105
- max_recreations (int): Maximum recreation attempts before circuit breaker. Default is 3.
106
- reset_interval_hours (float): Hours between recreation count resets. Default is 24.0.
107
103
  check_interval_seconds (float): How often to check for stalls. Default is 30.0.
108
104
  **kwargs: Additional parameters for exchange configuration.
109
105
 
@@ -117,9 +113,7 @@ def get_ccxt_exchange_manager(
117
113
  'secret': secret,
118
114
  'loop': loop,
119
115
  'use_testnet': use_testnet,
120
- **{k: v for k, v in kwargs.items() if k not in {
121
- 'max_recreations', 'reset_interval_hours', 'check_interval_seconds'
122
- }}
116
+ **{k: v for k, v in kwargs.items() if k != 'check_interval_seconds'}
123
117
  }
124
118
 
125
119
  # Create raw CCXT exchange using public factory method
@@ -137,8 +131,6 @@ def get_ccxt_exchange_manager(
137
131
  exchange_name=exchange,
138
132
  factory_params=factory_params,
139
133
  initial_exchange=ccxt_exchange,
140
- max_recreations=max_recreations,
141
- reset_interval_hours=reset_interval_hours,
142
134
  check_interval_seconds=check_interval_seconds,
143
135
  )
144
136
 
@@ -468,12 +468,22 @@ class StrategyContext(IStrategyContext):
468
468
  def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
469
469
  return self._trading_manager.close_positions(market_type, without_signals)
470
470
 
471
- def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
471
+ def cancel_order(self, order_id: str, exchange: str | None = None) -> bool:
472
+ """Cancel a specific order synchronously."""
472
473
  return self._trading_manager.cancel_order(order_id, exchange)
473
474
 
474
- def cancel_orders(self, instrument: Instrument):
475
+ def cancel_order_async(self, order_id: str, exchange: str | None = None) -> None:
476
+ """Cancel a specific order asynchronously (non blocking)."""
477
+ return self._trading_manager.cancel_order_async(order_id, exchange)
478
+
479
+ def cancel_orders(self, instrument: Instrument) -> None:
480
+ """Cancel all orders for an instrument."""
475
481
  return self._trading_manager.cancel_orders(instrument)
476
482
 
483
+ def update_order(self, order_id: str, price: float, amount: float, exchange: str | None = None) -> Order:
484
+ """Update an existing limit order with new price and amount."""
485
+ return self._trading_manager.update_order(order_id, price, amount, exchange)
486
+
477
487
  # IUniverseManager delegation
478
488
  def set_universe(
479
489
  self, instruments: list[Instrument], skip_callback: bool = False, if_has_position_then: RemovalPolicy = "close"
@@ -237,12 +237,20 @@ class CachedMarketDataHolder:
237
237
  bought_volume_quote = volume_quote if trade.side == 1 else 0.0
238
238
  for ser in series.values():
239
239
  if len(ser) > 0:
240
- current_bar_start = floor_t64(np.datetime64(ser[0].time, 'ns'), np.timedelta64(ser.timeframe, 'ns'))
241
- trade_bar_start = floor_t64(np.datetime64(trade.time, 'ns'), np.timedelta64(ser.timeframe, 'ns'))
240
+ current_bar_start = floor_t64(np.datetime64(ser[0].time, "ns"), np.timedelta64(ser.timeframe, "ns"))
241
+ trade_bar_start = floor_t64(np.datetime64(trade.time, "ns"), np.timedelta64(ser.timeframe, "ns"))
242
242
  if trade_bar_start < current_bar_start:
243
243
  # Trade belongs to a previous bar - skip it
244
244
  continue
245
- ser.update(trade.time, trade.price, total_vol, bought_vol, volume_quote, bought_volume_quote, 1)
245
+ ser.update(
246
+ trade.time,
247
+ trade.price,
248
+ volume=total_vol,
249
+ bvolume=bought_vol,
250
+ volume_quote=volume_quote,
251
+ bought_volume_quote=bought_volume_quote,
252
+ trade_count=1,
253
+ )
246
254
 
247
255
  def finalize_ohlc_for_instruments(self, time: dt_64, instruments: list[Instrument]):
248
256
  """
@@ -348,14 +348,25 @@ class IBroker:
348
348
  """
349
349
  raise NotImplementedError("send_order_async is not implemented")
350
350
 
351
- def cancel_order(self, order_id: str) -> None:
352
- """Cancel an existing order (non blocking).
351
+ def cancel_order(self, order_id: str) -> bool:
352
+ """Cancel an existing order synchronously.
353
353
 
354
354
  Args:
355
355
  order_id: The ID of the order to cancel.
356
+
357
+ Returns:
358
+ bool: True if cancellation was successful, False otherwise.
356
359
  """
357
360
  raise NotImplementedError("cancel_order is not implemented")
358
361
 
362
+ def cancel_order_async(self, order_id: str) -> None:
363
+ """Cancel an existing order asynchronously (non blocking).
364
+
365
+ Args:
366
+ order_id: The ID of the order to cancel.
367
+ """
368
+ raise NotImplementedError("cancel_order_async is not implemented")
369
+
359
370
  def cancel_orders(self, instrument: Instrument) -> None:
360
371
  """Cancel all orders for an instrument.
361
372
 
@@ -364,8 +375,8 @@ class IBroker:
364
375
  """
365
376
  raise NotImplementedError("cancel_orders is not implemented")
366
377
 
367
- def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
368
- """Update an existing order.
378
+ def update_order(self, order_id: str, price: float, amount: float) -> Order:
379
+ """Update an existing order with new price and amount.
369
380
 
370
381
  Args:
371
382
  order_id: The ID of the order to update.
@@ -378,7 +389,8 @@ class IBroker:
378
389
  Raises:
379
390
  NotImplementedError: If the method is not implemented
380
391
  OrderNotFound: If the order is not found
381
- BadRequest: If the request is invalid
392
+ BadRequest: If the request is invalid (e.g., not a limit order)
393
+ InvalidOrderParameters: If the order cannot be updated
382
394
  """
383
395
  raise NotImplementedError("update_order is not implemented")
384
396
 
@@ -698,11 +710,24 @@ class ITradingManager:
698
710
  """Close all positions."""
699
711
  ...
700
712
 
701
- def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
702
- """Cancel a specific order.
713
+ def cancel_order(self, order_id: str, exchange: str | None = None) -> bool:
714
+ """Cancel a specific order synchronously.
703
715
 
704
716
  Args:
705
717
  order_id: ID of the order to cancel
718
+ exchange: Exchange to cancel on (optional)
719
+
720
+ Returns:
721
+ bool: True if cancellation was successful, False otherwise.
722
+ """
723
+ ...
724
+
725
+ def cancel_order_async(self, order_id: str, exchange: str | None = None) -> None:
726
+ """Cancel a specific order asynchronously (non blocking).
727
+
728
+ Args:
729
+ order_id: ID of the order to cancel
730
+ exchange: Exchange to cancel on (optional)
706
731
  """
707
732
  ...
708
733
 
@@ -714,6 +739,25 @@ class ITradingManager:
714
739
  """
715
740
  ...
716
741
 
742
+ def update_order(self, order_id: str, price: float, amount: float, exchange: str | None = None) -> Order:
743
+ """Update an existing limit order with new price and amount.
744
+
745
+ Args:
746
+ order_id: ID of the order to update
747
+ price: New price for the order
748
+ amount: New amount for the order
749
+ exchange: Exchange to update on (optional, defaults to first exchange)
750
+
751
+ Returns:
752
+ Order: The updated order object
753
+
754
+ Raises:
755
+ OrderNotFound: If the order is not found
756
+ BadRequest: If the order is not a limit order or other validation errors
757
+ InvalidOrderParameters: If the update parameters are invalid
758
+ """
759
+ ...
760
+
717
761
  def exchanges(self) -> list[str]: ...
718
762
 
719
763