Qubx 3.6.2.dev1__tar.gz → 3.8.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 (328) hide show
  1. {qubx-3.6.2.dev1 → qubx-3.8.0}/PKG-INFO +1 -1
  2. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/_version.py +2 -2
  3. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/connector.py +6 -0
  4. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +14 -1
  5. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/utils.py +21 -0
  6. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/diffs.py +2 -1
  7. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/reconciler.py +60 -7
  8. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/reducer.py +12 -5
  9. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/state.py +13 -3
  10. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/basics.py +36 -2
  11. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/events.py +4 -0
  12. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/utils.pyx +41 -2
  13. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/app.py +2 -0
  14. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/orders_table.py +8 -0
  15. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/positions_table.py +7 -0
  16. {qubx-3.6.2.dev1 → qubx-3.8.0}/.gitignore +0 -0
  17. {qubx-3.6.2.dev1 → qubx-3.8.0}/LICENSE +0 -0
  18. {qubx-3.6.2.dev1 → qubx-3.8.0}/README.md +0 -0
  19. {qubx-3.6.2.dev1 → qubx-3.8.0}/pyproject.toml +0 -0
  20. {qubx-3.6.2.dev1 → qubx-3.8.0}/scripts/build.py +0 -0
  21. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/__init__.py +0 -0
  22. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/_nb_magic.py +0 -0
  23. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/__init__.py +0 -0
  24. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/connector.py +0 -0
  25. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/data.py +0 -0
  26. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/iteratedstream.py +0 -0
  27. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/management.py +0 -0
  28. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/ome.py +0 -0
  29. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/optimization.py +0 -0
  30. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/runner.py +0 -0
  31. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/sentinels.py +0 -0
  32. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/simulated_data.py +0 -0
  33. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/simulated_exchange.py +0 -0
  34. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/simulator.py +0 -0
  35. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/transfers.py +0 -0
  36. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/utils.py +0 -0
  37. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/__init__.py +0 -0
  38. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/commands.py +0 -0
  39. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/deploy.py +0 -0
  40. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/misc.py +0 -0
  41. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/release.py +0 -0
  42. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/resolver.py +0 -0
  43. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/s3.py +0 -0
  44. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/theme.py +0 -0
  45. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/tui.py +0 -0
  46. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/user_config.py +0 -0
  47. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/config.py +0 -0
  48. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/__init__.py +0 -0
  49. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/__init__.py +0 -0
  50. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  51. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  52. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  53. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/data.py +0 -0
  54. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  55. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  56. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  57. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/_two_stream.py +0 -0
  58. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  59. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/binance/connector.py +0 -0
  60. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  61. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/bitfinex/connector.py +0 -0
  62. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  63. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  64. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  65. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  66. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  67. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  68. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/okx/connector.py +0 -0
  69. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  70. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/factory.py +0 -0
  71. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  72. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  73. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  74. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  75. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  76. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  77. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  78. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  79. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  80. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  81. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/plugin.py +0 -0
  82. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
  83. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  84. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  85. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  86. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  87. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/plugin.py +0 -0
  88. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/registry.py +0 -0
  89. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/tardis/data.py +0 -0
  90. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/tardis/plugin.py +0 -0
  91. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/tardis/utils.py +0 -0
  92. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/__init__.py +0 -0
  93. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/builtin.py +0 -0
  94. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/decorator.py +0 -0
  95. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/executor.py +0 -0
  96. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/interfaces.py +0 -0
  97. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/server.py +0 -0
  98. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/types.py +0 -0
  99. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/__init__.py +0 -0
  100. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/__init__.py +0 -0
  101. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/config.py +0 -0
  102. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/manager.py +0 -0
  103. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/state_machine.py +0 -0
  104. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/boot.py +0 -0
  105. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/connector.py +0 -0
  106. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/context.py +0 -0
  107. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/detectors/__init__.py +0 -0
  108. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/detectors/delisting.py +0 -0
  109. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/detectors/stale.py +0 -0
  110. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/errors.py +0 -0
  111. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/exceptions.py +0 -0
  112. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/fit_context.py +0 -0
  113. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/fit_executor.py +0 -0
  114. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/helpers.py +0 -0
  115. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/initializer.py +0 -0
  116. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/instrument_service.py +0 -0
  117. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/interfaces.py +0 -0
  118. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/loggers.py +0 -0
  119. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/lookups.py +0 -0
  120. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/metrics.py +0 -0
  121. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/__init__.py +0 -0
  122. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/instrument_service.py +0 -0
  123. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/market.py +0 -0
  124. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/processing.py +0 -0
  125. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/subscription.py +0 -0
  126. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/trading.py +0 -0
  127. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/universe.py +0 -0
  128. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/utils.py +0 -0
  129. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/series.pxd +0 -0
  130. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/series.pyi +0 -0
  131. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/series.pyx +0 -0
  132. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/status.py +0 -0
  133. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/utils.pyi +0 -0
  134. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/__init__.py +0 -0
  135. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/cache.py +0 -0
  136. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/containers.py +0 -0
  137. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/guards.py +0 -0
  138. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/registry.py +0 -0
  139. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storage.py +0 -0
  140. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/ccxt.py +0 -0
  141. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/csv.py +0 -0
  142. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/handy.py +0 -0
  143. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/multi.py +0 -0
  144. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/questdb.py +0 -0
  145. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/stub.py +0 -0
  146. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/utils.py +0 -0
  147. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/transformers.py +0 -0
  148. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/__init__.py +0 -0
  149. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/base.py +0 -0
  150. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/composite.py +0 -0
  151. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/csv.py +0 -0
  152. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/indicator.py +0 -0
  153. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/inmemory.py +0 -0
  154. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/prometheus.py +0 -0
  155. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/questdb.py +0 -0
  156. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/__init__.py +0 -0
  157. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/composite.py +0 -0
  158. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/__init__.py +0 -0
  159. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/base.py +0 -0
  160. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/incremental.py +0 -0
  161. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/slack.py +0 -0
  162. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/target_position.py +0 -0
  163. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/redis_streams.py +0 -0
  164. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/slack.py +0 -0
  165. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/gathering/simplest.py +0 -0
  166. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/health/__init__.py +0 -0
  167. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/health/base.py +0 -0
  168. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/health/dummy.py +0 -0
  169. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/__init__.py +0 -0
  170. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/csv.py +0 -0
  171. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/factory.py +0 -0
  172. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/inmemory.py +0 -0
  173. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/mongo.py +0 -0
  174. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/postgres.py +0 -0
  175. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/notifications/__init__.py +0 -0
  176. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/notifications/composite.py +0 -0
  177. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/notifications/slack.py +0 -0
  178. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/notifications/throttler.py +0 -0
  179. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/pandaz/__init__.py +0 -0
  180. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/pandaz/stats.py +0 -0
  181. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/pandaz/ta.py +0 -0
  182. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/pandaz/utils.py +0 -0
  183. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/plugins/__init__.py +0 -0
  184. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/plugins/loader.py +0 -0
  185. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/__init__.py +0 -0
  186. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/backend.py +0 -0
  187. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/config.py +0 -0
  188. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/engine.py +0 -0
  189. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/ip_resolver.py +0 -0
  190. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/manager.py +0 -0
  191. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/pools.py +0 -0
  192. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/redis_backend.py +0 -0
  193. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/resilient.py +0 -0
  194. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/_build.py +0 -0
  195. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/crypto-fees.ini +0 -0
  196. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  197. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  198. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  199. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  200. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  201. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  202. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  203. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  204. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  205. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  206. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  207. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restarts/__init__.py +0 -0
  208. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restarts/state_resolvers.py +0 -0
  209. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restarts/time_finders.py +0 -0
  210. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/__init__.py +0 -0
  211. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/balance.py +0 -0
  212. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/factory.py +0 -0
  213. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/interfaces.py +0 -0
  214. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/position.py +0 -0
  215. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/signal.py +0 -0
  216. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/state.py +0 -0
  217. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/utils.py +0 -0
  218. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/state/__init__.py +0 -0
  219. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/state/dummy.py +0 -0
  220. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/state/redis.py +0 -0
  221. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/state/safe.py +0 -0
  222. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/ta/__init__.py +0 -0
  223. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/ta/indicators.pxd +0 -0
  224. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/ta/indicators.pyi +0 -0
  225. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/ta/indicators.pyx +0 -0
  226. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/__init__.py +0 -0
  227. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/base.py +0 -0
  228. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  229. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/config.yml.j2 +0 -0
  230. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  231. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  232. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  233. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  234. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  235. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/template.yml +0 -0
  236. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  237. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  238. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  239. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  240. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  241. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  242. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.python-version +0 -0
  243. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  244. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  245. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  246. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  247. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/README.md.j2 +0 -0
  248. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  249. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/justfile.j2 +0 -0
  250. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  251. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/research/.gitkeep +0 -0
  252. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  253. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  254. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  255. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/template.yml +0 -0
  256. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  257. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  258. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  259. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/config.yml.j2 +0 -0
  260. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  261. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  262. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  263. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/template.yml +0 -0
  264. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/__init__.py +0 -0
  265. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/advanced.py +0 -0
  266. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/composite.py +0 -0
  267. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/rebalancers.py +0 -0
  268. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/riskctrl.py +0 -0
  269. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/sizers.py +0 -0
  270. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/__init__.py +0 -0
  271. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/_pyxreloader.py +0 -0
  272. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/charting/lookinglass.py +0 -0
  273. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  274. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/charting/orderbook.py +0 -0
  275. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/clock.py +0 -0
  276. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/collections.py +0 -0
  277. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/hft/__init__.py +0 -0
  278. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/hft/numba_utils.py +0 -0
  279. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/hft/orderbook.pyi +0 -0
  280. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/hft/orderbook.pyx +0 -0
  281. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/marketdata/binance.py +0 -0
  282. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/marketdata/ccxt.py +0 -0
  283. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/marketdata/dukas.py +0 -0
  284. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/misc.py +0 -0
  285. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/nonce.py +0 -0
  286. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/numbers_utils.py +0 -0
  287. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/orderbook.py +0 -0
  288. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/__init__.py +0 -0
  289. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/dashboard.py +0 -0
  290. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/data.py +0 -0
  291. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/interfaces.py +0 -0
  292. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  293. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  294. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/questdb.py +0 -0
  295. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/rate_limiter.py +0 -0
  296. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/results.py +0 -0
  297. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/ringbuffer.pxd +0 -0
  298. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/ringbuffer.pyi +0 -0
  299. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/ringbuffer.pyx +0 -0
  300. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/__init__.py +0 -0
  301. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  302. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/accounts.py +0 -0
  303. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/configs.py +0 -0
  304. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/factory.py +0 -0
  305. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/kernel_service.py +0 -0
  306. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/runner.py +0 -0
  307. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/__init__.py +0 -0
  308. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/handlers.py +0 -0
  309. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/init_code.py +0 -0
  310. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/kernel.py +0 -0
  311. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  312. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  313. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
  314. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  315. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  316. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/log_filter.py +0 -0
  317. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  318. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  319. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/s3.py +0 -0
  320. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/slack.py +0 -0
  321. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/threading.py +0 -0
  322. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/throttler.py +0 -0
  323. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/time.py +0 -0
  324. {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/websocket_manager.py +0 -0
  325. {qubx-3.6.2.dev1 → qubx-3.8.0}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  326. {qubx-3.6.2.dev1 → qubx-3.8.0}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  327. {qubx-3.6.2.dev1 → qubx-3.8.0}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  328. {qubx-3.6.2.dev1 → qubx-3.8.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: 3.6.2.dev1
3
+ Version: 3.8.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 = '3.6.2.dev1'
22
- __version_tuple__ = version_tuple = (3, 6, 2, 'dev1')
21
+ __version__ = version = '3.8.0'
22
+ __version_tuple__ = version_tuple = (3, 8, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -52,6 +52,7 @@ from qubx.core.basics import (
52
52
  OrderStatus,
53
53
  OrderType,
54
54
  Position,
55
+ RejectCause,
55
56
  dt_64,
56
57
  resolve_reduce_only,
57
58
  )
@@ -94,6 +95,7 @@ from .utils import (
94
95
  instrument_to_ccxt_symbol,
95
96
  normalize_margin_mode,
96
97
  prepare_ccxt_order_payload,
98
+ reject_cause_of,
97
99
  )
98
100
 
99
101
  # Venue-verdict exceptions: every one of these is the venue refusing the order,
@@ -380,6 +382,7 @@ class CcxtConnector(ChannelEmitter):
380
382
  client_order_id=client_id,
381
383
  reason=str(error),
382
384
  code=type(error).__name__,
385
+ cause=reject_cause_of(error),
383
386
  )
384
387
  )
385
388
 
@@ -441,6 +444,7 @@ class CcxtConnector(ChannelEmitter):
441
444
  venue_order_id: str | None,
442
445
  reason: str | None = None,
443
446
  code: str | None = None,
447
+ cause: RejectCause = RejectCause.UNKNOWN,
444
448
  ) -> None:
445
449
  # Carry both ids: AM's reject handler resolves the order by cid first, then venue id,
446
450
  # so the order can revert out of PENDING_CANCEL regardless of which id the caller had.
@@ -451,6 +455,7 @@ class CcxtConnector(ChannelEmitter):
451
455
  venue_order_id=venue_order_id,
452
456
  reason=reason or f"venue rejected cancel for {venue_order_id or client_order_id}",
453
457
  code=code,
458
+ cause=cause,
454
459
  )
455
460
  )
456
461
 
@@ -772,6 +777,7 @@ class CcxtConnector(ChannelEmitter):
772
777
  venue_order_id=venue_order_id,
773
778
  reason=str(error),
774
779
  code=type(error).__name__,
780
+ cause=reject_cause_of(error),
775
781
  )
776
782
  )
777
783
 
@@ -731,7 +731,20 @@ class BinancePortfolioMargin(BinanceQVUSDM):
731
731
  "defaultType": "swap",
732
732
  "portfolioMargin": True,
733
733
  "fetchMarkets": ["spot", "linear", "inverse"],
734
- }
734
+ },
735
+ # ccxt's handle_errors reads the URL-scoped map BEFORE the global one, and its
736
+ # portfolioMargin group maps -2019 to OperationFailed — so a papi order refused
737
+ # for margin never reached BinanceQV's global "-2019": InsufficientFunds. It has
738
+ # to sit here, in the LAST describe of the chain: binanceusdm's own describe runs
739
+ # after BinanceQV's and would overwrite the group. Measured on BINANCE.PM: 105
740
+ # margin refusals arrived as OperationFailed and were logged as unexpected errors.
741
+ "exceptions": {
742
+ "portfolioMargin": {
743
+ "exact": {
744
+ "-2019": InsufficientFunds,
745
+ },
746
+ },
747
+ },
735
748
  },
736
749
  )
737
750
 
@@ -22,6 +22,7 @@ from qubx.core.basics import (
22
22
  OrderSide,
23
23
  OrderStatus,
24
24
  Position,
25
+ RejectCause,
25
26
  classify_origin,
26
27
  dt_64,
27
28
  )
@@ -58,6 +59,19 @@ _CCXT_STATUS_MAP: dict[str, OrderStatus] = {
58
59
  "rejected": OrderStatus.REJECTED,
59
60
  }
60
61
 
62
+ # - ccxt normalises every venue's error into its own exception classes, so the class name is
63
+ # already a portable vocabulary; this is the reading of it the framework acts on. Names, not
64
+ # classes, so a ccxt version that drops one does not break the import.
65
+ _REJECT_CAUSE_BY_CCXT_ERROR: dict[str, RejectCause] = {
66
+ "InsufficientFunds": RejectCause.INSUFFICIENT_MARGIN,
67
+ "OrderNotFillable": RejectCause.NOT_FILLABLE,
68
+ "OrderImmediatelyFillable": RejectCause.NOT_FILLABLE,
69
+ "RateLimitExceeded": RejectCause.RATE_LIMITED,
70
+ "DDoSProtection": RejectCause.RATE_LIMITED,
71
+ "OrderNotFound": RejectCause.NOT_FOUND,
72
+ "InvalidOrder": RejectCause.TOO_SMALL,
73
+ }
74
+
61
75
 
62
76
  def info_float(info: dict[str, Any], key: str) -> float | None:
63
77
  """Parse an optional numeric field from a raw venue payload; None when absent/malformed.
@@ -693,3 +707,10 @@ def ccxt_convert_timeframe_to_exchange_format(timeframe: str) -> str | None:
693
707
  _t = re.match(r"(\d+)(\w+)", timeframe)
694
708
  timeframe = f"{_t[1]}{_t[2][0].lower()}" if _t and len(_t.groups()) > 1 else timeframe
695
709
  return timeframe
710
+
711
+
712
+ def reject_cause_of(error: Exception) -> RejectCause:
713
+ """
714
+ Classify a ccxt error for callers that cannot read venue codes. UNKNOWN when unmapped.
715
+ """
716
+ return _REJECT_CAUSE_BY_CCXT_ERROR.get(type(error).__name__, RejectCause.UNKNOWN)
@@ -30,7 +30,8 @@ BALANCE_ABS_TOL = 0.01
30
30
 
31
31
  # Maintenance margin re-marks with every snapshot's mark price, so RTOL-tight comparison
32
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.
33
+ # re-syncing; sub-0.5% (and sub-cent) drift is mark noise. Measured over one live run: 175
34
+ # diffs, median $0.000052, only one above a cent — and that one was a real position change.
34
35
  MARGIN_RTOL = 5e-3
35
36
 
36
37
 
@@ -19,6 +19,7 @@ import numpy as np
19
19
  from qubx import area_logger
20
20
  from qubx.core.account_manager.diffs import (
21
21
  BalanceMismatch,
22
+ Diff,
22
23
  Differ,
23
24
  LocalOrderMissing,
24
25
  LocalPositionMissing,
@@ -433,8 +434,30 @@ class Reconciler:
433
434
  state.mark_snapshot_applied(snap.as_of)
434
435
  changed = changed_positions if changed_positions is not None else []
435
436
  actions: list[Action] = []
436
- for difference in self._differ.diff(state, snap):
437
+ differences = self._differ.diff(state, snap)
438
+ # - a snapshot requested before a deal was booked cannot contain that deal, so letting it
439
+ # write the position rewinds a real fill. The decision is per instrument, not per diff:
440
+ # a size change always brings a margin change, and PositionFieldMismatch routes through
441
+ # reconcile_position_from_snapshot, which is authoritative for size and avg-price too.
442
+ # Measured LIGHTER 2026-08-28 (ENAUSDC 272.6 -> 181.8, 84ms after the deal booked) and
443
+ # BINANCE.UM (TRXUSDT 146 -> 127 against a snapshot 1.58s stale); on ALGOUSDT the
444
+ # executor sized its next clip off the rewound number and overshot by 16.9%.
445
+ stale_for = {
446
+ instr
447
+ for instr in {self._position_instrument(d) for d in differences}
448
+ if instr is not None
449
+ and (booked := state.get_position_deal_booked_at(instr)) is not None
450
+ and booked > snap.as_of
451
+ }
452
+ for instr in stale_for:
453
+ _log.info(
454
+ f"[{state.exchange}] reconcile: <y>{instr}</y> snapshot as_of={snap.as_of} predates a deal "
455
+ f"booked at {state.get_position_deal_booked_at(instr)} — keeping the local position"
456
+ )
457
+ for difference in differences:
437
458
  _log.debug(difference.describe())
459
+ if self._position_instrument(difference) in stale_for:
460
+ continue
438
461
  # - each atom is applied in isolation: a handler that raises (e.g. a venue/state
439
462
  # surprise) must not sink the rest of the snapshot reconcile (balances/figures/
440
463
  # other positions). Log and move on; the next snapshot re-derives the diff.
@@ -452,7 +475,7 @@ class Reconciler:
452
475
  self._recover_order(state, snap_order, snap.as_of)
453
476
 
454
477
  case OrderFieldMismatch(origin=snap_order):
455
- if (ev := self._reconcile_order(state, snap_order)) is not None:
478
+ if (ev := self._reconcile_order(state, snap_order, now)) is not None:
456
479
  actions.append(RouteEvent(ev))
457
480
 
458
481
  # - size diff = missed deals
@@ -494,6 +517,18 @@ class Reconciler:
494
517
 
495
518
  return actions + self._dispatch(SnapshotIn(snap), state, now)
496
519
 
520
+ @staticmethod
521
+ def _position_instrument(difference: Diff) -> Instrument | None:
522
+ """
523
+ The instrument a position diff is about, or None for the order and balance diffs.
524
+ """
525
+ match difference:
526
+ case PositionFieldMismatch(origin=pos):
527
+ return pos.instrument
528
+ case OriginalPositionMissing(position=pos) | LocalPositionMissing(position=pos):
529
+ return pos.instrument
530
+ return None
531
+
497
532
  @staticmethod
498
533
  def _recover_order(state: AccountState, snap_order: Order, as_of: np.datetime64) -> Order:
499
534
  # - venue order absent locally: trust the producer-assigned origin (only the connector
@@ -592,20 +627,38 @@ class Reconciler:
592
627
  )
593
628
  return state.get_position(instrument)
594
629
 
595
- def _reconcile_order(self, state: AccountState, snap_order: Order) -> Action | None:
630
+ def _reconcile_order(self, state: AccountState, snap_order: Order, now: np.datetime64) -> Action | None:
596
631
  """Apply a snapshot order's status/filled to local state; return the fill event to route.
597
632
 
598
- Snapshot is authoritative for the order's own state. Guards: skip if locally terminal or
599
- not venue-newer; never wipe an in-flight pending marker (the venue resolves that race).
633
+ Snapshot is authoritative for the order's own state. Skipped when the order is locally
634
+ terminal, and while a pending marker is still inside the confirm window — see
635
+ _pending_expired. For anything else the snapshot has to be venue-newer.
600
636
  """
601
637
  local = state.get_active_order(snap_order.client_order_id)
602
- if local is None or local.status.is_terminal or not self._venue_newer(snap_order, local):
638
+ if local is None or local.status.is_terminal:
603
639
  return None
604
- if local.status.is_pending and not snap_order.status.is_terminal:
640
+ if local.status.is_pending:
641
+ # - a pending marker carries the local clock while the snapshot carries the venue's, so
642
+ # _venue_newer cannot judge it: the confirm window decides instead
643
+ if not snap_order.status.is_terminal and not self._pending_expired(local, now):
644
+ return None
645
+ elif not self._venue_newer(snap_order, local):
605
646
  return None
606
647
  self._apply_order_snapshot(state, local, snap_order)
607
648
  return self._fill_event(local)
608
649
 
650
+ def _pending_expired(self, local: Order, now: np.datetime64) -> bool:
651
+ """
652
+ True once a pending marker has outlived the confirm window with the venue still holding the
653
+ order live — the cancel or update never reached it, so the snapshot has to win.
654
+
655
+ Inside the window the marker is defended: a snapshot fetched before the cancel landed shows
656
+ the order alive, and reverting then would undo a correct PENDING_CANCEL. Measured cancel
657
+ confirm on LIGHTER is 568 ms median, so the window is ~9x headroom. Both stamps are the
658
+ local clock — transition_order stamps a locally-driven transition with `now`.
659
+ """
660
+ return local.last_update_time is not None and (now - local.last_update_time) >= self._order_confirm_wait # type: ignore
661
+
609
662
  @staticmethod
610
663
  def _venue_newer(snap: Order, local: Order) -> bool:
611
664
  # - venue-vs-venue only (the connector stamps last_update_time on snapshot orders); no
@@ -280,6 +280,7 @@ def _handle_rejected(state: AccountState, event: OrderRejectedEvent, now: np.dat
280
280
  return ApplyResult()
281
281
  order.rejected_reason = event.reason
282
282
  order.error_code = event.code
283
+ order.reject_cause = event.cause
283
284
  order = transition(state, order.client_order_id, OrderStatus.REJECTED, now, update_time=event.last_update_time)
284
285
  return ApplyResult(order=order, order_change=OrderChange.REJECTED)
285
286
 
@@ -300,7 +301,7 @@ def _reconciled_past(state: AccountState, instrument: Instrument, deal: Deal) ->
300
301
  return watermark is not None and deal.time <= watermark
301
302
 
302
303
 
303
- def _book_deal(state: AccountState, instrument: Instrument, deal: Deal) -> Position:
304
+ def _book_deal(state: AccountState, instrument: Instrument, deal: Deal, now: np.datetime64) -> Position:
304
305
  """Apply a deal's effect to the position and balances. Caller dedups first.
305
306
 
306
307
  Futures/swap: credits realized PnL and debits the fee to the settle-currency
@@ -314,6 +315,8 @@ def _book_deal(state: AccountState, instrument: Instrument, deal: Deal) -> Posit
314
315
  _before = pos.quantity
315
316
  realized_pnl, fee = pos.update_position_by_deal(deal, state.conversion_rate(instrument))
316
317
  logger.debug("deal {} amt={} {}->{} tid={}", instrument.symbol, deal.amount, _before, pos.quantity, deal.trade_id)
318
+ # - a position snapshot requested before this moment cannot contain this deal
319
+ state.mark_position_deal_booked(instrument, now)
317
320
  if instrument.is_futures():
318
321
  state.mark_cash_currency(instrument.settle)
319
322
  # TODO(account-mgmt): fee is folded into settle here (correct when
@@ -387,11 +390,13 @@ def _handle_fill(state: AccountState, event: OrderFilledEvent, now: np.datetime6
387
390
  if event.fill is not None
388
391
  else None
389
392
  )
390
- position = _book_deal(state, order.instrument, deal) if deal is not None and order.instrument is not None else None
393
+ position = (
394
+ _book_deal(state, order.instrument, deal, now) if deal is not None and order.instrument is not None else None
395
+ )
391
396
  # Terminal fill-gap: book executions the venue counted but never delivered as deals so
392
397
  # position AND realized PnL converge now, not size-only at the next snapshot.
393
398
  if (gap := _reconcile_fill_gap(state, order, event, now)) is not None and order.instrument is not None:
394
- position = _book_deal(state, order.instrument, gap)
399
+ position = _book_deal(state, order.instrument, gap, now)
395
400
  deal = deal or gap
396
401
  order = transition(state, order.client_order_id, OrderStatus.FILLED, now, update_time=event.last_update_time)
397
402
  return ApplyResult(order=order, order_change=OrderChange.FILLED, deal=deal, position=position)
@@ -410,7 +415,9 @@ def _handle_partial_fill(state: AccountState, event: OrderPartiallyFilledEvent,
410
415
  if event.fill is not None
411
416
  else None
412
417
  )
413
- position = _book_deal(state, order.instrument, deal) if deal is not None and order.instrument is not None else None
418
+ position = (
419
+ _book_deal(state, order.instrument, deal, now) if deal is not None and order.instrument is not None else None
420
+ )
414
421
  # a pending cancel/update is resolved by the venue separately — don't disturb its status
415
422
  pending = order.status.is_pending
416
423
  if pending:
@@ -460,7 +467,7 @@ def _handle_deal(state: AccountState, event: DealEvent, now: np.datetime64) -> A
460
467
  pos = state.ensure_position(order.instrument)
461
468
  pos.update_position_by_deal(deal, state.conversion_rate(order.instrument), realize_only=True)
462
469
  return ApplyResult(deal=deal, position=pos)
463
- return ApplyResult(deal=deal, position=_book_deal(state, order.instrument, deal))
470
+ return ApplyResult(deal=deal, position=_book_deal(state, order.instrument, deal, now))
464
471
 
465
472
 
466
473
  def _handle_updated(state: AccountState, event: OrderUpdatedEvent, now: np.datetime64) -> ApplyResult:
@@ -69,6 +69,7 @@ class AccountState:
69
69
  "_applied_funding_buckets",
70
70
  "_balance_push_as_of",
71
71
  "_position_reconcile_as_of",
72
+ "_position_deal_booked_at",
72
73
  )
73
74
 
74
75
  def __init__(self, exchange: str, base_currency: str, *, terminal_history_size: int = 10_000):
@@ -117,6 +118,11 @@ class AccountState:
117
118
  # - watermark: snapshot reconciled the size up to this venue time; a deal at/under it is
118
119
  # already in that size → reducer records but doesn't re-book it
119
120
  self._position_reconcile_as_of: dict[Instrument, np.datetime64] = {}
121
+ # - local clock of the most recent deal booked into the size, per instrument. A position
122
+ # snapshot requested before it cannot contain that deal, so it must not overwrite the
123
+ # size. Same domain as AccountSnapshot.as_of, which both connectors stamp before the
124
+ # fetch (ccxt connector.py:1708, lighter connector.py:487).
125
+ self._position_deal_booked_at: dict[Instrument, np.datetime64] = {}
120
126
 
121
127
  def __repr__(self) -> str:
122
128
  return (
@@ -179,6 +185,12 @@ class AccountState:
179
185
  def get_balance_push_as_of(self, currency: str) -> np.datetime64 | None:
180
186
  return self._balance_push_as_of.get(currency)
181
187
 
188
+ def get_position_deal_booked_at(self, instrument: Instrument) -> np.datetime64 | None:
189
+ return self._position_deal_booked_at.get(instrument)
190
+
191
+ def mark_position_deal_booked(self, instrument: Instrument, at: np.datetime64) -> None:
192
+ self._position_deal_booked_at[instrument] = at
193
+
182
194
  def get_transition_counts(self) -> dict[str, int]:
183
195
  """Audit counter snapshot: status.value -> number of transitions into it."""
184
196
  return dict(self._transition_counts)
@@ -457,9 +469,7 @@ class AccountState:
457
469
  f"[{self.exchange}] reconcile: position <y>{snapshot.instrument}</y> from snapshot -> "
458
470
  f"size {existing.quantity}->{snapshot.quantity} avg {existing.position_avg_price}->{snapshot.position_avg_price}"
459
471
  )
460
- existing.reconcile_size(
461
- snapshot.quantity, snapshot.position_avg_price, timestamp=snapshot.last_update_time
462
- )
472
+ existing.reconcile_size(snapshot.quantity, snapshot.position_avg_price, timestamp=snapshot.last_update_time)
463
473
 
464
474
  # external margins first, so the mark refresh below skips recalculating them
465
475
  if snapshot._maint_margin_external:
@@ -10,9 +10,10 @@ from typing import Any, Literal, TypeAlias
10
10
  import numpy as np
11
11
  import pandas as pd
12
12
 
13
+ from qubx import logger
13
14
  from qubx.core.exceptions import QueueTimeout
14
15
  from qubx.core.series import Bar, OrderBook, Quote, Trade, time_as_nsec
15
- from qubx.core.utils import prec_ceil, prec_floor, time_delta_to_str, time_to_str
16
+ from qubx.core.utils import add_in_lots, is_lot_multiple, prec_ceil, prec_floor, time_delta_to_str, time_to_str
16
17
  from qubx.utils.clock import start_clock_discipline, time_now
17
18
  from qubx.utils.misc import Stopwatch
18
19
  from qubx.utils.time import to_timedelta
@@ -789,6 +790,25 @@ def resolve_reduce_only(options: dict[str, Any]) -> bool | None:
789
790
  return None
790
791
 
791
792
 
793
+ class RejectCause(StrEnum):
794
+ """
795
+ Why a venue refused an order, cancel or update, in terms a caller can act on.
796
+
797
+ ``OrderRejectedEvent.code`` keeps the venue's own value — a ccxt exception class, a Lighter
798
+ numeric code — which is what a log reader wants. This is the portable reading of it, so a
799
+ strategy can say "give up on repeated INSUFFICIENT_MARGIN, keep retrying NOT_FILLABLE"
800
+ without a per-venue lookup table. Anything a connector does not map stays UNKNOWN.
801
+ """
802
+
803
+ INSUFFICIENT_MARGIN = "INSUFFICIENT_MARGIN"
804
+ NOT_FILLABLE = "NOT_FILLABLE" # - post-only that would have crossed
805
+ RATE_LIMITED = "RATE_LIMITED"
806
+ TOO_SMALL = "TOO_SMALL" # - below min size / min notional
807
+ NOT_FOUND = "NOT_FOUND" # - cancel/update of an order the venue does not have
808
+ SEQUENCE = "SEQUENCE" # - venue-ordering refusal; the connector rewrites it
809
+ UNKNOWN = "UNKNOWN"
810
+
811
+
792
812
  class OrderChange(StrEnum):
793
813
  """What happened to an order, paired with it on ApplyResult. Covers the cases
794
814
  order.status can't express on its own: UPDATED (status unchanged), CANCEL_REJECTED/
@@ -870,6 +890,8 @@ class Order:
870
890
  # venue/connector error code accompanying a reject (e.g. the ccxt error class name);
871
891
  # None when the reject path carries no code (synthetic reconcile rejects).
872
892
  error_code: str | None = None
893
+ # the portable reading of that code; UNKNOWN when the connector does not map it
894
+ reject_cause: RejectCause = RejectCause.UNKNOWN
873
895
  reduce_only: bool = False
874
896
  post_only: bool = False
875
897
  # Defaults to FRAMEWORK (the common case); the snapshot/external materialization
@@ -1212,9 +1234,21 @@ class Position:
1212
1234
  *,
1213
1235
  realize_only: bool = False,
1214
1236
  ) -> tuple[float, float]:
1237
+ # - both operands are lot multiples, so add in whole lots: 28.2 + (-28.1) is 282 + (-281)
1238
+ # ticks, exactly one tick. Flooring the float sum loses a lot whenever subtraction leaves
1239
+ # the result a few ulp short of the grid, which is how a position reached 0.0 locally
1240
+ # while the venue still held one lot.
1241
+ lot = self.instrument.lot_size
1242
+ if not is_lot_multiple(self.quantity, lot) or not is_lot_multiple(amount, lot):
1243
+ # - stale instrument metadata or a connector fault. Reported, then booked to the
1244
+ # nearest lot: dropping the deal is the worse error.
1245
+ logger.error(
1246
+ f"[{self.instrument.symbol}] booking {self.quantity} + {amount} is not on the "
1247
+ f"lot grid (lot_size {lot}) — rounding to the nearest lot"
1248
+ )
1215
1249
  return self.update_position(
1216
1250
  timestamp,
1217
- self.instrument.round_size_down(self.quantity + amount),
1251
+ add_in_lots(self.quantity, amount, lot),
1218
1252
  exec_price,
1219
1253
  fee_amount,
1220
1254
  conversion_rate=conversion_rate,
@@ -15,6 +15,7 @@ from qubx.core.basics import (
15
15
  OrderBook,
16
16
  Position,
17
17
  Quote,
18
+ RejectCause,
18
19
  Trade,
19
20
  )
20
21
 
@@ -71,6 +72,7 @@ class OrderAcceptedEvent(OrderEvent):
71
72
  class OrderRejectedEvent(OrderEvent):
72
73
  reason: str
73
74
  code: str | None = None
75
+ cause: RejectCause = RejectCause.UNKNOWN
74
76
 
75
77
 
76
78
  @msg
@@ -137,12 +139,14 @@ class OrderUpdatedEvent(OrderEvent):
137
139
  class OrderCancelRejectedEvent(OrderEvent):
138
140
  reason: str
139
141
  code: str | None = None
142
+ cause: RejectCause = RejectCause.UNKNOWN
140
143
 
141
144
 
142
145
  @msg
143
146
  class OrderUpdateRejectedEvent(OrderEvent):
144
147
  reason: str
145
148
  code: str | None = None
149
+ cause: RejectCause = RejectCause.UNKNOWN
146
150
 
147
151
 
148
152
  @msg
@@ -3,6 +3,8 @@ import numpy as np
3
3
  cimport numpy as np
4
4
  import pandas as pd
5
5
  import datetime
6
+ from libc.float cimport DBL_EPSILON
7
+ from libc.math cimport ceil, copysign, fabs, floor, fmax, pow, round
6
8
 
7
9
  NS = 1_000_000_000
8
10
 
@@ -82,9 +84,46 @@ cpdef recognize_timeframe(timeframe):
82
84
  return tf
83
85
 
84
86
 
87
+ cdef inline double _snap_tick_noise(double x) noexcept:
88
+ """
89
+ Snap x to the nearest integer when it is already within float noise of one. 0.29 * 100 is
90
+ 28.999999999999996, which would otherwise floor to 28. The tolerance is relative because
91
+ float error scales with magnitude: 3.6e-15 at x=29, 3.7e-9 at x=2.9e7.
92
+ """
93
+ cdef double nearest = round(x)
94
+ cdef double tolerance = 16.0 * DBL_EPSILON * fmax(1.0, fabs(x))
95
+ if fabs(x - nearest) <= tolerance:
96
+ return nearest
97
+ return x
98
+
99
+
100
+ # - previous implementation rounded the SCALED value to `precision` decimals before the floor.
101
+ # At precision 0 that window is half a lot, so prec_floor(3.7, 0) gave 4.0 and
102
+ # prec_ceil(3.4, 0) gave 3.0 — neither was a floor or a ceil any more.
103
+ # return np.sign(a) * np.true_divide(np.ceil(round(abs(a) * 10**precision, precision)), 10**precision)
104
+ # return np.sign(a) * np.true_divide(np.floor(round(abs(a) * 10**precision, precision)), 10**precision)
85
105
  cpdef double prec_ceil(double a, int precision):
86
- return np.sign(a) * np.true_divide(np.ceil(round(abs(a) * 10**precision, precision)), 10**precision)
106
+ cdef double scale = pow(10.0, precision)
107
+ cdef double ticks = _snap_tick_noise(fabs(a) * scale)
108
+ return copysign(ceil(ticks) / scale, a)
87
109
 
88
110
 
89
111
  cpdef double prec_floor(double a, int precision):
90
- return np.sign(a) * np.true_divide(np.floor(round(abs(a) * 10**precision, precision)), 10**precision)
112
+ cdef double scale = pow(10.0, precision)
113
+ cdef double ticks = _snap_tick_noise(fabs(a) * scale)
114
+ return copysign(floor(ticks) / scale, a)
115
+
116
+
117
+ cpdef double add_in_lots(double quantity, double amount, double lot_size) noexcept:
118
+ """
119
+ Sum two lot multiples in whole lots, so a subtraction cannot land a few ulp off the grid.
120
+ """
121
+ return (round(quantity / lot_size) + round(amount / lot_size)) * lot_size
122
+
123
+
124
+ cpdef bint is_lot_multiple(double quantity, double lot_size) noexcept:
125
+ """
126
+ True when quantity sits on the lot grid, within the float noise of one division.
127
+ """
128
+ cdef double raw = quantity / lot_size
129
+ return fabs(raw - round(raw)) <= 16.0 * DBL_EPSILON * fmax(1.0, fabs(raw))
@@ -287,12 +287,14 @@ class TextualStrategyApp(App[None]):
287
287
  with Vertical(id="tables-container", classes="tables-column"):
288
288
  # Positions panel
289
289
  self.positions_panel = Vertical(id="positions-panel", classes="side-panel")
290
+ self.positions_panel.border_title = "Positions"
290
291
  with self.positions_panel:
291
292
  self.positions_table = PositionsTable(id="positions-table")
292
293
  self.positions_table.setup_columns()
293
294
  yield self.positions_table
294
295
  # Orders panel
295
296
  self.orders_panel = Vertical(id="orders-panel", classes="side-panel")
297
+ self.orders_panel.border_title = "Orders"
296
298
  with self.orders_panel:
297
299
  self.orders_table = OrdersTable(id="orders-table")
298
300
  self.orders_table.setup_columns()
@@ -109,6 +109,14 @@ class OrdersTable(DataTable):
109
109
  except Exception as e:
110
110
  logger.warning(f"Failed to add order row {order_id}: {e}")
111
111
 
112
+ self._retitle(len(new_orders))
113
+
114
+ def _retitle(self, active_orders: int) -> None:
115
+ # - the border lives on the containing panel, so the count goes there
116
+ panel = self.parent
117
+ if panel is not None:
118
+ panel.border_title = f"Orders ({active_orders})"
119
+
112
120
  def _get_price_precision(self, row: dict) -> int:
113
121
  """Get price precision for formatting (default to 2 decimals)."""
114
122
  # Could be enhanced to use instrument precision if available
@@ -142,6 +142,13 @@ class PositionsTable(DataTable):
142
142
  logger.warning(f"Failed to add position row {pos_key}: {e}")
143
143
 
144
144
  self._sort()
145
+ self._retitle(sum(1 for r in sanitized_rows if r.get("qty")))
146
+
147
+ def _retitle(self, open_positions: int) -> None:
148
+ # - the border lives on the containing panel, so the count goes there
149
+ panel = self.parent
150
+ if panel is not None:
151
+ panel.border_title = f"Positions ({open_positions})"
145
152
 
146
153
  def _get_side(self, r: dict) -> str:
147
154
  leverage = r.get("leverage", 0.0)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes