Qubx 3.6.1__tar.gz → 3.6.2.dev1__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 (328) hide show
  1. {qubx-3.6.1 → qubx-3.6.2.dev1}/PKG-INFO +1 -1
  2. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/_version.py +2 -2
  3. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/commands.py +6 -1
  4. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/diffs.py +10 -1
  5. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/state.py +9 -1
  6. qubx-3.6.2.dev1/src/qubx/core/boot.py +103 -0
  7. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/fit_executor.py +3 -0
  8. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/initializer.py +7 -0
  9. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/interfaces.py +16 -0
  10. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/processing.py +142 -75
  11. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restarts/state_resolvers.py +55 -36
  12. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/runner.py +0 -4
  13. {qubx-3.6.1 → qubx-3.6.2.dev1}/.gitignore +0 -0
  14. {qubx-3.6.1 → qubx-3.6.2.dev1}/LICENSE +0 -0
  15. {qubx-3.6.1 → qubx-3.6.2.dev1}/README.md +0 -0
  16. {qubx-3.6.1 → qubx-3.6.2.dev1}/pyproject.toml +0 -0
  17. {qubx-3.6.1 → qubx-3.6.2.dev1}/scripts/build.py +0 -0
  18. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/__init__.py +0 -0
  19. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/_nb_magic.py +0 -0
  20. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/__init__.py +0 -0
  21. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/connector.py +0 -0
  22. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/data.py +0 -0
  23. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/iteratedstream.py +0 -0
  24. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/management.py +0 -0
  25. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/ome.py +0 -0
  26. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/optimization.py +0 -0
  27. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/runner.py +0 -0
  28. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/sentinels.py +0 -0
  29. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/simulated_data.py +0 -0
  30. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/simulated_exchange.py +0 -0
  31. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/simulator.py +0 -0
  32. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/transfers.py +0 -0
  33. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/utils.py +0 -0
  34. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/__init__.py +0 -0
  35. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/deploy.py +0 -0
  36. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/misc.py +0 -0
  37. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/release.py +0 -0
  38. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/resolver.py +0 -0
  39. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/s3.py +0 -0
  40. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/theme.py +0 -0
  41. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/tui.py +0 -0
  42. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/user_config.py +0 -0
  43. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/config.py +0 -0
  44. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/__init__.py +0 -0
  45. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/__init__.py +0 -0
  46. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  47. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  48. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  49. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/connector.py +0 -0
  50. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/data.py +0 -0
  51. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  52. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  53. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  54. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/_two_stream.py +0 -0
  55. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  56. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/binance/connector.py +0 -0
  57. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  58. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  59. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/bitfinex/connector.py +0 -0
  60. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  61. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  62. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  63. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  64. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  65. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  66. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/connector.py +0 -0
  67. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  68. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/factory.py +0 -0
  69. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  70. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  71. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  72. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  73. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  74. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  75. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  76. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  77. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  78. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  79. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/plugin.py +0 -0
  80. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
  81. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  82. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  83. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  84. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/utils.py +0 -0
  85. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  86. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/plugin.py +0 -0
  87. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/registry.py +0 -0
  88. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/tardis/data.py +0 -0
  89. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/tardis/plugin.py +0 -0
  90. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/tardis/utils.py +0 -0
  91. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/__init__.py +0 -0
  92. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/builtin.py +0 -0
  93. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/decorator.py +0 -0
  94. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/executor.py +0 -0
  95. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/interfaces.py +0 -0
  96. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/server.py +0 -0
  97. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/types.py +0 -0
  98. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/__init__.py +0 -0
  99. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/__init__.py +0 -0
  100. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/config.py +0 -0
  101. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/manager.py +0 -0
  102. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/reconciler.py +0 -0
  103. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/reducer.py +0 -0
  104. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/state_machine.py +0 -0
  105. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/basics.py +0 -0
  106. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/connector.py +0 -0
  107. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/context.py +0 -0
  108. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/detectors/__init__.py +0 -0
  109. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/detectors/delisting.py +0 -0
  110. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/detectors/stale.py +0 -0
  111. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/errors.py +0 -0
  112. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/events.py +0 -0
  113. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/exceptions.py +0 -0
  114. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/fit_context.py +0 -0
  115. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/helpers.py +0 -0
  116. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/instrument_service.py +0 -0
  117. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/loggers.py +0 -0
  118. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/lookups.py +0 -0
  119. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/metrics.py +0 -0
  120. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/__init__.py +0 -0
  121. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/instrument_service.py +0 -0
  122. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/market.py +0 -0
  123. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/subscription.py +0 -0
  124. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/trading.py +0 -0
  125. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/universe.py +0 -0
  126. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/utils.py +0 -0
  127. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/series.pxd +0 -0
  128. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/series.pyi +0 -0
  129. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/series.pyx +0 -0
  130. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/status.py +0 -0
  131. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/utils.pyi +0 -0
  132. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/utils.pyx +0 -0
  133. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/__init__.py +0 -0
  134. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/cache.py +0 -0
  135. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/containers.py +0 -0
  136. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/guards.py +0 -0
  137. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/registry.py +0 -0
  138. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storage.py +0 -0
  139. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/ccxt.py +0 -0
  140. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/csv.py +0 -0
  141. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/handy.py +0 -0
  142. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/multi.py +0 -0
  143. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/questdb.py +0 -0
  144. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/stub.py +0 -0
  145. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/utils.py +0 -0
  146. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/transformers.py +0 -0
  147. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/__init__.py +0 -0
  148. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/base.py +0 -0
  149. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/composite.py +0 -0
  150. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/csv.py +0 -0
  151. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/indicator.py +0 -0
  152. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/inmemory.py +0 -0
  153. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/prometheus.py +0 -0
  154. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/questdb.py +0 -0
  155. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/__init__.py +0 -0
  156. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/composite.py +0 -0
  157. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/__init__.py +0 -0
  158. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/base.py +0 -0
  159. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/incremental.py +0 -0
  160. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/slack.py +0 -0
  161. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/target_position.py +0 -0
  162. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/redis_streams.py +0 -0
  163. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/slack.py +0 -0
  164. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/gathering/simplest.py +0 -0
  165. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/health/__init__.py +0 -0
  166. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/health/base.py +0 -0
  167. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/health/dummy.py +0 -0
  168. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/__init__.py +0 -0
  169. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/csv.py +0 -0
  170. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/factory.py +0 -0
  171. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/inmemory.py +0 -0
  172. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/mongo.py +0 -0
  173. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/postgres.py +0 -0
  174. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/notifications/__init__.py +0 -0
  175. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/notifications/composite.py +0 -0
  176. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/notifications/slack.py +0 -0
  177. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/notifications/throttler.py +0 -0
  178. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/pandaz/__init__.py +0 -0
  179. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/pandaz/stats.py +0 -0
  180. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/pandaz/ta.py +0 -0
  181. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/pandaz/utils.py +0 -0
  182. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/plugins/__init__.py +0 -0
  183. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/plugins/loader.py +0 -0
  184. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/__init__.py +0 -0
  185. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/backend.py +0 -0
  186. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/config.py +0 -0
  187. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/engine.py +0 -0
  188. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/ip_resolver.py +0 -0
  189. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/manager.py +0 -0
  190. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/pools.py +0 -0
  191. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/redis_backend.py +0 -0
  192. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/resilient.py +0 -0
  193. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/_build.py +0 -0
  194. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/crypto-fees.ini +0 -0
  195. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  196. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  197. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  198. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  199. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  200. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  201. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  202. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  203. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  204. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  205. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  206. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restarts/__init__.py +0 -0
  207. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restarts/time_finders.py +0 -0
  208. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/__init__.py +0 -0
  209. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/balance.py +0 -0
  210. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/factory.py +0 -0
  211. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/interfaces.py +0 -0
  212. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/position.py +0 -0
  213. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/signal.py +0 -0
  214. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/state.py +0 -0
  215. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/utils.py +0 -0
  216. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/state/__init__.py +0 -0
  217. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/state/dummy.py +0 -0
  218. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/state/redis.py +0 -0
  219. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/state/safe.py +0 -0
  220. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/ta/__init__.py +0 -0
  221. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/ta/indicators.pxd +0 -0
  222. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/ta/indicators.pyi +0 -0
  223. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/ta/indicators.pyx +0 -0
  224. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/__init__.py +0 -0
  225. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/base.py +0 -0
  226. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  227. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/config.yml.j2 +0 -0
  228. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  229. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  230. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  231. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  232. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  233. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/template.yml +0 -0
  234. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  235. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  236. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  237. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  238. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  239. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  240. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.python-version +0 -0
  241. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  242. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  243. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  244. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  245. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/README.md.j2 +0 -0
  246. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  247. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/justfile.j2 +0 -0
  248. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  249. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/research/.gitkeep +0 -0
  250. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  251. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  252. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  253. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/template.yml +0 -0
  254. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  255. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  256. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  257. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/config.yml.j2 +0 -0
  258. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  259. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  260. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  261. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/template.yml +0 -0
  262. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/__init__.py +0 -0
  263. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/advanced.py +0 -0
  264. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/composite.py +0 -0
  265. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/rebalancers.py +0 -0
  266. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/riskctrl.py +0 -0
  267. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/sizers.py +0 -0
  268. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/__init__.py +0 -0
  269. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/_pyxreloader.py +0 -0
  270. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/charting/lookinglass.py +0 -0
  271. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  272. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/charting/orderbook.py +0 -0
  273. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/clock.py +0 -0
  274. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/collections.py +0 -0
  275. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/hft/__init__.py +0 -0
  276. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/hft/numba_utils.py +0 -0
  277. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/hft/orderbook.pyi +0 -0
  278. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/hft/orderbook.pyx +0 -0
  279. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/marketdata/binance.py +0 -0
  280. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/marketdata/ccxt.py +0 -0
  281. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/marketdata/dukas.py +0 -0
  282. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/misc.py +0 -0
  283. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/nonce.py +0 -0
  284. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/numbers_utils.py +0 -0
  285. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/orderbook.py +0 -0
  286. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/__init__.py +0 -0
  287. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/dashboard.py +0 -0
  288. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/data.py +0 -0
  289. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/interfaces.py +0 -0
  290. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  291. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  292. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/questdb.py +0 -0
  293. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/rate_limiter.py +0 -0
  294. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/results.py +0 -0
  295. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/ringbuffer.pxd +0 -0
  296. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/ringbuffer.pyi +0 -0
  297. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/ringbuffer.pyx +0 -0
  298. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/__init__.py +0 -0
  299. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  300. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/accounts.py +0 -0
  301. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/configs.py +0 -0
  302. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/factory.py +0 -0
  303. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/kernel_service.py +0 -0
  304. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/__init__.py +0 -0
  305. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/app.py +0 -0
  306. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/handlers.py +0 -0
  307. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/init_code.py +0 -0
  308. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/kernel.py +0 -0
  309. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  310. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  311. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
  312. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  313. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  314. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/log_filter.py +0 -0
  315. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  316. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  317. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  318. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  319. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/s3.py +0 -0
  320. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/slack.py +0 -0
  321. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/threading.py +0 -0
  322. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/throttler.py +0 -0
  323. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/time.py +0 -0
  324. {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/websocket_manager.py +0 -0
  325. {qubx-3.6.1 → qubx-3.6.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  326. {qubx-3.6.1 → qubx-3.6.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  327. {qubx-3.6.1 → qubx-3.6.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  328. {qubx-3.6.1 → qubx-3.6.2.dev1}/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: 3.6.1
3
+ Version: 3.6.2.dev1
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 = '3.6.1'
22
- __version_tuple__ = version_tuple = (3, 6, 1)
21
+ __version__ = version = '3.6.2.dev1'
22
+ __version_tuple__ = version_tuple = (3, 6, 2, 'dev1')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -332,6 +332,9 @@ def run(
332
332
  help="Write simulation logs to a specific file path.",
333
333
  show_default=True,
334
334
  )
335
+ @click.option(
336
+ "--dev", is_flag=True, default=False, help="Enable dev mode (adds ~/projects to path).", show_default=True
337
+ )
335
338
  def simulate(
336
339
  config_file: Path,
337
340
  start: str | None,
@@ -340,6 +343,7 @@ def simulate(
340
343
  report: str | None,
341
344
  name: str | None,
342
345
  log_file: str | None,
346
+ dev: bool,
343
347
  ):
344
348
  """
345
349
  Simulates the strategy with the given configuration file.
@@ -347,7 +351,8 @@ def simulate(
347
351
  from qubx.utils.misc import add_project_to_system_path, logo
348
352
  from qubx.utils.runner.runner import simulate_strategy
349
353
 
350
- add_project_to_system_path()
354
+ if dev:
355
+ add_project_to_system_path() # Adds ~/projects in dev mode
351
356
  add_project_to_system_path(str(config_file.parent))
352
357
  logo()
353
358
  logger.info(f"Process PID: <g>{os.getpid()}</g>")
@@ -28,6 +28,11 @@ RTOL = 1e-9
28
28
  # under 1 cent is not a reconcilable difference. Absolute, because "1 cent" is absolute.
29
29
  BALANCE_ABS_TOL = 0.01
30
30
 
31
+ # Maintenance margin re-marks with every snapshot's mark price, so RTOL-tight comparison
32
+ # emits a diff per position per poll. Only a material move (leverage/size change) is worth
33
+ # re-syncing; sub-0.5% (and sub-cent) drift is mark noise.
34
+ MARGIN_RTOL = 5e-3
35
+
31
36
 
32
37
  # - Diff atoms ------------------------------------------------------------------ #
33
38
  @dataclass_transform(frozen_default=True, kw_only_default=True)
@@ -69,6 +74,10 @@ def _close_bal(a: float, b: float) -> bool:
69
74
  return abs(a - b) <= BALANCE_ABS_TOL
70
75
 
71
76
 
77
+ def _close_margin(a: float, b: float) -> bool:
78
+ return abs(a - b) <= max(BALANCE_ABS_TOL, MARGIN_RTOL * max(abs(a), abs(b)))
79
+
80
+
72
81
  def _material_pos(position: Position) -> bool:
73
82
  # a position smaller than half a lot is effectively flat — never a presence diff
74
83
  return abs(position.quantity) > _qeps(position.instrument)
@@ -377,7 +386,7 @@ class Differ:
377
386
  if abs(lp.position_avg_price - sp.position_avg_price) > _peps(sp.instrument):
378
387
  diffs.append(PositionAvgPriceMismatch(local=lp, origin=sp))
379
388
 
380
- if not _close_rel(lp.maint_margin, sp.maint_margin):
389
+ if not _close_margin(lp.maint_margin, sp.maint_margin):
381
390
  diffs.append(PositionMarginMismatch(local=lp, origin=sp))
382
391
 
383
392
  for inst, lp in local_pos.items():
@@ -26,6 +26,10 @@ from qubx.core.basics import STABLE_CURRENCIES, Balance, Deal, Instrument, Order
26
26
  # sessions. A re-delivered funding event only needs RECENT buckets to dedup against.
27
27
  _FUNDING_BUCKET_CAP: int = 4096
28
28
 
29
+ # Snapshot balance-apply log level: a total move above this is real money (fill/funding/
30
+ # transfer) and logs INFO; below it (incl. pure free/locked reshuffles) logs DEBUG.
31
+ _BALANCE_LOG_ABS_TOL: float = 0.01
32
+
29
33
 
30
34
  @dataclass
31
35
  class VenueAccountFigures:
@@ -527,7 +531,11 @@ class AccountState:
527
531
  )
528
532
  if changed:
529
533
  old = "—" if existing is None else f"total={existing.total} free={existing.free} locked={existing.locked}"
530
- logger.info(
534
+ # - free/locked reshuffle with total unchanged is margin mark-to-market noise
535
+ # (every poll on a leveraged book); only a real total move is INFO-worthy
536
+ material = existing is None or abs(balance.total - existing.total) > _BALANCE_LOG_ABS_TOL
537
+ log = logger.info if material else logger.debug
538
+ log(
531
539
  f"[{self.exchange}] reconcile: balance <y>{balance.currency}</y> from snapshot -> "
532
540
  f"total=<g>{balance.total}</g> free={balance.free} locked={balance.locked} (was {old})"
533
541
  )
@@ -0,0 +1,103 @@
1
+ from enum import IntEnum
2
+
3
+ from qubx import logger
4
+ from qubx.core.basics import dt_64, td_64
5
+
6
+
7
+ class BootPhase(IntEnum):
8
+ BLOCKED = -1
9
+ WAIT_READY = 0
10
+ ON_START = 1
11
+ RESOLVE = 2
12
+ RESTORE = 3
13
+ WARMUP_FINISHED = 4
14
+ BOOT_FIT = 5
15
+ TRADING = 6
16
+
17
+
18
+ class BootStateMachine:
19
+ """Live-boot phase bookkeeping: current phase, boot-fit retry policy, health gauges.
20
+
21
+ Pure state — the ProcessingManager drives transitions and executes side effects
22
+ (see _advance_boot). BLOCKED is sticky for trading; a later successful fit releases
23
+ it (record_fit_success), a late account snapshot releases WAIT_READY (caller-side).
24
+ """
25
+
26
+ def __init__(self, health_monitor, *, fit_max_attempts: int = 3, fit_retry_delay: td_64 = td_64(60, "s")) -> None:
27
+ self.phase = BootPhase.WAIT_READY
28
+ self.blocked_reason: str | None = None
29
+ self.fit_attempts = 0
30
+ self._health = health_monitor
31
+ self._fit_max_attempts = fit_max_attempts
32
+ self._fit_retry_delay = fit_retry_delay
33
+ self._next_fit_attempt: dt_64 | None = None
34
+ self._sync_alerted = False
35
+
36
+ @property
37
+ def is_trading(self) -> bool:
38
+ return self.phase == BootPhase.TRADING
39
+
40
+ @property
41
+ def is_blocked(self) -> bool:
42
+ return self.phase == BootPhase.BLOCKED
43
+
44
+ def advance(self, phase: BootPhase) -> None:
45
+ if phase == self.phase:
46
+ return
47
+ logger.info(f"<yellow>boot</yellow> :: {self.phase.name} -> <green>{phase.name}</green>")
48
+ self.phase = phase
49
+ self._gauge("boot.state", float(phase))
50
+
51
+ def account_sync_alert(self) -> None:
52
+ if self._sync_alerted:
53
+ return
54
+ self._sync_alerted = True
55
+ self._gauge("boot.account_sync_blocked", 1.0)
56
+ logger.warning("<yellow>Boot blocked: initial account snapshot not applied — holding until synced</yellow>")
57
+
58
+ def account_synced(self) -> None:
59
+ if not self._sync_alerted:
60
+ return
61
+ self._sync_alerted = False
62
+ self._gauge("boot.account_sync_blocked", 0.0)
63
+
64
+ def fit_attempt_allowed(self, now: dt_64) -> bool:
65
+ if self.phase != BootPhase.BOOT_FIT:
66
+ return False
67
+ return self._next_fit_attempt is None or now >= self._next_fit_attempt
68
+
69
+ def record_fit_attempt(self) -> None:
70
+ self.fit_attempts += 1
71
+ self._gauge("boot.fit_attempts", float(self.fit_attempts))
72
+
73
+ def record_fit_failure(self, now: dt_64) -> None:
74
+ if self.is_blocked:
75
+ return
76
+ if self.fit_attempts >= self._fit_max_attempts:
77
+ self.blocked_reason = "boot fit failed"
78
+ self.advance(BootPhase.BLOCKED)
79
+ self._gauge("boot.fit_failed", 1.0)
80
+ logger.error(
81
+ f"<red>Boot fit failed after {self.fit_attempts} attempts — trading blocked, "
82
+ "book unreconciled; a later successful fit will unblock</red>"
83
+ )
84
+ else:
85
+ self._next_fit_attempt = now + self._fit_retry_delay
86
+ logger.warning(
87
+ f"<yellow>Boot fit failed (attempt {self.fit_attempts}/{self._fit_max_attempts}) — retrying</yellow>"
88
+ )
89
+
90
+ def record_fit_success(self) -> None:
91
+ if self.is_blocked:
92
+ self._gauge("boot.fit_failed", 0.0)
93
+ self.blocked_reason = None
94
+ self.advance(BootPhase.TRADING)
95
+
96
+ def record_warmup_finished_failure(self) -> None:
97
+ self._gauge("boot.warmup_finished_failed", 1.0)
98
+
99
+ def _gauge(self, name: str, value: float) -> None:
100
+ try:
101
+ self._health.record_gauge(name, value)
102
+ except Exception:
103
+ logger.exception(f"failed to record {name} gauge")
@@ -47,11 +47,14 @@ class FitCommitData:
47
47
  order (they run on the ProcessorThread, where the fit-thread interception no
48
48
  longer triggers, so each takes today's normal path). ``signals`` are the
49
49
  fit-emitted signals, drained into the normal pipeline in emission order.
50
+ ``error`` carries whatever made the fit fail (strategy raise or framework raise):
51
+ the ProcessorThread latches ``is_on_fit_called`` only when it is None.
50
52
  """
51
53
 
52
54
  ops: tuple[Callable[[], None], ...] = ()
53
55
  signals: tuple[Signal, ...] = ()
54
56
  duration_s: float = 0.0
57
+ error: BaseException | None = None
55
58
 
56
59
 
57
60
  class FitCycleState:
@@ -37,6 +37,7 @@ class BasicStrategyInitializer(IStrategyInitializer):
37
37
  default_instrument_leverage: float | None = None
38
38
  start_time_finder: StartTimeFinderProtocol | None = None
39
39
  mismatch_resolver: StateResolverProtocol | None = None
40
+ _fit_on_start: bool = False
40
41
  auto_subscribe: bool | None = None
41
42
  simulation: bool | None = None
42
43
  subscription_warmup: dict[Any, str] | None = None
@@ -131,6 +132,12 @@ class BasicStrategyInitializer(IStrategyInitializer):
131
132
  def set_state_resolver(self, resolver: StateResolverProtocol) -> None:
132
133
  self.mismatch_resolver = resolver
133
134
 
135
+ def set_fit_on_start(self, enabled: bool = True) -> None:
136
+ self._fit_on_start = enabled
137
+
138
+ def get_fit_on_start(self) -> bool:
139
+ return self._fit_on_start
140
+
134
141
  def set_config(self, key: str, value: Any) -> None:
135
142
  self.config[key] = value
136
143
 
@@ -2311,6 +2311,22 @@ class IStrategyInitializer:
2311
2311
  """
2312
2312
  ...
2313
2313
 
2314
+ def set_fit_on_start(self, enabled: bool = True) -> None:
2315
+ """
2316
+ Force the first on_fit to run in the live context even when a warmup-sim fit ran.
2317
+
2318
+ The warmup simulation fits against sim state; strategies that reconcile positions
2319
+ through their tracker on fit (e.g. buffered trackers) need the first fit to run
2320
+ live, against live positions. Opting in guarantees exactly one live fit after
2321
+ on_warmup_finished and replaces calling ctx.trigger_fit() there (doing both
2322
+ double-fits). No-op when no warmup sim runs — the first fit is live anyway.
2323
+ """
2324
+ ...
2325
+
2326
+ def get_fit_on_start(self) -> bool:
2327
+ """Whether the first on_fit is forced to run in the live context (default False)."""
2328
+ ...
2329
+
2314
2330
  def set_config(self, key: str, value: Any) -> None:
2315
2331
  """
2316
2332
  Set an additional configuration value.
@@ -28,6 +28,7 @@ from qubx.core.basics import (
28
28
  dt_64,
29
29
  td_64,
30
30
  )
31
+ from qubx.core.boot import BootPhase, BootStateMachine
31
32
  from qubx.core.connector import IConnector, IMarketDataSink
32
33
  from qubx.core.detectors import DelistingDetector, StaleDataDetector
33
34
  from qubx.core.errors import BaseErrorEvent
@@ -59,6 +60,7 @@ from qubx.core.interfaces import (
59
60
  )
60
61
  from qubx.core.loggers import StrategyLogging
61
62
  from qubx.core.series import Bar, OrderBook, Quote, Trade
63
+ from qubx.restarts.state_resolvers import StateResolver
62
64
  from qubx.state.dummy import DummyStatePersistence
63
65
  from qubx.trackers.riskctrl import _InitializationStageTracker
64
66
  from qubx.utils.throttler import InstrumentThrottler
@@ -165,7 +167,7 @@ class ProcessingManager(IProcessingManager):
165
167
  _last_data_ready_log_time: dt_64 | None = None
166
168
  _all_instruments_ready_logged: bool = False
167
169
  _account_sync_deadline: dt_64 | None = None # - startup: bound the wait for the initial snapshot
168
- _account_sync_timeout_logged: bool = False
170
+ _boot: BootStateMachine
169
171
  _emitted_signals: list[Signal] = [] # signals that were emitted
170
172
  _active_targets: dict[Instrument, TargetPosition] = {}
171
173
  _pending_no_quote_signals: dict[Instrument, Signal] = {} # signals waiting for quote to arrive
@@ -240,8 +242,8 @@ class ProcessingManager(IProcessingManager):
240
242
  self._last_data_ready_log_time = None
241
243
  self._all_instruments_ready_logged = False
242
244
  self._account_sync_deadline = None
243
- self._account_sync_timeout_logged = False
244
245
  self._emitted_signals = []
246
+ self._boot = BootStateMachine(self._health_monitor)
245
247
 
246
248
  # - special tracker for post-warmup initialization signals
247
249
  self._init_stage_position_tracker = _InitializationStageTracker()
@@ -497,48 +499,14 @@ class ProcessingManager(IProcessingManager):
497
499
  return self._run_strategy_pipeline(event)
498
500
 
499
501
  def _run_strategy_pipeline(self, event: Any) -> bool:
500
- """Warmup/fit checks, strategy callback firing and signal processing — the
502
+ """Boot advance, strategy callback firing and signal processing — the
501
503
  shared tail of the tuple data path (__process_data)."""
502
- if not self._context._strategy_state.is_on_start_called:
503
- self._handle_start()
504
-
505
- if (
506
- not self._context._strategy_state.is_on_warmup_finished_called
507
- and not self._context._strategy_state.is_warmup_in_progress
508
- and not self._warmup_finished_is_running
509
- ):
510
- if self._context.get_warmup_positions() or self._context.get_warmup_orders():
511
- self._handle_state_resolution()
512
-
513
- # Restore tracker and gatherer state if available
514
- restored_state = self._context.get_restored_state()
515
- if restored_state is not None:
516
- self._restore_tracker_and_gatherer_state(restored_state)
517
-
518
- self._handle_warmup_finished()
519
-
520
- # - check if it still didn't call on_fit() for first time
521
- if (
522
- not self._context._strategy_state.is_on_fit_called
523
- and not self._fit_is_running
524
- and not self._warmup_finished_is_running
525
- ):
526
- self._handle_fit(None, "fit", (None, self._time_provider.time()))
504
+ if not self._advance_boot():
527
505
  return False
528
506
 
529
507
  if not event and not self._emitted_signals:
530
508
  return False
531
509
 
532
- # - if fit was not called - skip on_event call
533
- if not self._context._strategy_state.is_on_fit_called or (
534
- not self._is_simulation and not self._context._strategy_state.is_on_warmup_finished_called
535
- ):
536
- return False
537
-
538
- # - warmup-finished still running (inline): skip on_event call
539
- if self._warmup_finished_is_running:
540
- return False
541
-
542
510
  # - strategy still fitting: inline mode (and simulation) skips the strategy
543
511
  # callbacks — the fit owns the thread, this is a re-entrancy guard. Thread
544
512
  # mode DELIVERS instead: handlers run here on the ProcessorThread concurrently
@@ -730,7 +698,8 @@ class ProcessingManager(IProcessingManager):
730
698
  self._time_provider.time(), signals, self._account_manager, _targets_from_trackers
731
699
  )
732
700
 
733
- def __invoke_on_fit(self) -> None:
701
+ def __invoke_on_fit(self) -> bool:
702
+ """Run on_fit inline. True ⇔ it completed without raising."""
734
703
  with self._health_monitor("ctx.on_fit"):
735
704
  try:
736
705
  # - enforce the blacklist before fitting: refresh the cache AND force-close
@@ -743,15 +712,21 @@ class ProcessingManager(IProcessingManager):
743
712
  self._strategy.on_fit(self._context)
744
713
  self._subscription_manager.commit() # apply pending operations
745
714
  logger.debug(f"[<y>{self.__class__.__name__}</y>] :: <g>{self._strategy_name}</g> is fitted")
715
+ self._context._strategy_state.is_on_fit_called = True
716
+ return True
746
717
  except Exception as strat_error:
747
718
  logger.error(
748
719
  f"[{self.__class__.__name__}] :: Strategy {self._strategy_name} on_fit raised an exception: {strat_error}"
749
720
  )
750
721
  logger.opt(colors=False).error(traceback.format_exc())
751
- finally:
752
- self._context._strategy_state.is_on_fit_called = True
722
+ # - simulation keeps the latch: no retry machinery there, and a raising fit
723
+ # must not re-fire on every tick
724
+ if self._is_simulation:
725
+ self._context._strategy_state.is_on_fit_called = True
726
+ return False
753
727
 
754
- def __invoke_on_warmup_finished(self) -> None:
728
+ def __invoke_on_warmup_finished(self) -> bool:
729
+ """Run on_warmup_finished inline. True ⇔ it completed without raising."""
755
730
  with self._health_monitor("ctx.on_warmup_finished"):
756
731
  try:
757
732
  logger.debug(
@@ -762,12 +737,15 @@ class ProcessingManager(IProcessingManager):
762
737
  logger.debug(
763
738
  f"[<y>{self.__class__.__name__}</y>] :: <g>{self._strategy_name}</g> warmup finished completed"
764
739
  )
740
+ return True
765
741
  except Exception as strat_error:
766
742
  logger.error(
767
743
  f"[{self.__class__.__name__}] :: Strategy {self._strategy_name} on_warmup_finished raised an exception: {strat_error}"
768
744
  )
769
745
  logger.opt(colors=False).error(traceback.format_exc())
746
+ return False
770
747
  finally:
748
+ # - the hook is not guaranteed idempotent: latch it either way, no auto-retry
771
749
  self._context._strategy_state.is_on_warmup_finished_called = True
772
750
 
773
751
  def __preprocess_and_log_target_positions(self, target_positions: list[TargetPosition]) -> list[TargetPosition]:
@@ -921,27 +899,25 @@ class ProcessingManager(IProcessingManager):
921
899
  Gates on_start / on_fit / state-resolution / restore so those callbacks see REAL capital +
922
900
  venue positions instead of the pre-sync zero state (which would produce 0-capital sizing and
923
901
  transiently-flat restored positions). No account requirement in simulation (no venue snapshot).
924
- Falls through after ACCOUNT_SYNC_TIMEOUT so a missing/failed snapshot never hangs the strategy
925
- — mirrors the data-ready two-phase timeout; the reconciler heals state on the next snapshot.
902
+ Never falls through: trading against phantom-zero positions can double-open the book.
903
+ ACCOUNT_SYNC_TIMEOUT only arms an alert (once), the boot keeps holding until synced.
926
904
  """
927
905
  if not self._is_data_ready():
928
906
  return False
929
907
  # - simulation & paper have a locally-seeded account (no venue snapshot to wait for)
930
- if self._context.is_simulation or self._context.is_paper_trading or self._account_manager.is_synced():
908
+ if self._context.is_simulation or self._context.is_paper_trading:
931
909
  return True
932
- # - data ready but the initial venue snapshot hasn't applied yet: wait, bounded.
910
+ if self._account_manager.is_synced():
911
+ self._boot.account_synced()
912
+ return True
913
+ # - data ready but the initial venue snapshot hasn't applied: hold the boot.
914
+ # ACCOUNT_SYNC_TIMEOUT is the alert threshold, not a fall-through — trading
915
+ # against phantom-zero positions can double-open the book.
933
916
  now = self._time_provider.time()
934
917
  if self._account_sync_deadline is None:
935
918
  self._account_sync_deadline = now + self.ACCOUNT_SYNC_TIMEOUT
936
- return False
937
- if now >= self._account_sync_deadline:
938
- if not self._account_sync_timeout_logged:
939
- logger.warning(
940
- "Initial account snapshot not applied within timeout — starting without it; "
941
- "capital/positions may be incomplete until the next reconcile"
942
- )
943
- self._account_sync_timeout_logged = True
944
- return True
919
+ elif now >= self._account_sync_deadline:
920
+ self._boot.account_sync_alert()
945
921
  return False
946
922
 
947
923
  def __update_base_data(
@@ -1087,21 +1063,98 @@ class ProcessingManager(IProcessingManager):
1087
1063
  """It is used by simulation as a dummy to trigger actual time events."""
1088
1064
  pass
1089
1065
 
1090
- def _handle_start(self) -> None:
1091
- if not self._is_ready():
1092
- return
1093
- self._strategy.on_start(self._context)
1094
- self._context._strategy_state.is_on_start_called = True
1066
+ def _advance_boot(self) -> bool:
1067
+ """Drive the boot machine one pipeline pass. True ⇔ the strategy may trade.
1095
1068
 
1096
- def _handle_state_resolution(self) -> None:
1097
- if not self._is_ready():
1069
+ Linearizes the old flag checks: WAIT_READY → ON_START → RESOLVE → RESTORE →
1070
+ WARMUP_FINISHED → BOOT_FIT → TRADING. RESOLVE/RESTORE/WARMUP_FINISHED are
1071
+ live-boot steps: skipped in simulation (RESOLVE) and while the warmup sim is
1072
+ running this pipeline (is_warmup_in_progress). The boot-fit pass returns False
1073
+ so no event is processed on it.
1074
+ """
1075
+ boot = self._boot
1076
+ if boot.is_trading:
1077
+ return True
1078
+ if boot.is_blocked:
1079
+ return False
1080
+ state = self._context._strategy_state
1081
+
1082
+ if boot.phase == BootPhase.WAIT_READY:
1083
+ if not self._is_ready():
1084
+ return False
1085
+ boot.advance(BootPhase.ON_START)
1086
+
1087
+ if boot.phase == BootPhase.ON_START:
1088
+ if not state.is_on_start_called:
1089
+ self._strategy.on_start(self._context)
1090
+ state.is_on_start_called = True
1091
+ boot.advance(BootPhase.RESOLVE)
1092
+
1093
+ if boot.phase == BootPhase.RESOLVE:
1094
+ if not self._is_simulation and not state.is_on_warmup_finished_called:
1095
+ self._handle_state_resolution()
1096
+ boot.advance(BootPhase.RESTORE)
1097
+
1098
+ if boot.phase == BootPhase.RESTORE:
1099
+ if not state.is_warmup_in_progress and not state.is_on_warmup_finished_called:
1100
+ restored_state = self._context.get_restored_state()
1101
+ if restored_state is not None:
1102
+ self._restore_tracker_and_gatherer_state(restored_state)
1103
+ boot.advance(BootPhase.WARMUP_FINISHED)
1104
+
1105
+ if boot.phase == BootPhase.WARMUP_FINISHED:
1106
+ if state.is_warmup_in_progress:
1107
+ boot.advance(BootPhase.BOOT_FIT) # warmup-sim ctx: the hook belongs to the live boot
1108
+ else:
1109
+ if not state.is_on_warmup_finished_called and not self._warmup_finished_is_running:
1110
+ self._handle_warmup_finished()
1111
+ if not state.is_on_warmup_finished_called:
1112
+ return False
1113
+ if not self._is_simulation and state.is_on_fit_called and self._context.initializer.get_fit_on_start():
1114
+ state.is_on_fit_called = False # warmup fit doesn't count: force one live fit
1115
+ boot.advance(BootPhase.BOOT_FIT)
1116
+
1117
+ if boot.phase == BootPhase.BOOT_FIT:
1118
+ if state.is_on_fit_called:
1119
+ boot.record_fit_success()
1120
+ return True
1121
+ if self._fit_is_running or self._warmup_finished_is_running:
1122
+ return False
1123
+ if boot.fit_attempt_allowed(self._time_provider.time()):
1124
+ # - _handle_fit no-ops when readiness lapsed (account desync): such a pass
1125
+ # must not consume an attempt, or three of them would block an unrun fit
1126
+ if not self._is_ready():
1127
+ return False
1128
+ boot.record_fit_attempt()
1129
+ try:
1130
+ self._handle_fit(None, "fit", (None, self._time_provider.time()))
1131
+ except Exception:
1132
+ # - framework raise (cache finalize, health monitor, executor submit):
1133
+ # stays loud, but the attempt must arm the retry deadline like any
1134
+ # other failure instead of hot-looping on an unaccounted budget
1135
+ self._report_boot_fit(False)
1136
+ raise
1137
+ return False
1138
+
1139
+ def _report_boot_fit(self, ok: bool) -> None:
1140
+ """Hand a fit outcome to the boot machine — only while it still owns the fit
1141
+ (BOOT_FIT) or waits for a self-heal (BLOCKED). A fit triggered mid-boot (handlers
1142
+ run ahead of the pipeline) would otherwise jump the machine straight to TRADING.
1143
+ Failures are live-only: simulation latches instead of retrying.
1144
+ """
1145
+ boot = self._boot
1146
+ if boot.phase != BootPhase.BOOT_FIT and not boot.is_blocked:
1098
1147
  return
1148
+ if ok:
1149
+ boot.record_fit_success()
1150
+ elif not self._is_simulation:
1151
+ boot.record_fit_failure(self._time_provider.time())
1099
1152
 
1153
+ def _handle_state_resolution(self) -> None:
1100
1154
  _ctx = self._context
1101
1155
  resolver = _ctx.initializer.get_state_resolver()
1102
1156
  if resolver is None:
1103
- logger.warning("No state resolver found, skipping state resolution")
1104
- return
1157
+ resolver = StateResolver.REDUCE_ONLY
1105
1158
 
1106
1159
  self._log_state_mismatch()
1107
1160
 
@@ -1122,9 +1175,6 @@ class ProcessingManager(IProcessingManager):
1122
1175
  Args:
1123
1176
  restored_state: The restored state containing signals and target positions
1124
1177
  """
1125
- if not self._is_ready():
1126
- return
1127
-
1128
1178
  # Restore tracker state from signals.
1129
1179
  # NB: unlike targets below, signals are NOT re-persisted on restart, so a run that
1130
1180
  # hasn't re-emitted yet restores no signals (state restore is run-scoped). Harmless
@@ -1159,15 +1209,14 @@ class ProcessingManager(IProcessingManager):
1159
1209
  self._logging.save_targets(latest_targets)
1160
1210
 
1161
1211
  def _handle_warmup_finished(self) -> None:
1162
- if not self._is_ready():
1163
- return
1164
1212
  # The flag reset lives in an outer finally so no raise (health monitor included)
1165
1213
  # can strand it True — a stranded flag silently disables the strategy forever.
1166
1214
  self._warmup_finished_is_running = True
1167
1215
  try:
1168
1216
  # Runs inline on the processing thread (single-mutator invariant): account events
1169
1217
  # queue in the channel until the callback returns.
1170
- self.__invoke_on_warmup_finished()
1218
+ if not self.__invoke_on_warmup_finished():
1219
+ self._boot.record_warmup_finished_failure()
1171
1220
  finally:
1172
1221
  self._warmup_finished_is_running = False
1173
1222
 
@@ -1191,9 +1240,10 @@ class ProcessingManager(IProcessingManager):
1191
1240
  self._cache.finalize_ohlc_for_instruments(current_time, self._context.instruments)
1192
1241
  # Runs inline on the processing thread (single-mutator invariant): a long fit
1193
1242
  # blocks event processing until it returns.
1194
- self.__invoke_on_fit()
1243
+ ok = self.__invoke_on_fit()
1195
1244
  finally:
1196
1245
  self._fit_is_running = False
1246
+ self._report_boot_fit(ok)
1197
1247
 
1198
1248
  def _submit_threaded_fit(self, data: tuple[dt_64 | None, dt_64]) -> None:
1199
1249
  """Thread-mode fit (live only): submit the fit body to the single-thread
@@ -1236,6 +1286,7 @@ class ProcessingManager(IProcessingManager):
1236
1286
  _t_start = time.monotonic()
1237
1287
  _deadline_timer: threading.Timer | None = None
1238
1288
  fit_ctx: FitContext | None = None
1289
+ _fit_error: BaseException | None = None
1239
1290
  try:
1240
1291
  # Everything from here — including proxy construction and the timer start
1241
1292
  # (Timer.start can raise "can't start new thread" under resource pressure) —
@@ -1257,10 +1308,16 @@ class ProcessingManager(IProcessingManager):
1257
1308
  self._strategy.on_fit(fit_ctx) # type: ignore[arg-type]
1258
1309
  logger.debug(f"[<y>{self.__class__.__name__}</y>] :: <g>{self._strategy_name}</g> is fitted")
1259
1310
  except Exception as strat_error:
1311
+ _fit_error = strat_error
1260
1312
  logger.error(
1261
1313
  f"[{self.__class__.__name__}] :: Strategy {self._strategy_name} on_fit raised an exception: {strat_error}"
1262
1314
  )
1263
1315
  logger.opt(colors=False).error(traceback.format_exc())
1316
+ except BaseException as framework_error:
1317
+ # - the body never ran (proxy/timer/health-monitor raise): report it as a
1318
+ # failed fit too, so nothing latches as fitted
1319
+ _fit_error = framework_error
1320
+ raise
1264
1321
  finally:
1265
1322
  if _deadline_timer is not None:
1266
1323
  _deadline_timer.cancel()
@@ -1277,7 +1334,14 @@ class ProcessingManager(IProcessingManager):
1277
1334
  except Exception:
1278
1335
  logger.exception("failed to record fit_duration_s gauge")
1279
1336
  _ops, _signals = self._fit_state.end()
1280
- channel.send((None, FIT_COMMIT_EVENT, FitCommitData(ops=_ops, signals=_signals, duration_s=_duration_s), False))
1337
+ channel.send(
1338
+ (
1339
+ None,
1340
+ FIT_COMMIT_EVENT,
1341
+ FitCommitData(ops=_ops, signals=_signals, duration_s=_duration_s, error=_fit_error),
1342
+ False,
1343
+ )
1344
+ )
1281
1345
 
1282
1346
  def _warn_fit_soft_deadline(self) -> None:
1283
1347
  logger.warning(
@@ -1295,8 +1359,9 @@ class ProcessingManager(IProcessingManager):
1295
1359
  by the subscription commit to its background worker, so the replay stays fast and
1296
1360
  the added instruments go live only after their history landed), commit any
1297
1361
  remaining deferred subscriptions, then — in a finally so no replay failure can
1298
- strand the flag — drain fit-emitted signals into the normal pipeline, set
1299
- is_on_fit_called and clear _fit_is_running. Returning None hands control to
1362
+ strand the flag — drain fit-emitted signals into the normal pipeline, latch
1363
+ is_on_fit_called (only when the fit itself did not fail) and clear
1364
+ _fit_is_running. Returning None hands control to
1300
1365
  _run_strategy_pipeline, which processes the drained signals immediately.
1301
1366
  """
1302
1367
  try:
@@ -1314,8 +1379,10 @@ class ProcessingManager(IProcessingManager):
1314
1379
  finally:
1315
1380
  if commit.signals:
1316
1381
  self._emitted_signals.extend(commit.signals)
1317
- self._context._strategy_state.is_on_fit_called = True
1382
+ if commit.error is None:
1383
+ self._context._strategy_state.is_on_fit_called = True
1318
1384
  self._fit_is_running = False
1385
+ self._report_boot_fit(commit.error is None)
1319
1386
 
1320
1387
  def _handle_subscription_swap(self, instrument: Instrument | None, event_type: str, apply_swap: Callable) -> None:
1321
1388
  """Apply a deferred subscription commit's swap on the ProcessorThread. Posted by