Qubx 4.0.0.dev1__tar.gz → 4.1.0__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 (339) hide show
  1. {qubx-4.0.0.dev1 → qubx-4.1.0}/PKG-INFO +1 -1
  2. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/_version.py +2 -2
  3. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/runner.py +2 -1
  4. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/simulator.py +18 -3
  5. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/utils.py +35 -9
  6. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/connector.py +278 -20
  7. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/binance/connector.py +157 -2
  8. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +8 -0
  9. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/factory.py +7 -1
  10. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/plugin.py +6 -1
  11. qubx-4.1.0/src/qubx/connectors/ccxt/replace.py +111 -0
  12. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/plugin.py +10 -0
  13. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/builtin.py +22 -19
  14. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/diffs.py +6 -3
  15. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/manager.py +86 -34
  16. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/reconciler.py +2 -2
  17. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/reducer.py +9 -2
  18. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/state.py +41 -9
  19. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/basics.py +60 -22
  20. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/context.py +26 -0
  21. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/fit_context.py +3 -1
  22. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/interfaces.py +15 -0
  23. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/loggers.py +1 -1
  24. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/lookups.py +223 -205
  25. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/instrument_service.py +1 -1
  26. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/market.py +3 -1
  27. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/processing.py +16 -7
  28. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/subscription.py +27 -18
  29. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/trading.py +15 -4
  30. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/subscription_watchdog.py +31 -13
  31. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/multi.py +70 -41
  32. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/questdb.py +47 -10
  33. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/rebalancers.py +1 -1
  34. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/_jupyter_runner.pyt +2 -2
  35. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/configs.py +11 -0
  36. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/runner.py +78 -14
  37. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/init_code.py +2 -2
  38. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/time.py +3 -3
  39. {qubx-4.0.0.dev1 → qubx-4.1.0}/.gitignore +0 -0
  40. {qubx-4.0.0.dev1 → qubx-4.1.0}/LICENSE +0 -0
  41. {qubx-4.0.0.dev1 → qubx-4.1.0}/README.md +0 -0
  42. {qubx-4.0.0.dev1 → qubx-4.1.0}/pyproject.toml +0 -0
  43. {qubx-4.0.0.dev1 → qubx-4.1.0}/scripts/build.py +0 -0
  44. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/__init__.py +0 -0
  45. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/_nb_magic.py +0 -0
  46. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/__init__.py +0 -0
  47. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/connector.py +0 -0
  48. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/data.py +0 -0
  49. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/iteratedstream.py +0 -0
  50. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/management.py +0 -0
  51. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/ome.py +0 -0
  52. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/optimization.py +0 -0
  53. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/sentinels.py +0 -0
  54. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/simulated_data.py +0 -0
  55. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/simulated_exchange.py +0 -0
  56. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/transfers.py +0 -0
  57. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/__init__.py +0 -0
  58. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/commands.py +0 -0
  59. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/deploy.py +0 -0
  60. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/misc.py +0 -0
  61. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/release.py +0 -0
  62. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/resolver.py +0 -0
  63. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/s3.py +0 -0
  64. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/theme.py +0 -0
  65. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/tui.py +0 -0
  66. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/user_config.py +0 -0
  67. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/config.py +0 -0
  68. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/__init__.py +0 -0
  69. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/__init__.py +0 -0
  70. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  71. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  72. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  73. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/data.py +0 -0
  74. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  75. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  76. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  77. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/_two_stream.py +0 -0
  78. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  79. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  80. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bitfinex/connector.py +0 -0
  81. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bybit/__init__.py +0 -0
  82. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bybit/bybit.py +0 -0
  83. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bybit/connector.py +0 -0
  84. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  85. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  86. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  87. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  88. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  89. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  90. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/okx/connector.py +0 -0
  91. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  92. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  93. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  94. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  95. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  96. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  97. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  98. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  99. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  100. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  101. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  102. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
  103. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  104. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  105. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  106. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/utils.py +0 -0
  107. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  108. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/registry.py +0 -0
  109. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/tardis/data.py +0 -0
  110. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/tardis/plugin.py +0 -0
  111. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/tardis/utils.py +0 -0
  112. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/__init__.py +0 -0
  113. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/decorator.py +0 -0
  114. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/executor.py +0 -0
  115. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/interfaces.py +0 -0
  116. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/server.py +0 -0
  117. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/types.py +0 -0
  118. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/__init__.py +0 -0
  119. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/__init__.py +0 -0
  120. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/config.py +0 -0
  121. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/state_machine.py +0 -0
  122. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/boot.py +0 -0
  123. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/connector.py +0 -0
  124. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/detectors/__init__.py +0 -0
  125. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/detectors/delisting.py +0 -0
  126. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/detectors/stale.py +0 -0
  127. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/errors.py +0 -0
  128. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/events.py +0 -0
  129. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/exceptions.py +0 -0
  130. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/fit_executor.py +0 -0
  131. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/helpers.py +0 -0
  132. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/initializer.py +0 -0
  133. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/instrument_service.py +0 -0
  134. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/metrics.py +0 -0
  135. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/__init__.py +0 -0
  136. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/universe.py +0 -0
  137. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/utils.py +0 -0
  138. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/series.pxd +0 -0
  139. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/series.pyi +0 -0
  140. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/series.pyx +0 -0
  141. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/state_snapshot.py +0 -0
  142. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/status.py +0 -0
  143. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/utils.pyi +0 -0
  144. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/utils.pyx +0 -0
  145. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/__init__.py +0 -0
  146. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/cache.py +0 -0
  147. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/containers.py +0 -0
  148. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/guards.py +0 -0
  149. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/registry.py +0 -0
  150. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storage.py +0 -0
  151. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/_missing_extra.py +0 -0
  152. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/ccxt.py +0 -0
  153. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/csv.py +0 -0
  154. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/dukascopy.py +0 -0
  155. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/handy.py +0 -0
  156. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/iceberg.py +0 -0
  157. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/stub.py +0 -0
  158. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/utils.py +0 -0
  159. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/yahoo.py +0 -0
  160. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/transformers.py +0 -0
  161. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/__init__.py +0 -0
  162. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/base.py +0 -0
  163. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/composite.py +0 -0
  164. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/csv.py +0 -0
  165. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/indicator.py +0 -0
  166. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/inmemory.py +0 -0
  167. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/prometheus.py +0 -0
  168. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/questdb.py +0 -0
  169. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/__init__.py +0 -0
  170. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/composite.py +0 -0
  171. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/__init__.py +0 -0
  172. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/base.py +0 -0
  173. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/incremental.py +0 -0
  174. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/slack.py +0 -0
  175. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/target_position.py +0 -0
  176. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/redis_streams.py +0 -0
  177. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/slack.py +0 -0
  178. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/gathering/simplest.py +0 -0
  179. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/health/__init__.py +0 -0
  180. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/health/base.py +0 -0
  181. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/health/dummy.py +0 -0
  182. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/health/status.py +0 -0
  183. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/__init__.py +0 -0
  184. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/csv.py +0 -0
  185. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/factory.py +0 -0
  186. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/inmemory.py +0 -0
  187. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/postgres.py +0 -0
  188. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/notifications/__init__.py +0 -0
  189. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/notifications/composite.py +0 -0
  190. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/notifications/slack.py +0 -0
  191. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/notifications/throttler.py +0 -0
  192. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/pandaz/__init__.py +0 -0
  193. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/pandaz/stats.py +0 -0
  194. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/pandaz/ta.py +0 -0
  195. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/pandaz/utils.py +0 -0
  196. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/plugins/__init__.py +0 -0
  197. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/plugins/loader.py +0 -0
  198. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/__init__.py +0 -0
  199. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/backend.py +0 -0
  200. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/config.py +0 -0
  201. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/engine.py +0 -0
  202. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/ip_resolver.py +0 -0
  203. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/manager.py +0 -0
  204. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/pools.py +0 -0
  205. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/redis_backend.py +0 -0
  206. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/resilient.py +0 -0
  207. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/_build.py +0 -0
  208. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/crypto-fees.ini +0 -0
  209. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  210. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  211. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  212. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  213. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  214. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  215. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  216. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  217. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  218. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  219. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  220. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restarts/__init__.py +0 -0
  221. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restarts/state_resolvers.py +0 -0
  222. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restarts/time_finders.py +0 -0
  223. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/__init__.py +0 -0
  224. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/balance.py +0 -0
  225. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/factory.py +0 -0
  226. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/interfaces.py +0 -0
  227. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/position.py +0 -0
  228. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/signal.py +0 -0
  229. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/state.py +0 -0
  230. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/utils.py +0 -0
  231. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/__init__.py +0 -0
  232. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/dummy.py +0 -0
  233. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/redis.py +0 -0
  234. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/runtime_info.py +0 -0
  235. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/safe.py +0 -0
  236. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/ta/__init__.py +0 -0
  237. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/ta/indicators.pxd +0 -0
  238. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/ta/indicators.pyi +0 -0
  239. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/ta/indicators.pyx +0 -0
  240. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/__init__.py +0 -0
  241. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/base.py +0 -0
  242. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  243. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/config.yml.j2 +0 -0
  244. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  245. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  246. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  247. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  248. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  249. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/template.yml +0 -0
  250. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  251. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  252. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  253. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  254. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  255. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  256. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.python-version +0 -0
  257. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  258. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  259. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  260. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  261. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/README.md.j2 +0 -0
  262. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  263. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/justfile.j2 +0 -0
  264. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  265. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/research/.gitkeep +0 -0
  266. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  267. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  268. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  269. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/template.yml +0 -0
  270. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  271. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  272. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  273. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/config.yml.j2 +0 -0
  274. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  275. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  276. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  277. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/template.yml +0 -0
  278. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/__init__.py +0 -0
  279. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/advanced.py +0 -0
  280. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/composite.py +0 -0
  281. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/riskctrl.py +0 -0
  282. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/sizers.py +0 -0
  283. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/__init__.py +0 -0
  284. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/_pyxreloader.py +0 -0
  285. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/charting/lookinglass.py +0 -0
  286. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  287. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/charting/orderbook.py +0 -0
  288. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/clock.py +0 -0
  289. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/collections.py +0 -0
  290. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/hft/__init__.py +0 -0
  291. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/hft/numba_utils.py +0 -0
  292. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/hft/orderbook.pyi +0 -0
  293. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/hft/orderbook.pyx +0 -0
  294. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/marketdata/binance.py +0 -0
  295. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/marketdata/ccxt.py +0 -0
  296. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/marketdata/dukas.py +0 -0
  297. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/misc.py +0 -0
  298. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/nonce.py +0 -0
  299. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/numbers_utils.py +0 -0
  300. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/orderbook.py +0 -0
  301. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/__init__.py +0 -0
  302. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/dashboard.py +0 -0
  303. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/data.py +0 -0
  304. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/interfaces.py +0 -0
  305. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  306. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  307. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/questdb.py +0 -0
  308. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/rate_limiter.py +0 -0
  309. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/results.py +0 -0
  310. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/ringbuffer.pxd +0 -0
  311. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/ringbuffer.pyi +0 -0
  312. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/ringbuffer.pyx +0 -0
  313. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/__init__.py +0 -0
  314. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/accounts.py +0 -0
  315. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/factory.py +0 -0
  316. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/kernel_service.py +0 -0
  317. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/__init__.py +0 -0
  318. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/app.py +0 -0
  319. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/handlers.py +0 -0
  320. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/kernel.py +0 -0
  321. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  322. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  323. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
  324. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  325. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  326. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/log_filter.py +0 -0
  327. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  328. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  329. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  330. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  331. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/s3.py +0 -0
  332. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/slack.py +0 -0
  333. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/threading.py +0 -0
  334. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/throttler.py +0 -0
  335. {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/websocket_manager.py +0 -0
  336. {qubx-4.0.0.dev1 → qubx-4.1.0}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  337. {qubx-4.0.0.dev1 → qubx-4.1.0}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  338. {qubx-4.0.0.dev1 → qubx-4.1.0}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  339. {qubx-4.0.0.dev1 → qubx-4.1.0}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: Qubx
3
- Version: 4.0.0.dev1
3
+ Version: 4.1.0
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Project-URL: homepage, https://xlydian.com
6
6
  Project-URL: repository, https://github.com/xLydianSoftware/Qubx
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '4.0.0.dev1'
22
- __version_tuple__ = version_tuple = (4, 0, 0, 'dev1')
21
+ __version__ = version = '4.1.0'
22
+ __version_tuple__ = version_tuple = (4, 1, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -612,9 +612,10 @@ class SimulationRunner:
612
612
  connectors=self._connectors,
613
613
  base_currencies=self.setup.base_currencies,
614
614
  time=time_provider,
615
- cfg=AccountManagerConfig(),
615
+ cfg=AccountManagerConfig(maint_margin_rate=self.setup.maint_margin_rate),
616
616
  account_id=self.account_id,
617
617
  tcc=_exchange_to_tcc,
618
+ account_markets=self.setup.account_markets,
618
619
  )
619
620
 
620
621
  # - seed initial capital per exchange into the account state
@@ -5,8 +5,9 @@ from joblib import delayed
5
5
 
6
6
  from qubx import QubxLogConfig, file_formatter, logger
7
7
  from qubx.backtester.utils import SetupTypes
8
- from qubx.core.basics import Instrument
8
+ from qubx.core.basics import DEFAULT_MAINTENANCE_MARGIN, Instrument
9
9
  from qubx.core.exceptions import SimulationError
10
+ from qubx.core.lookups import lookup
10
11
  from qubx.core.metrics import TradingSessionResult
11
12
  from qubx.data.storage import IStorage
12
13
  from qubx.emitters.inmemory import InMemoryMetricEmitter
@@ -45,6 +46,7 @@ def simulate(
45
46
  accurate_stop_orders_execution: bool = False,
46
47
  signal_timeframe: str = "1Min",
47
48
  enable_funding: bool = False,
49
+ maint_margin_rate: float = DEFAULT_MAINTENANCE_MARGIN,
48
50
  debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
49
51
  show_latency_report: bool = False,
50
52
  portfolio_log_freq: str = "5Min",
@@ -55,6 +57,7 @@ def simulate(
55
57
  prefetch_config: PrefetchConfig | None = None,
56
58
  trading_sessions_time: str | dict[str, str | tuple[int, int] | tuple[str, str]] = "DEFAULT",
57
59
  log_file: str | None = None,
60
+ accounts: dict[str, list[str]] | None = None,
58
61
  ) -> list[TradingSessionResult]:
59
62
  """
60
63
  Backtest utility for trading strategies or signals using historical data.
@@ -76,6 +79,7 @@ def simulate(
76
79
  - accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
77
80
  - signal_timeframe (str): Timeframe for signals, default is "1Min".
78
81
  - enable_funding (bool): If True, enables funding rate simulation, default is False.
82
+ - maint_margin_rate (float): Maintenance margin as a fraction of notional for simulated positions (the account's `maint_margin_rate`).
79
83
  - debug (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None): Logging level for debugging.
80
84
  - show_latency_report: If True, shows simulator's latency report.
81
85
  - portfolio_log_freq (str): Frequency for portfolio logging, default is "5Min".
@@ -86,6 +90,10 @@ def simulate(
86
90
  - prefetch_config (dict[str, Any] | None): Configuration for prefetching auxiliary data, default is None.
87
91
  - trading_sessions_time (str | dict[str, str | tuple[int, int] | tuple[str, str]]): trading session times (may be "DEFAULT", "STOCKS", "CME" or tuple like ("9:30:00", "15:59:59") or specified for every exchange {"NYSE": "STOKS", ....})
88
92
  - log_file (str | None): Optional path to write simulation logs to a file (in addition to stdout). Defaults to None.
93
+ - accounts (dict[str, list[str]] | None): account -> the markets it trades, its own first, e.g.
94
+ {"BINANCE.UM": ["BINANCE.UM", "BINANCE"]} for a portfolio-margin account trading perps and spot: the spot
95
+ market keeps its own simulated exchange (fees, execution) but books into the BINANCE.UM account — one
96
+ capital pool, capital merged into it. Default: one account per exchange.
89
97
  Returns:
90
98
  - list[TradingSessionResult]: A list of TradingSessionResult objects containing the results of each simulation setup.
91
99
  """
@@ -125,6 +133,7 @@ def simulate(
125
133
  accurate_stop_orders_execution=accurate_stop_orders_execution,
126
134
  run_separate_instruments=run_separate_instruments,
127
135
  enable_funding=enable_funding,
136
+ maint_margin_rate=maint_margin_rate,
128
137
  )
129
138
  if not simulation_setups:
130
139
  logger.error(
@@ -132,6 +141,9 @@ def simulate(
132
141
  := "Can't recognize setup - it should be a strategy, a set of signals or list of signals/strategies + tracker !"
133
142
  )
134
143
  raise SimulationError(_msg)
144
+ if accounts:
145
+ for setup in simulation_setups:
146
+ setup.set_accounts(accounts)
135
147
 
136
148
  # - inform about separate instruments mode
137
149
  if run_separate_instruments and len(simulation_setups) > 1:
@@ -259,10 +271,13 @@ def _run_setup(
259
271
  # Adjust the start date for the simulation to the onboard date of the instrument with the minimum onboard date.
260
272
  # TODO: this can be removed once we add some artificial data stream to move the simulation
261
273
  if setup.setup_type in [SetupTypes.SIGNAL, SetupTypes.SIGNAL_AND_TRACKER]:
274
+ # - a relisted symbol has data from its first listing on
262
275
  listed_dates = [
263
- to_utc_naive(to_timestamp(instrument.listed_at))
276
+ to_utc_naive(to_timestamp(listing.listed_at))
264
277
  for instrument in setup.instruments
265
- if instrument.listed_at is not None
278
+ for listing in lookup.find_listings(instrument.exchange, instrument.symbol, instrument.market_type)
279
+ or [instrument]
280
+ if listing.listed_at is not None
266
281
  ]
267
282
  if listed_dates:
268
283
  start = max(start, min(listed_dates))
@@ -8,6 +8,7 @@ import pandas as pd
8
8
 
9
9
  from qubx import logger
10
10
  from qubx.core.basics import (
11
+ DEFAULT_MAINTENANCE_MARGIN,
11
12
  STABLE_CURRENCIES,
12
13
  Balance,
13
14
  CtrlChannel,
@@ -114,7 +115,10 @@ class SimulationSetup:
114
115
  signal_timeframe: str = "1Min"
115
116
  accurate_stop_orders_execution: bool = False
116
117
  enable_funding: bool = False
118
+ maint_margin_rate: float = DEFAULT_MAINTENANCE_MARGIN
117
119
  base_currencies: dict[str, str] = field(init=False, default_factory=dict)
120
+ # - account -> the markets it trades, its own first, for accounts trading more; see set_accounts
121
+ account_markets: dict[str, tuple[str, ...]] = field(init=False, default_factory=dict)
118
122
 
119
123
  def __post_init__(self) -> None:
120
124
  # Per-exchange base currency: explicit mapping wins, else the venue's shared settle
@@ -146,6 +150,26 @@ class SimulationSetup:
146
150
  per_exchange = float(self.capital) / n if n > 0 else float(self.capital)
147
151
  self.capital = {exchange: per_exchange for exchange in self.exchanges}
148
152
 
153
+ def set_accounts(self, accounts: dict[str, list[str]] | dict[str, tuple[str, ...]]) -> None:
154
+ """Trade each account's further markets on that account: one capital pool, their capital
155
+ merged into the account's. Each market keeps its own simulated exchange (fees, execution).
156
+ Markets not traded in this setup are ignored."""
157
+ assert isinstance(self.capital, dict)
158
+ for account, markets in accounts.items():
159
+ further = [m for m in markets if m != account and m in self.exchanges]
160
+ if not further:
161
+ continue
162
+ if account not in self.exchanges:
163
+ raise SimulationConfigError(f"account {account}: not traded in this setup")
164
+ for market in further:
165
+ if self.base_currencies[market] != self.base_currencies[account]:
166
+ raise SimulationConfigError(
167
+ f"account {account}: market {market} has another base currency "
168
+ f"({self.base_currencies[market]} vs {self.base_currencies[account]})"
169
+ )
170
+ self.capital[account] = self.capital.get(account, 0.0) + self.capital.pop(market, 0.0)
171
+ self.account_markets[account] = (account, *further)
172
+
149
173
  def __str__(self) -> str:
150
174
  return f"{self.name} {self.setup_type} capital {self.capital} {self.base_currency} for [{','.join(map(lambda x: x.symbol, self.instruments))}] @ {self.exchanges}[{self.commissions}]"
151
175
 
@@ -405,6 +429,7 @@ def recognize_simulation_configuration(
405
429
  accurate_stop_orders_execution: bool,
406
430
  run_separate_instruments: bool = False,
407
431
  enable_funding: bool = False,
432
+ maint_margin_rate: float = DEFAULT_MAINTENANCE_MARGIN,
408
433
  ) -> list[SimulationSetup]:
409
434
  """
410
435
  Recognize and create setups based on the provided simulation configuration.
@@ -426,6 +451,7 @@ def recognize_simulation_configuration(
426
451
  - accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
427
452
  - run_separate_instruments (bool): If True, creates separate setups for each instrument.
428
453
  - enable_funding (bool): If True, enables funding rate simulation, default is False.
454
+ - maint_margin_rate (float): Maintenance margin as a fraction of notional for simulated positions.
429
455
 
430
456
  Returns:
431
457
  - list[SimulationSetup]: A list of SimulationSetup objects, each representing a
@@ -447,7 +473,7 @@ def recognize_simulation_configuration(
447
473
  recognize_simulation_configuration(
448
474
  _n + n, v, instruments, exchanges, capital, basic_currency, commissions,
449
475
  signal_timeframe, accurate_stop_orders_execution, run_separate_instruments,
450
- enable_funding
476
+ enable_funding, maint_margin_rate
451
477
  )
452
478
  )
453
479
 
@@ -468,14 +494,14 @@ def recognize_simulation_configuration(
468
494
  if run_separate_instruments:
469
495
  # Create separate setups for each instrument
470
496
  for instrument in setup_instruments:
471
- _s1 = c1[instrument.symbol] if isinstance(_s, pd.DataFrame) else _s
497
+ _s1 = _s[instrument.symbol] if isinstance(_s, pd.DataFrame) else _s
472
498
  r.append(
473
499
  SimulationSetup(
474
500
  _t, f"{name}/{instrument.symbol}", _s1, c1, # type: ignore
475
501
  [instrument],
476
502
  exchanges, capital, basic_currency, commissions,
477
503
  signal_timeframe, accurate_stop_orders_execution,
478
- enable_funding
504
+ enable_funding, maint_margin_rate
479
505
  )
480
506
  )
481
507
  else:
@@ -485,7 +511,7 @@ def recognize_simulation_configuration(
485
511
  setup_instruments,
486
512
  exchanges, capital, basic_currency, commissions,
487
513
  signal_timeframe, accurate_stop_orders_execution,
488
- enable_funding
514
+ enable_funding, maint_margin_rate
489
515
  )
490
516
  )
491
517
  else:
@@ -495,7 +521,7 @@ def recognize_simulation_configuration(
495
521
  # name + "/" + str(j), s, instruments, exchange, capital, basic_currency, commissions
496
522
  name, s, instruments, exchanges, capital, basic_currency, commissions, # type: ignore
497
523
  signal_timeframe, accurate_stop_orders_execution, run_separate_instruments,
498
- enable_funding
524
+ enable_funding, maint_margin_rate
499
525
  )
500
526
  )
501
527
 
@@ -509,7 +535,7 @@ def recognize_simulation_configuration(
509
535
  f"{name}/{instrument.symbol}", configs, None, [instrument],
510
536
  exchanges, capital, basic_currency, commissions,
511
537
  signal_timeframe, accurate_stop_orders_execution,
512
- enable_funding
538
+ enable_funding, maint_margin_rate
513
539
  )
514
540
  )
515
541
  else:
@@ -519,7 +545,7 @@ def recognize_simulation_configuration(
519
545
  name, configs, None, instruments,
520
546
  exchanges, capital, basic_currency, commissions,
521
547
  signal_timeframe, accurate_stop_orders_execution,
522
- enable_funding
548
+ enable_funding, maint_margin_rate
523
549
  )
524
550
  )
525
551
 
@@ -538,7 +564,7 @@ def recognize_simulation_configuration(
538
564
  f"{name}/{instrument.symbol}", _c1, None, [instrument],
539
565
  exchanges, capital, basic_currency, commissions,
540
566
  signal_timeframe, accurate_stop_orders_execution,
541
- enable_funding
567
+ enable_funding, maint_margin_rate
542
568
  )
543
569
  )
544
570
  else:
@@ -548,7 +574,7 @@ def recognize_simulation_configuration(
548
574
  name, c1, None, setup_instruments,
549
575
  exchanges, capital, basic_currency, commissions,
550
576
  signal_timeframe, accurate_stop_orders_execution,
551
- enable_funding
577
+ enable_funding, maint_margin_rate
552
578
  )
553
579
  )
554
580
 
@@ -91,6 +91,7 @@ from qubx.utils.time import to_timedelta
91
91
 
92
92
  from .exceptions import CcxtSymbolNotRecognized
93
93
  from .exchange_manager import ExchangeManager
94
+ from .replace import Replacement
94
95
  from .utils import (
95
96
  FRAMEWORK_ONLY_OPTIONS,
96
97
  ccxt_convert_balance,
@@ -169,6 +170,25 @@ class _LeverageInfo:
169
170
  margin_mode: str | None = None
170
171
 
171
172
 
173
+ @dataclass(frozen=True)
174
+ class _ReplaceRequest:
175
+ """An amend to run as cancel + recreate, read off the Order synchronously in update_order."""
176
+
177
+ instrument: Instrument
178
+ client_order_id: str | None
179
+ venue_order_id: str | None
180
+ side: str
181
+ order_type: str
182
+ time_in_force: str
183
+ post_only: bool
184
+ reduce_only: bool
185
+ price: float | None
186
+ total: float
187
+ local_filled: float
188
+ requested_price: float | None
189
+ requested_total: float | None
190
+
191
+
172
192
  _VENUE_VERDICT_ERRORS: tuple[type[Exception], ...] = (
173
193
  ccxt.InsufficientFunds,
174
194
  ccxt.OrderNotFillable,
@@ -223,6 +243,9 @@ class CcxtConnector(ChannelEmitter):
223
243
  # make_client_id actually produces, so producer and classifier can never drift.
224
244
  cid_framework_prefix: str = FRAMEWORK_CID_PREFIX
225
245
 
246
+ # Markets an account of this venue can trade besides its own (ExchangePlugin.extra_markets).
247
+ EXTRA_MARKETS: tuple[str, ...] = ()
248
+
226
249
  # Whether the venue's ACCOUNT_UPDATE `a.B[].wb` is the ACCOUNT wallet balance.
227
250
  # True on plain UM/CM, where the futures wallet is the account. False on venues
228
251
  # whose stream reports a SUB-wallet (Binance PM) — there an absolute push would
@@ -269,6 +292,8 @@ class CcxtConnector(ChannelEmitter):
269
292
  self._leverage_future: Any = None
270
293
  # - venue leverage state per ccxt symbol, refreshed by _leverage_poll_loop
271
294
  self._leverage_cache: dict[str, _LeverageInfo] = {}
295
+ # - client id -> its amend-by-cancel+recreate record (see replace.py); touched on the loop only
296
+ self._replacements: dict[str, Replacement] = {}
272
297
 
273
298
  # Re-subscribe the account WS stream + resync against venue truth after the
274
299
  # ExchangeManager swaps in a fresh exchange (the running _subscribe_executions
@@ -396,6 +421,9 @@ class CcxtConnector(ChannelEmitter):
396
421
  if k in FRAMEWORK_ONLY_OPTIONS:
397
422
  continue
398
423
  payload["params"].setdefault(k, v)
424
+ if instrument.is_spot():
425
+ # - spot has no reduce-only (Binance PM margin rejects it); executors may still set it
426
+ payload["params"].pop("reduceOnly", None)
399
427
 
400
428
  self._spawn(self._submit_async(instrument, request.client_id, payload))
401
429
 
@@ -483,6 +511,13 @@ class CcxtConnector(ChannelEmitter):
483
511
  outcome (the cancel may still have landed), so the order is left inflight for
484
512
  AM to reconcile rather than terminal-rejected.
485
513
  """
514
+ if client_order_id is not None and (r := self._replacements.get(client_order_id)) is not None:
515
+ if r.in_flight:
516
+ # - the replace resolves it: no recreate, or a cancel of the recreated order
517
+ r.cancel_requested = True
518
+ return
519
+ if venue_order_id in r.superseded and r.current_vid is not None:
520
+ venue_order_id = r.current_vid
486
521
  try:
487
522
  # Outside _cancel_with_retry: one slot per operation, and its broad handlers would eat the timeout.
488
523
  await self._acquire_endpoint_budget("cancel_order")
@@ -713,6 +748,35 @@ class CcxtConnector(ChannelEmitter):
713
748
  f"update_order for {order.client_order_id}: effective amend amount "
714
749
  f"{wire_amount} <= 0 (total {total}, filled {order.filled_quantity})"
715
750
  )
751
+ if not self._amends_in_place(order.instrument):
752
+ if not self._replaces_with_same_client_id:
753
+ # - no in-place amend and no verified emulation: refuse without touching the order
754
+ self._emit_update_rejected(
755
+ order.client_order_id,
756
+ order.venue_order_id,
757
+ ccxt.NotSupported(f"{self.exchange_name} cannot amend {order.instrument.symbol} orders"),
758
+ )
759
+ return
760
+ self._spawn(
761
+ self._replace_async(
762
+ _ReplaceRequest(
763
+ instrument=order.instrument,
764
+ client_order_id=order.client_order_id,
765
+ venue_order_id=order.venue_order_id,
766
+ side=order.side,
767
+ order_type=str(order.type),
768
+ time_in_force=order.time_in_force,
769
+ post_only=order.post_only or str(order.time_in_force).upper() == "GTX",
770
+ reduce_only=order.reduce_only,
771
+ price=wire_price,
772
+ total=total,
773
+ local_filled=order.filled_quantity,
774
+ requested_price=price,
775
+ requested_total=quantity,
776
+ )
777
+ )
778
+ )
779
+ return
716
780
  self._spawn(
717
781
  self._update_async(
718
782
  order.client_order_id,
@@ -750,12 +814,7 @@ class CcxtConnector(ChannelEmitter):
750
814
  vid = venue_order_id
751
815
  try:
752
816
  await self._acquire_endpoint_budget("edit_order")
753
- if self._em.exchange.has.get("editOrder", False):
754
- r = await self._edit_order_direct(
755
- client_order_id, vid, symbol, side, order_type, wire_price, wire_amount
756
- )
757
- else:
758
- r = await self._update_via_cancel_recreate(client_order_id, venue_order_id, wire_price, wire_amount)
817
+ r = await self._edit_order_direct(client_order_id, vid, symbol, side, order_type, wire_price, wire_amount)
759
818
  except RateLimitGateTimeout as e:
760
819
  self._emit_update_rejected(client_order_id, venue_order_id, e)
761
820
  return
@@ -829,18 +888,202 @@ class CcxtConnector(ChannelEmitter):
829
888
  params={},
830
889
  )
831
890
 
832
- async def _update_via_cancel_recreate(
833
- self, client_order_id: str | None, venue_order_id: str | None, price: float | None, quantity: float | None
834
- ) -> dict[str, Any] | None:
835
- # Raise BEFORE cancelling: cancelling first and then failing to recreate would leave
836
- # the order DEAD at the venue while the strategy is told only "update rejected, order
837
- # still alive". The AM now passes the whole Order, so wiring a real cancel+recreate
838
- # (thread the order's side/type/tif + original price/qty) is straightforward — but it's
839
- # deferred to the recreate follow-up. Until then, reject without touching the live order.
840
- # TODO(account-mgmt): wire the recreate from the passed Order.
841
- # TODO(account-mgmt): this path is itself a REPLACEMENT — when wired, size the recreate
842
- # at total - filled, not the total-dialect `quantity` (wire_amount) it currently receives.
843
- raise ccxt.NotSupported("cancel+recreate update is not yet wired for editOrder-less venues")
891
+ # - the venue lets a cancelled order's client id be reused by a new order (unique among OPEN
892
+ # orders only), so an amend can be emulated as cancel + recreate; opt-in per venue, verified live
893
+ _replaces_with_same_client_id: bool = False
894
+
895
+ def _amends_in_place(self, instrument: Instrument) -> bool:
896
+ """True when the venue modifies an order of ``instrument`` in place; else ``update_order``
897
+ is emulated as cancel + recreate under the same client id (``_replace_async``)."""
898
+ return bool(self._em.exchange.has.get("editOrder", False))
899
+
900
+ async def _replace_async(self, req: "_ReplaceRequest") -> None:
901
+ """Amend as cancel + recreate, keeping the client id (see replace.py for the event rules).
902
+
903
+ The order stays live unless the cancel lands: any refusal before it is an update reject.
904
+ After it, the order is either recreated (OrderUpdatedEvent carrying the new venue id) or
905
+ it has ended (OrderCanceledEvent: nothing left to place, recreate refused, or the strategy
906
+ cancelled meanwhile). The recreate is sized from the cancel response's executed quantity,
907
+ the venue's final figure, not the local one, which may trail the fill stream.
908
+ """
909
+ cid, vid, instrument = req.client_order_id, req.venue_order_id, req.instrument
910
+ symbol = instrument_to_ccxt_symbol(instrument)
911
+ if vid is None:
912
+ self._emit_update_rejected(cid, vid, ccxt.NotSupported("order not acknowledged yet; amend after the ack"))
913
+ return
914
+ record = self._replacements.get(cid)
915
+ if record is not None and record.in_flight:
916
+ self._emit_update_rejected(cid, vid, ccxt.NotSupported("a replace of this order is already in flight"))
917
+ return
918
+ if record is not None and vid in record.superseded and record.current_vid is not None:
919
+ vid = record.current_vid
920
+ # - validate the replacement before touching the live order: a refusal now leaves it as is
921
+ try:
922
+ self._replacement_payload(req, req.total - req.local_filled)
923
+ except (InvalidOrderParameters, ccxt.BadRequest) as e:
924
+ self._emit_update_rejected(cid, vid, e)
925
+ return
926
+ if record is None:
927
+ record = self._replacements[cid] = Replacement(instrument=instrument, cancelling_vid=None)
928
+ record.begin(vid)
929
+
930
+ try:
931
+ await self._acquire_endpoint_budget("cancel_order")
932
+ ok, response = await self._cancel_with_retry(cid, vid, symbol)
933
+ except RateLimitGateTimeout as e:
934
+ self._end_replace(cid, record, vid, symbol)
935
+ self._emit_update_rejected(cid, vid, e)
936
+ return
937
+ except ccxt.NetworkError as e:
938
+ # - UNKNOWN whether the cancel landed: no recreate (two live orders would be worse than
939
+ # none); stop filtering so the venue's own report of the old order gets through
940
+ self._end_replace(cid, record, vid, symbol)
941
+ logger.warning(f"[{self.exchange_name}] replace of {cid}: cancel outcome unknown ({e}); leaving inflight")
942
+ return
943
+ if ok is False:
944
+ self._end_replace(cid, record, vid, symbol)
945
+ self._emit_update_rejected(cid, vid, ccxt.OperationRejected("venue refused the cancel of the replace"))
946
+ return
947
+ if ok is None:
948
+ # - already gone (filled, or ended elsewhere): its terminal report is the truth, fetch it
949
+ self._end_replace(cid, record)
950
+ self._emit_update_rejected(cid, vid, ccxt.OrderNotFound("order no longer open; not replaced"))
951
+ self._spawn(self._order_status_async(cid, vid, symbol, instrument))
952
+ return
953
+
954
+ filled = float((response or {}).get("filled") or 0.0)
955
+ cost = float((response or {}).get("cost") or 0.0)
956
+ record.cancelled(vid, filled, cost)
957
+ remaining = req.total - record.filled_before
958
+ if record.cancel_requested:
959
+ self._finish_replace_as_cancel(cid, vid, record, "cancel requested during the replace")
960
+ return
961
+ try:
962
+ payload = self._replacement_payload(req, remaining)
963
+ except (InvalidOrderParameters, ccxt.BadRequest) as e:
964
+ self._finish_replace_as_cancel(cid, vid, record, f"remaining {remaining} not placeable: {e}")
965
+ return
966
+
967
+ try:
968
+ await self._acquire_endpoint_budget("create_order")
969
+ created = await self._em.exchange.create_order(**payload)
970
+ except (RateLimitGateTimeout, *_VENUE_VERDICT_ERRORS) as e:
971
+ # - verdicts first: rate-limit / maintenance are NetworkError subclasses (see _submit_async)
972
+ self._report_order_limit_hit(e)
973
+ self._finish_replace_as_cancel(cid, vid, record, f"recreate refused: {e}")
974
+ return
975
+ except ccxt.NetworkError as e:
976
+ logger.warning(f"[{self.exchange_name}] replace of {cid}: recreate outcome unknown ({e}); looking it up")
977
+ await self._resolve_unknown_recreate(req, vid, record)
978
+ return
979
+ except Exception as e: # noqa: BLE001
980
+ logger.error(f"[{self.exchange_name}] replace of {cid}: unexpected recreate error {e!r}; looking it up")
981
+ await self._resolve_unknown_recreate(req, vid, record)
982
+ return
983
+ if created is None or created.get("id") is None:
984
+ await self._resolve_unknown_recreate(req, vid, record)
985
+ return
986
+ self._recreated(req, record, str(created["id"]))
987
+
988
+ def _replacement_payload(self, req: "_ReplaceRequest", amount: float) -> dict[str, Any]:
989
+ if amount <= 0:
990
+ raise InvalidOrderParameters(f"nothing left to place ({amount})")
991
+ quote = self._data_provider.get_quote(req.instrument)
992
+ price = req.price
993
+ if req.post_only and quote is not None and price is not None:
994
+ crosses = price >= quote.ask if req.side == "BUY" else price <= quote.bid
995
+ if crosses:
996
+ # - a post-only replacement at a crossing price would be refused AFTER the cancel
997
+ raise InvalidOrderParameters(f"post-only price {price} crosses the book ({quote.bid}/{quote.ask})")
998
+ payload = prepare_ccxt_order_payload(
999
+ instrument=req.instrument,
1000
+ order_side=req.side,
1001
+ order_type=req.order_type,
1002
+ amount=amount,
1003
+ price=price,
1004
+ client_id=req.client_order_id,
1005
+ time_in_force=req.time_in_force,
1006
+ quote=quote,
1007
+ reduce_only=req.reduce_only,
1008
+ post_only=req.post_only,
1009
+ )
1010
+ if req.instrument.is_spot():
1011
+ payload["params"].pop("reduceOnly", None)
1012
+ return payload
1013
+
1014
+ def _recreated(self, req: "_ReplaceRequest", record: Replacement, new_vid: str) -> None:
1015
+ record.recreated(new_vid)
1016
+ logger.debug(f"[{self.exchange_name}] replace of {req.client_order_id}: now venue order {new_vid}")
1017
+ self.send(
1018
+ OrderUpdatedEvent(
1019
+ instrument=None,
1020
+ client_order_id=req.client_order_id,
1021
+ venue_order_id=new_vid,
1022
+ new_price=req.requested_price,
1023
+ new_quantity=req.requested_total,
1024
+ )
1025
+ )
1026
+ if record.cancel_requested:
1027
+ self._spawn(self._cancel_async(req.client_order_id, new_vid, instrument_to_ccxt_symbol(req.instrument)))
1028
+
1029
+ async def _resolve_unknown_recreate(self, req: "_ReplaceRequest", old_vid: str, record: Replacement) -> None:
1030
+ """The recreate may or may not have landed: look the client id up (the venue returns its
1031
+ newest order) — a new venue id means it did, the cancelled one means it did not."""
1032
+ cid = req.client_order_id
1033
+ assert cid is not None
1034
+ symbol = instrument_to_ccxt_symbol(req.instrument)
1035
+ for attempt in range(3):
1036
+ await asyncio.sleep(1.0 + attempt)
1037
+ try:
1038
+ raw = await self._em.exchange.fetch_order_with_client_order_id(cid, symbol)
1039
+ except ccxt.OrderNotFound:
1040
+ break
1041
+ except Exception as e: # noqa: BLE001
1042
+ logger.warning(f"[{self.exchange_name}] replace of {cid}: lookup failed ({e}); retrying")
1043
+ continue
1044
+ found = None if raw is None else raw.get("id")
1045
+ if found is not None and str(found) != old_vid and str(found) not in record.superseded:
1046
+ self._recreated(req, record, str(found))
1047
+ self._handle_ws_order(raw)
1048
+ return
1049
+ break
1050
+ else:
1051
+ # - could not tell: stop filtering and leave the order to the reconciler
1052
+ logger.error(
1053
+ f"[{self.exchange_name}] replace of {cid}: recreate outcome still unknown; reconciler takes over"
1054
+ )
1055
+ # - a recreated order's venue id is unknown here: a pending cancel goes by client id
1056
+ self._end_replace(cid, record, None, symbol)
1057
+ return
1058
+ self._finish_replace_as_cancel(cid, old_vid, record, "recreate did not land")
1059
+
1060
+ def _finish_replace_as_cancel(self, cid: str | None, vid: str, record: Replacement, why: str) -> None:
1061
+ logger.warning(f"[{self.exchange_name}] replace of {cid} ended the order: {why}")
1062
+ self._end_replace(cid, record)
1063
+ self.send(OrderCanceledEvent(instrument=None, client_order_id=cid, venue_order_id=vid))
1064
+
1065
+ def _end_replace(
1066
+ self, cid: str | None, record: Replacement, live_vid: str | None = None, symbol: str | None = None
1067
+ ) -> None:
1068
+ """Stop the replace. With ``symbol`` given the order may still be live at the venue (``live_vid``, or
1069
+ unknown): a cancel the strategy asked for meanwhile, held back by the replace, is sent now."""
1070
+ # - kept only while a live recreated order needs its reports rewritten
1071
+ record.abandoned()
1072
+ if cid is not None and (record.current_vid is None or not record.superseded):
1073
+ self._replacements.pop(cid, None)
1074
+ if symbol is not None and record.cancel_requested:
1075
+ record.cancel_requested = False
1076
+ self._spawn(self._cancel_async(cid, live_vid, symbol))
1077
+
1078
+ def _replaced_view(self, order: Order) -> Order | None:
1079
+ """Route one venue report of an order through its replace record, if it has one."""
1080
+ record = self._replacements.get(order.client_order_id) if order.client_order_id else None
1081
+ if record is None:
1082
+ return order
1083
+ viewed = record.view(order)
1084
+ if viewed is not None and record.is_done(viewed):
1085
+ self._replacements.pop(order.client_order_id, None)
1086
+ return viewed
844
1087
 
845
1088
  def _emit_update_rejected(self, client_order_id: str | None, venue_order_id: str | None, error: Exception) -> None:
846
1089
  logger.warning(f"[{self.exchange_name}] Update for {client_order_id or venue_order_id} rejected: {error}")
@@ -1536,7 +1779,9 @@ class CcxtConnector(ChannelEmitter):
1536
1779
  logger.warning(f"[{self.exchange_name}] WS order for unknown symbol {raw.get('symbol')}; skipped")
1537
1780
  return
1538
1781
  order = ccxt_convert_order_info(instrument, raw, framework_prefix=self.cid_framework_prefix)
1539
- self._emit_order_events(instrument, order, raw)
1782
+ if (viewed := self._replaced_view(order)) is None:
1783
+ return
1784
+ self._emit_order_events(instrument, viewed, raw)
1540
1785
 
1541
1786
  def _emit_order_events(self, instrument: Instrument, order: Order, raw: dict[str, Any]) -> None:
1542
1787
  """Map a converted order's status to the typed lifecycle event(s).
@@ -1964,6 +2209,15 @@ class CcxtConnector(ChannelEmitter):
1964
2209
  logger.debug(f"[{self.exchange_name}] snapshot: no trigger open-orders surface ({e}); regular only")
1965
2210
  return []
1966
2211
 
2212
+ async def _fetch_extra_open_orders(self) -> list[dict]:
2213
+ """Open orders the venue-wide ``fetch_open_orders`` does not return: trigger orders here;
2214
+ a venue with a further order surface (Binance PM margin) adds it."""
2215
+ return await self._fetch_trigger_open_orders()
2216
+
2217
+ async def _balance_positions(self, raw_balance: dict[str, Any]) -> list[Position]:
2218
+ """Positions the venue reports as balances rather than positions (spot holdings); none here."""
2219
+ return []
2220
+
1967
2221
  def _merge_open_orders(self, raw_orders: list[dict], raw_trigger_orders: list[dict]) -> list[Order]:
1968
2222
  """Merge regular + trigger raw open orders into framework Orders (dedup by venue id).
1969
2223
 
@@ -1983,6 +2237,9 @@ class CcxtConnector(ChannelEmitter):
1983
2237
  )
1984
2238
  continue
1985
2239
  order = ccxt_convert_order_info(instrument, raw, framework_prefix=self.cid_framework_prefix)
2240
+ if (viewed := self._replaced_view(order)) is None:
2241
+ continue
2242
+ order = viewed
1986
2243
  key = order.venue_order_id or order.client_order_id
1987
2244
  if key is not None and key in seen:
1988
2245
  continue
@@ -2028,7 +2285,7 @@ class CcxtConnector(ChannelEmitter):
2028
2285
  if include_orders:
2029
2286
  raw_orders, raw_trigger_orders, raw_positions, raw_balance = await asyncio.gather(
2030
2287
  ex.fetch_open_orders(),
2031
- self._fetch_trigger_open_orders(),
2288
+ self._fetch_extra_open_orders(),
2032
2289
  ex.fetch_positions(),
2033
2290
  ex.fetch_balance(),
2034
2291
  )
@@ -2038,6 +2295,7 @@ class CcxtConnector(ChannelEmitter):
2038
2295
  open_orders = None # - not observed this tick -> reconcile skips order diffing
2039
2296
  positions = ccxt_convert_positions(raw_positions, ex.name, ex.markets)
2040
2297
  await self._fill_leverage_settings(positions)
2298
+ positions += await self._balance_positions(raw_balance)
2041
2299
  balances = self._convert_balances(raw_balance)
2042
2300
  figures = self._extract_venue_figures(raw_balance)
2043
2301
  except Exception as e: # noqa: BLE001 — AM retries on its next snapshot tick