Qubx 1.3.3.dev2__tar.gz → 1.3.5__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 (310) hide show
  1. {qubx-1.3.3.dev2 → qubx-1.3.5}/PKG-INFO +1 -1
  2. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/_version.py +2 -2
  3. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/commands.py +0 -19
  4. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/release.py +2 -6
  5. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/s3.py +0 -92
  6. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/interfaces.py +3 -2
  7. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/series.pxd +3 -2
  8. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/series.pyi +8 -1
  9. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/series.pyx +69 -9
  10. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/emitters/base.py +29 -5
  11. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/emitters/composite.py +20 -4
  12. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/emitters/csv.py +65 -4
  13. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/emitters/prometheus.py +65 -2
  14. {qubx-1.3.3.dev2 → qubx-1.3.5}/.gitignore +0 -0
  15. {qubx-1.3.3.dev2 → qubx-1.3.5}/LICENSE +0 -0
  16. {qubx-1.3.3.dev2 → qubx-1.3.5}/README.md +0 -0
  17. {qubx-1.3.3.dev2 → qubx-1.3.5}/pyproject.toml +0 -0
  18. {qubx-1.3.3.dev2 → qubx-1.3.5}/scripts/build.py +0 -0
  19. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/__init__.py +0 -0
  20. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/_nb_magic.py +0 -0
  21. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/__init__.py +0 -0
  22. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/account.py +0 -0
  23. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/broker.py +0 -0
  24. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/data.py +0 -0
  25. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/iteratedstream.py +0 -0
  26. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/management.py +0 -0
  27. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/ome.py +0 -0
  28. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/optimization.py +0 -0
  29. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/runner.py +0 -0
  30. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/sentinels.py +0 -0
  31. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/simulated_data.py +0 -0
  32. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/simulated_exchange.py +0 -0
  33. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/simulator.py +0 -0
  34. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/transfers.py +0 -0
  35. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/backtester/utils.py +0 -0
  36. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/__init__.py +0 -0
  37. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/deploy.py +0 -0
  38. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/misc.py +0 -0
  39. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/resolver.py +0 -0
  40. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/theme.py +0 -0
  41. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/tui.py +0 -0
  42. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/cli/user_config.py +0 -0
  43. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/config.py +0 -0
  44. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/__init__.py +0 -0
  45. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/__init__.py +0 -0
  46. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/account.py +0 -0
  47. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  48. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  49. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/broker.py +0 -0
  50. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  51. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/data.py +0 -0
  52. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  53. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  54. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  55. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  56. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  57. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  58. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  59. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  60. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  61. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  62. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  63. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  64. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  65. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  66. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  67. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  68. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/okx/account.py +0 -0
  69. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
  70. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  71. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/factory.py +0 -0
  72. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  73. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  74. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  75. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  76. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  77. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  78. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  79. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  80. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  81. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  82. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
  83. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  84. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  85. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  86. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/utils.py +0 -0
  87. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  88. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/registry.py +0 -0
  89. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/tardis/data.py +0 -0
  90. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/connectors/tardis/utils.py +0 -0
  91. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/control/__init__.py +0 -0
  92. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/control/builtin.py +0 -0
  93. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/control/decorator.py +0 -0
  94. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/control/executor.py +0 -0
  95. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/control/interfaces.py +0 -0
  96. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/control/server.py +0 -0
  97. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/control/types.py +0 -0
  98. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/__init__.py +0 -0
  99. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/account.py +0 -0
  100. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/basics.py +0 -0
  101. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/context.py +0 -0
  102. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/detectors/__init__.py +0 -0
  103. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/detectors/delisting.py +0 -0
  104. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/detectors/stale.py +0 -0
  105. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/errors.py +0 -0
  106. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/exceptions.py +0 -0
  107. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/helpers.py +0 -0
  108. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/initializer.py +0 -0
  109. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/loggers.py +0 -0
  110. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/lookups.py +0 -0
  111. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/metrics.py +0 -0
  112. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/mixins/__init__.py +0 -0
  113. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/mixins/market.py +0 -0
  114. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/mixins/processing.py +0 -0
  115. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/mixins/subscription.py +0 -0
  116. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/mixins/trading.py +0 -0
  117. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/mixins/universe.py +0 -0
  118. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/mixins/utils.py +0 -0
  119. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/utils.pyi +0 -0
  120. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/core/utils.pyx +0 -0
  121. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/__init__.py +0 -0
  122. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/cache.py +0 -0
  123. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/containers.py +0 -0
  124. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/guards.py +0 -0
  125. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/registry.py +0 -0
  126. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/storage.py +0 -0
  127. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/storages/ccxt.py +0 -0
  128. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/storages/csv.py +0 -0
  129. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/storages/handy.py +0 -0
  130. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/storages/multi.py +0 -0
  131. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/storages/questdb.py +0 -0
  132. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/storages/stub.py +0 -0
  133. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/storages/utils.py +0 -0
  134. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/data/transformers.py +0 -0
  135. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/emitters/__init__.py +0 -0
  136. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/emitters/indicator.py +0 -0
  137. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/emitters/inmemory.py +0 -0
  138. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/emitters/questdb.py +0 -0
  139. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/__init__.py +0 -0
  140. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/composite.py +0 -0
  141. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/formatters/__init__.py +0 -0
  142. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/formatters/base.py +0 -0
  143. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/formatters/incremental.py +0 -0
  144. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/formatters/slack.py +0 -0
  145. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/formatters/target_position.py +0 -0
  146. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/redis_streams.py +0 -0
  147. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/exporters/slack.py +0 -0
  148. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/gathering/simplest.py +0 -0
  149. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/health/__init__.py +0 -0
  150. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/health/base.py +0 -0
  151. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/health/dummy.py +0 -0
  152. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/loggers/__init__.py +0 -0
  153. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/loggers/csv.py +0 -0
  154. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/loggers/factory.py +0 -0
  155. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/loggers/inmemory.py +0 -0
  156. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/loggers/mongo.py +0 -0
  157. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/loggers/postgres.py +0 -0
  158. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/notifications/__init__.py +0 -0
  159. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/notifications/composite.py +0 -0
  160. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/notifications/slack.py +0 -0
  161. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/notifications/throttler.py +0 -0
  162. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/pandaz/__init__.py +0 -0
  163. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/pandaz/stats.py +0 -0
  164. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/pandaz/ta.py +0 -0
  165. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/pandaz/utils.py +0 -0
  166. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/plugins/__init__.py +0 -0
  167. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/plugins/loader.py +0 -0
  168. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/rate_limiting/__init__.py +0 -0
  169. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/rate_limiting/backend.py +0 -0
  170. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/rate_limiting/config.py +0 -0
  171. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/rate_limiting/engine.py +0 -0
  172. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/rate_limiting/ip_resolver.py +0 -0
  173. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/rate_limiting/manager.py +0 -0
  174. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/rate_limiting/pools.py +0 -0
  175. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/rate_limiting/redis_backend.py +0 -0
  176. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/_build.py +0 -0
  177. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/crypto-fees.ini +0 -0
  178. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  179. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  180. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  181. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  182. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  183. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  184. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  185. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  186. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  187. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  188. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  189. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restarts/__init__.py +0 -0
  190. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restarts/state_resolvers.py +0 -0
  191. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restarts/time_finders.py +0 -0
  192. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restorers/__init__.py +0 -0
  193. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restorers/balance.py +0 -0
  194. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restorers/factory.py +0 -0
  195. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restorers/interfaces.py +0 -0
  196. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restorers/position.py +0 -0
  197. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restorers/signal.py +0 -0
  198. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restorers/state.py +0 -0
  199. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/restorers/utils.py +0 -0
  200. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/state/__init__.py +0 -0
  201. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/state/dummy.py +0 -0
  202. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/state/redis.py +0 -0
  203. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/ta/__init__.py +0 -0
  204. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/ta/indicators.pxd +0 -0
  205. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/ta/indicators.pyi +0 -0
  206. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/ta/indicators.pyx +0 -0
  207. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/__init__.py +0 -0
  208. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/base.py +0 -0
  209. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  210. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/project/config.yml.j2 +0 -0
  211. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  212. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  213. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  214. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  215. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  216. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/project/template.yml +0 -0
  217. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  218. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  219. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  220. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  221. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  222. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  223. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.python-version +0 -0
  224. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  225. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  226. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  227. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  228. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/README.md.j2 +0 -0
  229. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  230. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/justfile.j2 +0 -0
  231. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  232. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/research/.gitkeep +0 -0
  233. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  234. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  235. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  236. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/template.yml +0 -0
  237. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  238. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  239. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  240. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/simple/config.yml.j2 +0 -0
  241. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  242. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  243. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  244. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/templates/simple/template.yml +0 -0
  245. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/trackers/__init__.py +0 -0
  246. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/trackers/advanced.py +0 -0
  247. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/trackers/composite.py +0 -0
  248. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/trackers/rebalancers.py +0 -0
  249. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/trackers/riskctrl.py +0 -0
  250. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/trackers/sizers.py +0 -0
  251. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/__init__.py +0 -0
  252. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/_pyxreloader.py +0 -0
  253. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/charting/lookinglass.py +0 -0
  254. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  255. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/charting/orderbook.py +0 -0
  256. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/collections.py +0 -0
  257. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/hft/__init__.py +0 -0
  258. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/hft/numba_utils.py +0 -0
  259. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/hft/orderbook.pyi +0 -0
  260. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/hft/orderbook.pyx +0 -0
  261. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/marketdata/binance.py +0 -0
  262. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/marketdata/ccxt.py +0 -0
  263. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/marketdata/dukas.py +0 -0
  264. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/misc.py +0 -0
  265. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/nonce.py +0 -0
  266. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/ntp.py +0 -0
  267. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/numbers_utils.py +0 -0
  268. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/orderbook.py +0 -0
  269. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/plotting/__init__.py +0 -0
  270. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/plotting/dashboard.py +0 -0
  271. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/plotting/data.py +0 -0
  272. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/plotting/interfaces.py +0 -0
  273. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  274. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  275. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/questdb.py +0 -0
  276. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/rate_limiter.py +0 -0
  277. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/results.py +0 -0
  278. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/ringbuffer.pxd +0 -0
  279. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/ringbuffer.pyi +0 -0
  280. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/ringbuffer.pyx +0 -0
  281. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/__init__.py +0 -0
  282. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  283. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/accounts.py +0 -0
  284. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/configs.py +0 -0
  285. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/factory.py +0 -0
  286. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/kernel_service.py +0 -0
  287. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/runner.py +0 -0
  288. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/__init__.py +0 -0
  289. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/app.py +0 -0
  290. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/handlers.py +0 -0
  291. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/init_code.py +0 -0
  292. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/kernel.py +0 -0
  293. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  294. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  295. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
  296. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  297. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  298. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  299. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  300. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  301. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  302. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/s3.py +0 -0
  303. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/slack.py +0 -0
  304. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/throttler.py +0 -0
  305. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/time.py +0 -0
  306. {qubx-1.3.3.dev2 → qubx-1.3.5}/src/qubx/utils/websocket_manager.py +0 -0
  307. {qubx-1.3.3.dev2 → qubx-1.3.5}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  308. {qubx-1.3.3.dev2 → qubx-1.3.5}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  309. {qubx-1.3.3.dev2 → qubx-1.3.5}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  310. {qubx-1.3.3.dev2 → qubx-1.3.5}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 1.3.3.dev2
3
+ Version: 1.3.5
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 = '1.3.3.dev2'
22
- __version_tuple__ = version_tuple = (1, 3, 3, 'dev2')
21
+ __version__ = version = '1.3.5'
22
+ __version_tuple__ = version_tuple = (1, 3, 5)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1060,25 +1060,6 @@ def s3_rm_cmd(path: str, recursive: bool, yes: bool):
1060
1060
  s3_rm(path, recursive=recursive)
1061
1061
 
1062
1062
 
1063
- @s3.command("parquet-stats")
1064
- @click.argument("path", type=str)
1065
- @click.option("--per-column", is_flag=True, help="Show per-column chunk sizes for row group 0.")
1066
- def s3_parquet_stats_cmd(path: str, per_column: bool):
1067
- """
1068
- Print parquet file-level summary and row-group size distribution.
1069
-
1070
- Accepts both S3 paths (account:bucket/key) and local paths. Reads only
1071
- the footer, so the full file is NOT downloaded.
1072
-
1073
- Examples:\n
1074
- qubx s3 parquet-stats r2:dvault-features/.../part-2026-03.parquet\n
1075
- qubx s3 parquet-stats ./local.parquet --per-column
1076
- """
1077
- from .s3 import s3_parquet_stats
1078
-
1079
- s3_parquet_stats(path, per_column=per_column)
1080
-
1081
-
1082
1063
  @s3.command("cp")
1083
1064
  @click.argument("src", type=str)
1084
1065
  @click.argument("dst", type=str)
@@ -1454,16 +1454,12 @@ def _bundle_source_overrides(
1454
1454
  subprocess.run(["git", "clone", git_url, checkout_dir], check=True, capture_output=True, text=True)
1455
1455
  subprocess.run(["git", "checkout", commit_sha], cwd=checkout_dir, check=True, capture_output=True, text=True)
1456
1456
 
1457
- # Honor [tool.uv.sources] `subdirectory` for monorepo git sources
1458
- subdir = source.get("subdirectory")
1459
- build_cwd = os.path.join(checkout_dir, subdir) if subdir else checkout_dir
1460
-
1461
- logger.info(f" Bundling {pkg_name}=={pkg_ver} from {build_cwd} ...")
1457
+ logger.info(f" Bundling {pkg_name}=={pkg_ver} from {checkout_dir} ...")
1462
1458
  os.makedirs(wheels_dir, exist_ok=True)
1463
1459
  try:
1464
1460
  result = subprocess.run(
1465
1461
  ["uv", "build", "--wheel", ".", "--out-dir", wheels_dir],
1466
- cwd=build_cwd,
1462
+ cwd=checkout_dir,
1467
1463
  check=True,
1468
1464
  capture_output=True,
1469
1465
  text=True,
@@ -109,98 +109,6 @@ def s3_cp(src: str, dst: str, recursive: bool = False) -> None:
109
109
  raise click.Abort()
110
110
 
111
111
 
112
- def s3_parquet_stats(path: str, per_column: bool = False) -> None:
113
- """Print file-level summary + row-group size distribution for one parquet file.
114
-
115
- Accepts either an S3 URI (``account:bucket/key``) or a local filesystem path.
116
- Uses pyarrow for metadata parsing so no data pages are fetched.
117
- """
118
- import pyarrow.parquet as pq
119
- from pyarrow.fs import LocalFileSystem
120
-
121
- # Heuristic: an S3 URI has "account:..." where account is non-empty and
122
- # contains no path separator. Fall back to local for anything else.
123
- is_s3 = ":" in path and "/" not in path.split(":", 1)[0] and not path.startswith("/")
124
- if is_s3:
125
- try:
126
- client, s3_path = S3Client.from_uri(path)
127
- except ValueError as e:
128
- raise click.BadParameter(str(e))
129
- fs = client.fs
130
- open_path = s3_path
131
- else:
132
- fs = LocalFileSystem()
133
- open_path = path
134
-
135
- try:
136
- with fs.open_input_file(open_path) as f:
137
- meta = pq.ParquetFile(f).metadata
138
- except Exception as e:
139
- click.echo(click.style(f"Error reading {path}: {e}", fg="red"), err=True)
140
- raise click.Abort()
141
-
142
- click.echo(click.style(f"File: {path}", fg="cyan", bold=True))
143
- click.echo(f" num_rows: {meta.num_rows:,}")
144
- click.echo(f" num_row_groups: {meta.num_row_groups}")
145
- click.echo(f" num_columns: {meta.num_columns}")
146
- click.echo(f" format_version: {meta.format_version}")
147
- if meta.created_by:
148
- click.echo(f" created_by: {meta.created_by}")
149
-
150
- if meta.num_row_groups == 0:
151
- return
152
-
153
- # Parquet spec: RowGroup.total_byte_size is the SUM of uncompressed
154
- # column sizes. The compressed on-disk size is the sum of
155
- # ColumnChunk.total_compressed_size across columns.
156
- rows: list[int] = []
157
- per_rg_compressed: list[int] = []
158
- per_rg_uncompressed: list[int] = []
159
- compressions: set[str] = set()
160
- for i in range(meta.num_row_groups):
161
- rg = meta.row_group(i)
162
- rows.append(rg.num_rows)
163
- c_sz = 0
164
- u_sz = 0
165
- for c in range(rg.num_columns):
166
- col = rg.column(c)
167
- c_sz += col.total_compressed_size
168
- u_sz += col.total_uncompressed_size
169
- compressions.add(str(col.compression))
170
- per_rg_compressed.append(c_sz)
171
- per_rg_uncompressed.append(u_sz)
172
-
173
- total_compressed = sum(per_rg_compressed)
174
- total_uncompressed = sum(per_rg_uncompressed)
175
- click.echo()
176
- click.echo(click.style("Row groups:", fg="cyan", bold=True))
177
- click.echo(f" rows per rg: min={min(rows):,} max={max(rows):,} avg={sum(rows)//len(rows):,}")
178
- click.echo(
179
- f" cmp per rg: min={_human_size(min(per_rg_compressed))} "
180
- f"max={_human_size(max(per_rg_compressed))} avg={_human_size(sum(per_rg_compressed) // len(per_rg_compressed))}"
181
- )
182
- click.echo(f" total: compressed={_human_size(total_compressed)} uncompressed={_human_size(total_uncompressed)}")
183
- if total_uncompressed > 0:
184
- ratio = 100 * total_compressed / total_uncompressed
185
- click.echo(f" compression: {', '.join(sorted(compressions))} ({ratio:.1f}% of uncompressed)")
186
- else:
187
- click.echo(f" compression: {', '.join(sorted(compressions))}")
188
-
189
- if per_column:
190
- click.echo()
191
- click.echo(click.style("Columns (row group 0):", fg="cyan", bold=True))
192
- rg0 = meta.row_group(0)
193
- click.echo(f" {'path':30s} {'type':12s} {'compressed':>11s} {'uncompressed':>12s} pct compression")
194
- for c in range(rg0.num_columns):
195
- col = rg0.column(c)
196
- pct = (100 * col.total_compressed_size / col.total_uncompressed_size) if col.total_uncompressed_size else 0.0
197
- click.echo(
198
- f" {col.path_in_schema:30s} {str(col.physical_type):12s} "
199
- f"{_human_size(col.total_compressed_size):>11s} {_human_size(col.total_uncompressed_size):>12s} "
200
- f"{pct:5.1f}% {col.compression}"
201
- )
202
-
203
-
204
112
  def s3_accounts() -> None:
205
113
  s = get_settings()
206
114
  if not s.s3:
@@ -12,6 +12,7 @@ This module includes:
12
12
  - Strategy lifecycle notifiers
13
13
  """
14
14
 
15
+ import datetime
15
16
  import inspect
16
17
  import traceback
17
18
  from dataclasses import dataclass
@@ -2790,7 +2791,7 @@ class IMetricEmitter:
2790
2791
 
2791
2792
  def emit_signals(
2792
2793
  self,
2793
- time: dt_64,
2794
+ time: dt_64 | pd.Timestamp | datetime.datetime,
2794
2795
  signals: list[Signal],
2795
2796
  account: "IAccountViewer",
2796
2797
  target_positions: list["TargetPosition"] | None = None,
@@ -2810,7 +2811,7 @@ class IMetricEmitter:
2810
2811
 
2811
2812
  def emit_deals(
2812
2813
  self,
2813
- time: dt_64,
2814
+ time: dt_64 | pd.Timestamp | datetime.datetime,
2814
2815
  instrument: Instrument,
2815
2816
  deals: list[Deal],
2816
2817
  account: "IAccountViewer",
@@ -187,8 +187,9 @@ cdef class BundledSeries(TimeSeries):
187
187
  """
188
188
  Virtual series that bundles fields from multiple source TimeSeries.
189
189
  """
190
- cdef dict _fields # field_name -> TimeSeries
191
- cdef list _field_names # ordered list of field names
190
+ cdef dict _fields # field_name -> TimeSeries
191
+ cdef list _field_names # ordered list of field names
192
+ cdef long long _align_timeframe # alignment bin (ns) for cross-source timestamp flooring
192
193
 
193
194
  cdef double _lookup_value(BundledSeries self, TimeSeries series, long long time)
194
195
  cdef TimeSeries _find_root(BundledSeries self, TimeSeries series)
@@ -266,7 +266,14 @@ class BundledSeries(TimeSeries):
266
266
  _fields: dict[str, TimeSeries]
267
267
  _field_names: list[str]
268
268
 
269
- def __init__(self, name: str, timeframe, fields: dict[str, TimeSeries], max_series_length: int | float = ...) -> None: ...
269
+ def __init__(
270
+ self,
271
+ name: str,
272
+ timeframe,
273
+ fields: dict[str, TimeSeries],
274
+ align_timeframe: str | None = ...,
275
+ max_series_length: int | float = ...,
276
+ ) -> None: ...
270
277
  def update(self, time: int, value: Any, new_item_started: bool) -> Any: ...
271
278
  def to_series(self, length: int | None = None) -> pd.DataFrame: ...
272
279
 
@@ -126,15 +126,50 @@ cdef class Indexed:
126
126
 
127
127
  def lookup_idx(self, value, str method) -> int:
128
128
  """
129
- Find value's index in series using specified method (ffill: previous index, bfill: next index)
129
+ Find value's index in series using specified method.
130
+
131
+ - ffill: previous index — equivalent to np.searchsorted(side='right') - 1.
132
+ Returns -1 if value is below all stored items.
133
+ - bfill: next index — equivalent to np.searchsorted(side='left').
134
+ Returns -1 if value is above all stored items.
135
+
136
+ Implementation note: this is a pure-Cython binary search over the Python
137
+ list `self.values`. It replaces a previous `np.searchsorted` call which
138
+ spent the bulk of its time in `np.asarray(list)` conversion (O(n) per
139
+ call) — `_wrapit` was the dominant hotspot in profiles. Element
140
+ comparisons here go through Python rich comparison, which is fast for
141
+ the int/float scalars actually stored. Behavior matches the original
142
+ for sorted ascending input (the only contract the rest of the codebase
143
+ relies on).
130
144
  """
145
+ cdef int n = len(self.values)
146
+ cdef int lo, hi, mid
131
147
  cdef int i0
148
+
132
149
  if method == 'ffill':
133
- i0 = int(np.searchsorted(self.values, value, side='right'))
150
+ # - rightmost insertion point, then -1
151
+ lo = 0
152
+ hi = n
153
+ while lo < hi:
154
+ mid = (lo + hi) >> 1
155
+ if self.values[mid] <= value:
156
+ lo = mid + 1
157
+ else:
158
+ hi = mid
159
+ i0 = lo
134
160
  return max(-1, i0 - 1)
135
161
  elif method == 'bfill':
136
- i0 = int(np.searchsorted(self.values, value, side='left'))
137
- return -1 if i0 >= len(self.values) else i0
162
+ # - leftmost insertion point
163
+ lo = 0
164
+ hi = n
165
+ while lo < hi:
166
+ mid = (lo + hi) >> 1
167
+ if self.values[mid] < value:
168
+ lo = mid + 1
169
+ else:
170
+ hi = mid
171
+ i0 = lo
172
+ return -1 if i0 >= n else i0
138
173
  else:
139
174
  raise ValueError(f"Unsupported method {method}")
140
175
 
@@ -1845,18 +1880,26 @@ cdef class BundledSeries(TimeSeries):
1845
1880
  # When either vtwap or ohlcv updates, bundle gathers values and triggers spread
1846
1881
  """
1847
1882
 
1848
- def __init__(self, str name, timeframe, dict fields, max_series_length=INFINITY):
1883
+ def __init__(self, str name, timeframe, dict fields, align_timeframe=None, max_series_length=INFINITY):
1849
1884
  """
1850
1885
  Args:
1851
1886
  name: Name of this bundled series
1852
1887
  timeframe: Timeframe string (e.g., "1m", "1h")
1853
1888
  fields: Dict mapping field names to TimeSeries
1854
1889
  e.g., {"vwap": vtwap_ts, "close": close_ts}
1890
+ align_timeframe: Optional timeframe string used to bin-align source
1891
+ timestamps before joining (floor_t64(t, align_timeframe)).
1892
+ Defaults to `timeframe` when None. Use this when source series
1893
+ have timestamps offset within the same logical bar (e.g. OHLC
1894
+ at bar-open x:00:00 vs vendor data shifted to bar-close x:14:55
1895
+ for a 15-minute timeframe) — both floor to the same bin.
1855
1896
  max_series_length: Maximum number of values to store
1856
1897
  """
1857
1898
  super().__init__(name, timeframe, max_series_length)
1858
1899
  self._fields = fields
1859
1900
  self._field_names = list(fields.keys())
1901
+ # - alignment bin: explicit align_timeframe if provided, else self.timeframe (already ns after super().__init__)
1902
+ self._align_timeframe = recognize_timeframe(align_timeframe) if align_timeframe is not None else self.timeframe
1860
1903
 
1861
1904
  # Attach to each source series' calculation chain
1862
1905
  for field_name, series in fields.items():
@@ -1883,22 +1926,39 @@ cdef class BundledSeries(TimeSeries):
1883
1926
  return series
1884
1927
 
1885
1928
  def _initial_data_recalculate_bundled(self):
1886
- """Align all sources by timestamp for initial calculation."""
1887
- # Collect all source data as pandas Series
1929
+ """
1930
+ Align all sources by timestamp for initial calculation.
1931
+
1932
+ Each source's timestamps are floored to self._align_timeframe first, then
1933
+ outer-joined and forward-filled so mixed-cadence sources (e.g. 15m OHLC +
1934
+ 1h OI) and offset-stamped sources (e.g. open-stamped OHLC x:00:00 +
1935
+ close-stamped vendor x:14:55 within the same 15m bin) all land in the
1936
+ same row. Rows where any field is still NaN after ffill are dropped —
1937
+ this handles warm-up bars where the slow-cadence source hasn't ticked yet.
1938
+ """
1888
1939
  cdef list dfs = []
1889
1940
  cdef str name
1890
1941
  cdef TimeSeries series
1942
+ cdef long long tf_ns = self._align_timeframe
1891
1943
 
1892
1944
  for name, series in self._fields.items():
1893
1945
  s = series.to_series()
1894
1946
  if len(s) > 0:
1947
+ # - floor each timestamp to the align bin, keep last value within each bin
1948
+ floored = pd.DatetimeIndex(
1949
+ [pd.Timestamp(floor_t64(<long long> t.value, tf_ns)) for t in s.index]
1950
+ )
1951
+ s = s.copy()
1952
+ s.index = floored
1953
+ s = s[~s.index.duplicated(keep='last')]
1895
1954
  dfs.append(s.rename(name))
1896
1955
 
1897
1956
  if not dfs:
1898
1957
  return
1899
1958
 
1900
- # Align by timestamp (inner join - only times present in all sources)
1901
- aligned = pd.concat(dfs, axis=1).dropna()
1959
+ # - outer-join on floored timestamps, ffill to carry slow-cadence sources forward,
1960
+ # - then drop leading rows where any field is still NaN (warm-up)
1961
+ aligned = pd.concat(dfs, axis=1).sort_index().ffill().dropna()
1902
1962
 
1903
1963
  # Process each aligned row
1904
1964
  for t in aligned.index:
@@ -4,10 +4,13 @@ Base Metric Emitter.
4
4
  This module provides a base implementation of IMetricEmitter that can be extended by other emitters.
5
5
  """
6
6
 
7
+ import datetime
7
8
  from typing import Any, Dict, List, Optional, Set
8
9
 
10
+ import pandas as pd
11
+
9
12
  from qubx import logger
10
- from qubx.core.basics import Instrument, Signal, TargetPosition, dt_64
13
+ from qubx.core.basics import Deal, Instrument, Signal, TargetPosition, dt_64
11
14
  from qubx.core.interfaces import IAccountViewer, IMetricEmitter, IStrategyContext
12
15
  from qubx.utils.time import to_timedelta
13
16
 
@@ -199,10 +202,10 @@ class BaseMetricEmitter(IMetricEmitter):
199
202
 
200
203
  def emit_signals(
201
204
  self,
202
- time: dt_64,
203
- signals: list["Signal"],
204
- account: "IAccountViewer",
205
- target_positions: list["TargetPosition"] | None = None,
205
+ time: dt_64 | pd.Timestamp | datetime.datetime,
206
+ signals: list[Signal],
207
+ account: IAccountViewer,
208
+ target_positions: list[TargetPosition] | None = None,
206
209
  ) -> None:
207
210
  """
208
211
  Emit signals to the monitoring system.
@@ -218,6 +221,27 @@ class BaseMetricEmitter(IMetricEmitter):
218
221
  """
219
222
  pass
220
223
 
224
+ def emit_deals(
225
+ self,
226
+ time: dt_64 | pd.Timestamp | datetime.datetime,
227
+ instrument: Instrument,
228
+ deals: list[Deal],
229
+ account: IAccountViewer,
230
+ ) -> None:
231
+ """
232
+ Emit deals to the monitoring system.
233
+
234
+ Base implementation does nothing - subclasses should override this method
235
+ to implement specific deal emission logic.
236
+
237
+ Args:
238
+ time: Timestamp when the deals were generated
239
+ instrument: Instrument the deals belong to
240
+ deals: List of deals to emit
241
+ account: Account viewer to get account information like total capital, leverage, etc.
242
+ """
243
+ pass
244
+
221
245
  def notify(self, context: IStrategyContext) -> None:
222
246
  """
223
247
  Notify the metric emitter of a time update.
@@ -4,10 +4,13 @@ Composite Metric Emitter.
4
4
  This module provides a composite implementation of IMetricEmitter that delegates to multiple emitters.
5
5
  """
6
6
 
7
+ import datetime
7
8
  from typing import Dict, List, Optional
8
9
 
10
+ import pandas as pd
11
+
9
12
  from qubx import logger
10
- from qubx.core.basics import Deal, Instrument, Signal, dt_64
13
+ from qubx.core.basics import Deal, Instrument, Signal, TargetPosition, dt_64
11
14
  from qubx.core.interfaces import IAccountViewer, IMetricEmitter, IStrategyContext
12
15
  from qubx.emitters.base import BaseMetricEmitter
13
16
 
@@ -70,7 +73,13 @@ class CompositeMetricEmitter(BaseMetricEmitter):
70
73
  except Exception as e:
71
74
  logger.error(f"Error emitting strategy stats to {emitter.__class__.__name__}: {e}")
72
75
 
73
- def emit_signals(self, time: dt_64, signals: list["Signal"], account: "IAccountViewer") -> None:
76
+ def emit_signals(
77
+ self,
78
+ time: dt_64 | pd.Timestamp | datetime.datetime,
79
+ signals: list[Signal],
80
+ account: IAccountViewer,
81
+ target_positions: list[TargetPosition] | None = None,
82
+ ) -> None:
74
83
  """
75
84
  Emit signals to all configured emitters.
76
85
 
@@ -78,14 +87,21 @@ class CompositeMetricEmitter(BaseMetricEmitter):
78
87
  time: Timestamp when the signals were generated
79
88
  signals: List of signals to emit
80
89
  account: Account viewer to get account information
90
+ target_positions: List of target positions (optional)
81
91
  """
82
92
  for emitter in self._emitters:
83
93
  try:
84
- emitter.emit_signals(time, signals, account)
94
+ emitter.emit_signals(time, signals, account, target_positions)
85
95
  except Exception as e:
86
96
  logger.error(f"Error emitting signals to {emitter.__class__.__name__}: {e}")
87
97
 
88
- def emit_deals(self, time: dt_64, instrument: Instrument, deals: list[Deal], account: "IAccountViewer") -> None:
98
+ def emit_deals(
99
+ self,
100
+ time: dt_64 | pd.Timestamp | datetime.datetime,
101
+ instrument: Instrument,
102
+ deals: list[Deal],
103
+ account: IAccountViewer,
104
+ ) -> None:
89
105
  """
90
106
  Emit deals to all configured emitters.
91
107
 
@@ -4,12 +4,15 @@ CSV Metric Emitter.
4
4
  This module provides an implementation of IMetricEmitter that exports metrics to a CSV file.
5
5
  """
6
6
 
7
+ import datetime
7
8
  import os
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
12
+ import pandas as pd
13
+
11
14
  from qubx import logger
12
- from qubx.core.basics import Signal, dt_64
15
+ from qubx.core.basics import Deal, Instrument, Signal, TargetPosition, dt_64
13
16
  from qubx.core.interfaces import IAccountViewer
14
17
  from qubx.emitters.base import BaseMetricEmitter
15
18
  from qubx.utils.ntp import time_now
@@ -84,7 +87,13 @@ class CSVMetricEmitter(BaseMetricEmitter):
84
87
  except Exception as e:
85
88
  logger.error(f"[CSVMetricEmitter] Failed to emit metric {name}: {e}")
86
89
 
87
- def emit_signals(self, time: dt_64, signals: list[Signal], account: IAccountViewer) -> None:
90
+ def emit_signals(
91
+ self,
92
+ time: dt_64 | pd.Timestamp | datetime.datetime,
93
+ signals: list[Signal],
94
+ account: IAccountViewer,
95
+ target_positions: list[TargetPosition] | None = None,
96
+ ) -> None:
88
97
  """
89
98
  Emit signals to CSV file.
90
99
 
@@ -92,10 +101,16 @@ class CSVMetricEmitter(BaseMetricEmitter):
92
101
  time: Timestamp when the signals were generated
93
102
  signals: List of signals to emit
94
103
  account: Account viewer to get account information
104
+ target_positions: Optional list of target positions generated from the signals
95
105
  """
96
106
  if not signals:
97
107
  return
98
108
 
109
+ target_positions_map: dict[Instrument, TargetPosition] = {}
110
+ if target_positions:
111
+ for target in target_positions:
112
+ target_positions_map[target.instrument] = target
113
+
99
114
  try:
100
115
  # Create a signals-specific CSV file
101
116
  signals_file_path = self._file_path.parent / f"signals_{self._file_path.stem}.csv"
@@ -104,7 +119,8 @@ class CSVMetricEmitter(BaseMetricEmitter):
104
119
  if not signals_file_path.exists():
105
120
  with open(signals_file_path, "w") as f:
106
121
  f.write(
107
- "timestamp,symbol,exchange,signal,price,stop,take,reference_price,group,comment,is_service\n"
122
+ "timestamp,symbol,exchange,signal,price,stop,take,reference_price,"
123
+ "target_position_size,group,comment,is_service\n"
108
124
  )
109
125
 
110
126
  # Write each signal to the CSV file
@@ -114,13 +130,58 @@ class CSVMetricEmitter(BaseMetricEmitter):
114
130
  stop = signal.stop if signal.stop is not None else ""
115
131
  take = signal.take if signal.take is not None else ""
116
132
  ref_price = signal.reference_price if signal.reference_price is not None else ""
133
+ target = target_positions_map.get(signal.instrument)
134
+ target_size = target.target_position_size if target is not None else ""
117
135
 
118
136
  with open(signals_file_path, "a") as f:
119
137
  f.write(
120
138
  f"{signal_time},{signal.instrument.symbol},{signal.instrument.exchange},"
121
- f"{signal.signal},{price},{stop},{take},{ref_price},"
139
+ f"{signal.signal},{price},{stop},{take},{ref_price},{target_size},"
122
140
  f"{signal.group},{signal.comment},{signal.is_service}\n"
123
141
  )
124
142
 
125
143
  except Exception as e:
126
144
  logger.error(f"[CSVMetricEmitter] Failed to emit signals: {e}")
145
+
146
+ def emit_deals(
147
+ self,
148
+ time: dt_64 | pd.Timestamp | datetime.datetime,
149
+ instrument: Instrument,
150
+ deals: list[Deal],
151
+ account: IAccountViewer,
152
+ ) -> None:
153
+ """
154
+ Emit deals to CSV file.
155
+
156
+ Args:
157
+ time: Timestamp when the deals were generated
158
+ instrument: Instrument the deals belong to
159
+ deals: List of deals to emit
160
+ account: Account viewer to get account information
161
+ """
162
+ if not deals:
163
+ return
164
+
165
+ try:
166
+ deals_file_path = self._file_path.parent / f"deals_{self._file_path.stem}.csv"
167
+
168
+ if not deals_file_path.exists():
169
+ with open(deals_file_path, "w") as f:
170
+ f.write(
171
+ "timestamp,symbol,exchange,amount,price,aggressive,"
172
+ "fee_amount,fee_currency,deal_id,order_id\n"
173
+ )
174
+
175
+ with open(deals_file_path, "a") as f:
176
+ for deal in deals:
177
+ deal_time = str(deal.time) if hasattr(deal.time, "__str__") else str(time)
178
+ fee_amount = deal.fee_amount if deal.fee_amount is not None else ""
179
+ fee_currency = deal.fee_currency if deal.fee_currency is not None else ""
180
+ f.write(
181
+ f"{deal_time},{instrument.symbol},{instrument.exchange},"
182
+ f"{deal.amount},{deal.price},{deal.aggressive},"
183
+ f"{fee_amount},{fee_currency},{deal.id},{deal.order_id}\n"
184
+ )
185
+
186
+ except Exception as e:
187
+ logger.error(f"[CSVMetricEmitter] Failed to emit deals: {e}")