Qubx 1.1.1.dev3__tar.gz → 1.1.2__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 (296) hide show
  1. {qubx-1.1.1.dev3 → qubx-1.1.2}/PKG-INFO +1 -1
  2. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/__init__.py +38 -8
  3. qubx-1.1.2/src/qubx/_version.py +24 -0
  4. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/data.py +3 -4
  5. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/management.py +29 -4
  6. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/runner.py +9 -6
  7. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/simulated_data.py +28 -17
  8. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/utils.py +17 -8
  9. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/commands.py +1 -11
  10. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/release.py +273 -260
  11. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/tui.py +35 -5
  12. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/account.py +19 -6
  13. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/broker.py +2 -2
  14. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/data.py +3 -4
  15. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +2 -2
  16. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/okx/account.py +51 -1
  17. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/ohlc.py +4 -4
  18. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/quote.py +14 -0
  19. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/utils.py +4 -4
  20. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/tardis/data.py +6 -5
  21. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/basics.py +18 -9
  22. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/detectors/delisting.py +4 -5
  23. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/detectors/stale.py +2 -3
  24. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/exceptions.py +4 -0
  25. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/helpers.py +5 -5
  26. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/loggers.py +18 -1
  27. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/lookups.py +2 -1
  28. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/metrics.py +233 -35
  29. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/mixins/market.py +5 -3
  30. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/mixins/trading.py +18 -9
  31. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/mixins/universe.py +3 -3
  32. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/cache.py +12 -10
  33. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/guards.py +7 -6
  34. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/storages/ccxt.py +5 -5
  35. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/storages/multi.py +19 -7
  36. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/storages/questdb.py +8 -4
  37. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/storages/utils.py +3 -2
  38. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/transformers.py +5 -5
  39. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/emitters/base.py +2 -3
  40. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/emitters/indicator.py +2 -2
  41. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/emitters/inmemory.py +3 -2
  42. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/emitters/prometheus.py +4 -3
  43. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/emitters/questdb.py +2 -1
  44. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/formatters/base.py +3 -4
  45. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/loggers/__init__.py +2 -0
  46. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/loggers/factory.py +2 -0
  47. qubx-1.1.2/src/qubx/loggers/postgres.py +216 -0
  48. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/pandaz/ta.py +7 -7
  49. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restarts/time_finders.py +24 -1
  50. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restorers/balance.py +74 -1
  51. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restorers/factory.py +8 -4
  52. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restorers/position.py +86 -1
  53. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restorers/signal.py +117 -0
  54. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restorers/state.py +87 -3
  55. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/trackers/advanced.py +3 -2
  56. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/trackers/sizers.py +2 -2
  57. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/charting/lookinglass.py +4 -3
  58. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/charting/mpl_helpers.py +2 -2
  59. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/misc.py +4 -2
  60. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/results.py +13 -11
  61. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/configs.py +1 -2
  62. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/runner.py +248 -37
  63. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/app.py +26 -2
  64. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/handlers.py +8 -3
  65. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/init_code.py +32 -16
  66. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/styles.tcss +15 -0
  67. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/widgets/__init__.py +2 -1
  68. qubx-1.1.2/src/qubx/utils/runner/textual/widgets/account_summary.py +47 -0
  69. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/time.py +19 -1
  70. qubx-1.1.1.dev3/src/qubx/_version.py +0 -34
  71. {qubx-1.1.1.dev3 → qubx-1.1.2}/.gitignore +0 -0
  72. {qubx-1.1.1.dev3 → qubx-1.1.2}/LICENSE +0 -0
  73. {qubx-1.1.1.dev3 → qubx-1.1.2}/README.md +0 -0
  74. {qubx-1.1.1.dev3 → qubx-1.1.2}/pyproject.toml +0 -0
  75. {qubx-1.1.1.dev3 → qubx-1.1.2}/scripts/build.py +0 -0
  76. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/_nb_magic.py +0 -0
  77. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/__init__.py +0 -0
  78. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/account.py +0 -0
  79. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/broker.py +0 -0
  80. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/iteratedstream.py +0 -0
  81. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/ome.py +0 -0
  82. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/optimization.py +0 -0
  83. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/sentinels.py +0 -0
  84. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/simulated_exchange.py +0 -0
  85. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/simulator.py +0 -0
  86. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/backtester/transfers.py +0 -0
  87. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/__init__.py +0 -0
  88. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/deploy.py +0 -0
  89. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/misc.py +0 -0
  90. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/resolver.py +0 -0
  91. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/s3.py +0 -0
  92. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/theme.py +0 -0
  93. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/cli/user_config.py +0 -0
  94. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/config.py +0 -0
  95. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/__init__.py +0 -0
  96. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/__init__.py +0 -0
  97. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  98. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  99. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  100. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  101. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  102. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  103. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  104. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  105. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  106. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  107. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  108. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  109. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  110. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  111. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  112. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  113. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  114. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  115. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
  116. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  117. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/factory.py +0 -0
  118. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  119. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  120. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  121. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  122. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  123. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  124. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  125. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  126. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  127. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  128. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  129. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  130. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/registry.py +0 -0
  131. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/connectors/tardis/utils.py +0 -0
  132. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/__init__.py +0 -0
  133. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/account.py +0 -0
  134. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/context.py +0 -0
  135. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/detectors/__init__.py +0 -0
  136. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/errors.py +0 -0
  137. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/initializer.py +0 -0
  138. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/interfaces.py +0 -0
  139. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/mixins/__init__.py +0 -0
  140. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/mixins/processing.py +0 -0
  141. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/mixins/subscription.py +0 -0
  142. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/mixins/utils.py +0 -0
  143. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/series.pxd +0 -0
  144. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/series.pyi +0 -0
  145. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/series.pyx +0 -0
  146. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/utils.pyi +0 -0
  147. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/core/utils.pyx +0 -0
  148. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/__init__.py +0 -0
  149. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/containers.py +0 -0
  150. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/registry.py +0 -0
  151. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/storage.py +0 -0
  152. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/storages/csv.py +0 -0
  153. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/storages/handy.py +0 -0
  154. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/data/storages/stub.py +0 -0
  155. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/emitters/__init__.py +0 -0
  156. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/emitters/composite.py +0 -0
  157. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/emitters/csv.py +0 -0
  158. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/__init__.py +0 -0
  159. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/composite.py +0 -0
  160. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/formatters/__init__.py +0 -0
  161. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/formatters/incremental.py +0 -0
  162. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/formatters/slack.py +0 -0
  163. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/formatters/target_position.py +0 -0
  164. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/redis_streams.py +0 -0
  165. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/exporters/slack.py +0 -0
  166. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/gathering/simplest.py +0 -0
  167. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/health/__init__.py +0 -0
  168. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/health/base.py +0 -0
  169. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/health/dummy.py +0 -0
  170. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/health/server.py +0 -0
  171. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/loggers/csv.py +0 -0
  172. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/loggers/inmemory.py +0 -0
  173. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/loggers/mongo.py +0 -0
  174. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/notifications/__init__.py +0 -0
  175. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/notifications/composite.py +0 -0
  176. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/notifications/slack.py +0 -0
  177. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/notifications/throttler.py +0 -0
  178. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/pandaz/__init__.py +0 -0
  179. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/pandaz/stats.py +0 -0
  180. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/pandaz/utils.py +0 -0
  181. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/plugins/__init__.py +0 -0
  182. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/plugins/loader.py +0 -0
  183. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/_build.py +0 -0
  184. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/crypto-fees.ini +0 -0
  185. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  186. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  187. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  188. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  189. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  190. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  191. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  192. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  193. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  194. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  195. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  196. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restarts/__init__.py +0 -0
  197. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restarts/state_resolvers.py +0 -0
  198. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restorers/__init__.py +0 -0
  199. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restorers/interfaces.py +0 -0
  200. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/restorers/utils.py +0 -0
  201. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/state/__init__.py +0 -0
  202. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/state/dummy.py +0 -0
  203. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/state/redis.py +0 -0
  204. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/ta/__init__.py +0 -0
  205. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/ta/indicators.pxd +0 -0
  206. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/ta/indicators.pyi +0 -0
  207. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/ta/indicators.pyx +0 -0
  208. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/__init__.py +0 -0
  209. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/base.py +0 -0
  210. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  211. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/project/config.yml.j2 +0 -0
  212. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  213. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  214. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  215. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  216. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  217. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/project/template.yml +0 -0
  218. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  219. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  220. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  221. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  222. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  223. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  224. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.python-version +0 -0
  225. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  226. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  227. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  228. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  229. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/README.md.j2 +0 -0
  230. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  231. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/justfile.j2 +0 -0
  232. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  233. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/research/.gitkeep +0 -0
  234. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  235. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  236. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  237. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/template.yml +0 -0
  238. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  239. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  240. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  241. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/simple/config.yml.j2 +0 -0
  242. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  243. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  244. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  245. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/templates/simple/template.yml +0 -0
  246. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/trackers/__init__.py +0 -0
  247. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/trackers/composite.py +0 -0
  248. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/trackers/rebalancers.py +0 -0
  249. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/trackers/riskctrl.py +0 -0
  250. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/__init__.py +0 -0
  251. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/_pyxreloader.py +0 -0
  252. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/charting/orderbook.py +0 -0
  253. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/collections.py +0 -0
  254. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/hft/__init__.py +0 -0
  255. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/hft/numba_utils.py +0 -0
  256. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/hft/orderbook.pyi +0 -0
  257. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/hft/orderbook.pyx +0 -0
  258. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/marketdata/binance.py +0 -0
  259. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/marketdata/ccxt.py +0 -0
  260. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/marketdata/dukas.py +0 -0
  261. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/nonce.py +0 -0
  262. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/ntp.py +0 -0
  263. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/numbers_utils.py +0 -0
  264. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/orderbook.py +0 -0
  265. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/plotting/__init__.py +0 -0
  266. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/plotting/dashboard.py +0 -0
  267. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/plotting/data.py +0 -0
  268. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/plotting/interfaces.py +0 -0
  269. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  270. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  271. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/questdb.py +0 -0
  272. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/rate_limiter.py +0 -0
  273. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/ringbuffer.pxd +0 -0
  274. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/ringbuffer.pyi +0 -0
  275. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/ringbuffer.pyx +0 -0
  276. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/__init__.py +0 -0
  277. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  278. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/accounts.py +0 -0
  279. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/factory.py +0 -0
  280. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/kernel_service.py +0 -0
  281. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/__init__.py +0 -0
  282. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/kernel.py +0 -0
  283. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  284. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  285. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  286. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  287. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  288. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  289. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/s3.py +0 -0
  290. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/slack.py +0 -0
  291. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/throttler.py +0 -0
  292. {qubx-1.1.1.dev3 → qubx-1.1.2}/src/qubx/utils/websocket_manager.py +0 -0
  293. {qubx-1.1.1.dev3 → qubx-1.1.2}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  294. {qubx-1.1.1.dev3 → qubx-1.1.2}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  295. {qubx-1.1.1.dev3 → qubx-1.1.2}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  296. {qubx-1.1.1.dev3 → qubx-1.1.2}/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.1.1.dev3
3
+ Version: 1.1.2
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
@@ -47,6 +47,16 @@ def format_platform_identity(record) -> str:
47
47
  return ""
48
48
 
49
49
 
50
+ def format_phase(record) -> str:
51
+ """Return a colored phase tag from record extras."""
52
+ phase = record["extra"].get("phase")
53
+ if phase == "warmup":
54
+ return "<yellow>[WARMUP]</yellow> "
55
+ elif phase == "live":
56
+ return "<green>[LIVE]</green> "
57
+ return ""
58
+
59
+
50
60
  def formatter(record):
51
61
  end = record["extra"].get("end", "\n")
52
62
  fmt = "<lvl>{message}</lvl>%s" % end
@@ -54,9 +64,10 @@ def formatter(record):
54
64
  fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
55
65
 
56
66
  identity = format_platform_identity(record)
67
+ phase = format_phase(record)
57
68
  prefix = (
58
- "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] %s<cyan>({module})</cyan> "
59
- % (record["level"].icon, identity)
69
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] %s%s<cyan>({module})</cyan> "
70
+ % (record["level"].icon, identity, phase)
60
71
  )
61
72
 
62
73
  if record["exception"] is not None:
@@ -158,17 +169,36 @@ class QubxLogConfig:
158
169
  )
159
170
  logger = logger.opt(colors=colorize)
160
171
 
172
+ _bot_id: str | None = None
173
+ _instance_id: str | None = None
174
+ _phase: str | None = None
175
+
161
176
  @staticmethod
162
- def bind_platform_identity(bot_id: str | None = None, instance_id: str | None = None):
163
- """Bind platform identity fields (bot_id, instance_id) to all log messages globally."""
177
+ def _update_patcher():
178
+ """Reconfigure loguru with a single patcher that applies all bound fields."""
164
179
  def patcher(record):
165
- if bot_id:
166
- record["extra"]["bot_id"] = bot_id
167
- if instance_id:
168
- record["extra"]["instance_id"] = instance_id
180
+ if QubxLogConfig._bot_id:
181
+ record["extra"]["bot_id"] = QubxLogConfig._bot_id
182
+ if QubxLogConfig._instance_id:
183
+ record["extra"]["instance_id"] = QubxLogConfig._instance_id
184
+ if QubxLogConfig._phase:
185
+ record["extra"]["phase"] = QubxLogConfig._phase
169
186
 
170
187
  logger.configure(patcher=patcher)
171
188
 
189
+ @staticmethod
190
+ def bind_platform_identity(bot_id: str | None = None, instance_id: str | None = None):
191
+ """Bind platform identity fields (bot_id, instance_id) to all log messages globally."""
192
+ QubxLogConfig._bot_id = bot_id
193
+ QubxLogConfig._instance_id = instance_id
194
+ QubxLogConfig._update_patcher()
195
+
196
+ @staticmethod
197
+ def bind_phase(phase: str | None):
198
+ """Bind phase (warmup/live) to all log messages globally."""
199
+ QubxLogConfig._phase = phase
200
+ QubxLogConfig._update_patcher()
201
+
172
202
 
173
203
  QubxLogConfig.setup_logger()
174
204
 
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.1.2'
22
+ __version_tuple__ = version_tuple = (1, 1, 2)
23
+
24
+ __commit_id__ = commit_id = None
@@ -1,8 +1,6 @@
1
1
  from collections import defaultdict
2
2
  from typing import TypeVar
3
3
 
4
- import pandas as pd
5
-
6
4
  from qubx import logger
7
5
  from qubx.backtester.simulated_data import SimulatedDataIterator
8
6
  from qubx.core.basics import (
@@ -12,6 +10,7 @@ from qubx.core.basics import (
12
10
  )
13
11
  from qubx.core.interfaces import IDataProvider
14
12
  from qubx.core.series import Bar, Quote
13
+ from qubx.utils.time import to_timedelta, to_timestamp
15
14
 
16
15
  from .account import SimulatedAccountProcessor
17
16
  from .utils import SimulatedTimeProvider
@@ -144,8 +143,8 @@ class SimulatedDataProvider(IDataProvider):
144
143
  self._data_source.set_warmup_period(si[0], warm_period)
145
144
 
146
145
  def get_ohlc(self, instrument: Instrument, timeframe: str, nbarsback: int) -> list[Bar]:
147
- start = pd.Timestamp(self.time_provider.time())
148
- end = start - nbarsback * pd.Timedelta(timeframe)
146
+ start = to_timestamp(self.time_provider.time())
147
+ end = start - nbarsback * to_timedelta(timeframe)
149
148
  return self._data_source.get_ohlc(instrument, timeframe, start, end)
150
149
 
151
150
  def get_quote(self, instrument: Instrument) -> Quote | None:
@@ -573,7 +573,7 @@ class BacktestStorage:
573
573
  if self._is_cloud:
574
574
  self._setup_cloud_duckdb()
575
575
 
576
- def get_log(self, backtest_id: str, config_name: str | None = None) -> str | None:
576
+ def get_log(self, backtest_id: str, config_name: str | None = None, max_lines: int = 1000) -> tuple[str, bool] | None:
577
577
  """
578
578
  Read the log file for a backtest run.
579
579
 
@@ -581,14 +581,29 @@ class BacktestStorage:
581
581
  backtest_id: Relative path within base_path.
582
582
  config_name: Config name (used for log filename).
583
583
  If None, searches for any .log file.
584
+ max_lines: Maximum number of lines to return (from the end). 0 = unlimited.
584
585
 
585
586
  Returns:
586
- Log file content as string, or None if not found.
587
+ (log_content, truncated) tuple, or None if not found.
587
588
  """
588
589
  run_dir = f"{self.base_path}{backtest_id.strip('/')}/"
589
590
 
590
591
  if config_name:
591
592
  log_path = f"{run_dir}{config_name}.log"
593
+ elif self._is_cloud:
594
+ try:
595
+ from pyarrow.fs import FileSelector, FileType
596
+
597
+ from qubx.utils.s3 import strip_scheme
598
+
599
+ client = S3Client(storage_options=self._storage_options)
600
+ entries = client.fs.get_file_info(FileSelector(strip_scheme(run_dir)))
601
+ log_files = [e.path for e in entries if e.type == FileType.File and e.path.endswith(".log")]
602
+ if not log_files:
603
+ return None
604
+ log_path = f"s3://{log_files[0]}"
605
+ except Exception:
606
+ return None
592
607
  else:
593
608
  try:
594
609
  files_df = self._conn.execute(f"SELECT file FROM glob('{run_dir}*.log')").df()
@@ -598,20 +613,30 @@ class BacktestStorage:
598
613
  except Exception:
599
614
  return None
600
615
 
616
+ def _tail(content: str) -> tuple[str, bool]:
617
+ if not max_lines:
618
+ return content, False
619
+ lines = content.splitlines()
620
+ if len(lines) <= max_lines:
621
+ return content, False
622
+ return "\n".join(lines[-max_lines:]), True
623
+
601
624
  if self._is_cloud:
602
625
  try:
603
626
  from qubx.utils.s3 import strip_scheme
604
627
 
605
628
  client = S3Client(storage_options=self._storage_options)
606
629
  with client.fs.open_input_stream(strip_scheme(log_path)) as f:
607
- return f.read().decode("utf-8")
630
+ return _tail(f.read().decode("utf-8"))
608
631
  except Exception:
609
632
  return None
610
633
  else:
611
634
  from pathlib import Path
612
635
 
613
636
  p = Path(log_path)
614
- return p.read_text(encoding="utf-8", errors="replace") if p.is_file() else None
637
+ if not p.is_file():
638
+ return None
639
+ return _tail(p.read_text(encoding="utf-8", errors="replace"))
615
640
 
616
641
  def export_backtests_to_markdown(self, backtest_id: str, path: str, tags: tuple[str] | None = None):
617
642
  if tsr := self.load(backtest_id):
@@ -34,7 +34,7 @@ from qubx.health import DummyHealthMonitor
34
34
  from qubx.loggers.inmemory import InMemoryLogsWriter
35
35
  from qubx.pandaz.utils import _frame_to_str
36
36
  from qubx.utils.runner.configs import PrefetchConfig
37
- from qubx.utils.time import now_ns
37
+ from qubx.utils.time import now_ns, to_timedelta
38
38
 
39
39
  from .account import SimulatedAccountProcessor
40
40
  from .broker import SimulatedBroker
@@ -131,6 +131,11 @@ class SimulationRunner:
131
131
  self._create_backtest_context()
132
132
  self._handle_ctx_subscriptions()
133
133
 
134
+ @property
135
+ def data_time_range(self) -> tuple[int, int] | None:
136
+ """Time range (ns) of historical data actually processed: (first, last), or None if no data arrived."""
137
+ return self._simulated_data_source.data_time_range
138
+
134
139
  def run(self, silent: bool = False, catch_keyboard_interrupt: bool = True, close_data_readers: bool = False):
135
140
  """
136
141
  Run the backtest from start to stop.
@@ -437,7 +442,7 @@ class SimulationRunner:
437
442
  # - TimeGuard is applied by callers that need it (aux/strategy-facing access).
438
443
  # - Main simulation data (DataPump) must NOT be time-guarded: DataPump provides
439
444
  # its own explicit [start, end] window and needs to read the full range upfront.
440
- _prefetch_period = str(min(self.stop - self.start, pd.Timedelta(prefetch_cfg.prefetch_period)))
445
+ _prefetch_period = str(min(self.stop - self.start, to_timedelta(prefetch_cfg.prefetch_period)))
441
446
  return CachedStorage(
442
447
  storage, prefetch_period=_prefetch_period, cache_factory=lambda: MemoryCache(prefetch_cfg.cache_size_mb)
443
448
  )
@@ -611,10 +616,8 @@ class SimulationRunner:
611
616
 
612
617
  _account_processors = {}
613
618
  for exchange in self.setup.exchanges:
614
- _initial_capital = self.setup.capital
615
- if isinstance(_initial_capital, dict):
616
- _initial_capital = _initial_capital[exchange]
617
- assert isinstance(_initial_capital, (float, int))
619
+ assert isinstance(self.setup.capital, dict)
620
+ _initial_capital = self.setup.capital[exchange]
618
621
  _account_processors[exchange] = SimulatedAccountProcessor(
619
622
  account_id=self.account_id,
620
623
  exchange=_exchange_to_simulated_exchange[exchange],
@@ -16,7 +16,7 @@ from qubx.data.containers import IRawContainer, RawData, RawMultiData
16
16
  from qubx.data.storage import IDataTransformer, IReader, IStorage, Transformable
17
17
  from qubx.data.storages.utils import find_column_index_in_list
18
18
  from qubx.data.transformers import TypedRecords
19
- from qubx.utils.time import convert_times_to_ns, infer_series_frequency
19
+ from qubx.utils.time import convert_times_to_ns, infer_series_frequency, to_timedelta, to_timestamp
20
20
 
21
21
 
22
22
  class EmulatedUpdatesFromOHLC(IDataTransformer):
@@ -26,7 +26,7 @@ class EmulatedUpdatesFromOHLC(IDataTransformer):
26
26
 
27
27
  @staticmethod
28
28
  def timedelta_to_numpy(x: str) -> int:
29
- return pd.Timedelta(x).to_numpy().item()
29
+ return to_timedelta(x).to_numpy().item()
30
30
 
31
31
  D1, H1 = timedelta_to_numpy("1D"), timedelta_to_numpy("1h")
32
32
  MS1 = 1_000_000
@@ -329,9 +329,9 @@ class RawSymbolBuffer:
329
329
  if isinstance(value, pd.Timestamp):
330
330
  return value.value
331
331
  if isinstance(value, np.datetime64):
332
- return pd.Timestamp(value).value
332
+ return to_timestamp(value).value
333
333
  # - fallback: try to convert via pd.Timestamp
334
- return pd.Timestamp(value).value
334
+ return to_timestamp(value).value
335
335
 
336
336
  def activate(self):
337
337
  self._active = True
@@ -367,10 +367,10 @@ class RawSymbolBuffer:
367
367
  # - dedup: filter out rows already in buffer using native pyarrow comparison
368
368
  if self._watermark > 0:
369
369
  if pa.types.is_timestamp(time_col.type):
370
- wm_scalar = pa.scalar(pd.Timestamp(self._watermark, unit="ns"), type=time_col.type)
370
+ wm_scalar = pa.scalar(to_timestamp(self._watermark, unit="ns"), type=time_col.type)
371
371
  elif pa.types.is_date(time_col.type):
372
372
  # - date32 stores days since epoch; watermark is ns, convert to datetime.date
373
- wm_scalar = pa.scalar(pd.Timestamp(self._watermark, unit="ns").date(), type=time_col.type)
373
+ wm_scalar = pa.scalar(to_timestamp(self._watermark, unit="ns").date(), type=time_col.type)
374
374
  else:
375
375
  wm_scalar = pa.scalar(self._watermark, type=time_col.type)
376
376
  mask = pa.compute.greater(time_col, wm_scalar)
@@ -398,7 +398,7 @@ class RawSymbolBuffer:
398
398
  return len(self._batches)
399
399
 
400
400
  def __repr__(self) -> str:
401
- _wm = pd.Timestamp(self._watermark, unit="ns") if self._watermark else "N/A"
401
+ _wm = to_timestamp(self._watermark, unit="ns") if self._watermark else "N/A"
402
402
  return f"RawSymbolBuffer({self._symbol}, chunks={len(self)}, wm={_wm}, active={self._active})"
403
403
 
404
404
 
@@ -644,13 +644,13 @@ class DataPump:
644
644
  Starts backend reader, creates MemReaders for all symbols.
645
645
  Returns dict suitable for slicer.put().
646
646
  """
647
- self._end = pd.Timestamp(end)
647
+ self._end = to_timestamp(end)
648
648
  symbols = self._active_symbols()
649
649
  if not symbols:
650
650
  return {}
651
651
 
652
652
  # - all symbols need warmup on first read
653
- read_start = pd.Timestamp(start)
653
+ read_start = to_timestamp(start)
654
654
  if self._warmup_period:
655
655
  read_start = read_start - self._warmup_period
656
656
  self._warmed.update(symbols)
@@ -686,7 +686,7 @@ class DataPump:
686
686
  dict of NEW MemReaders only (for slicer.put())
687
687
  """
688
688
  if end is not None:
689
- self._end = pd.Timestamp(end)
689
+ self._end = to_timestamp(end)
690
690
 
691
691
  symbols = self._active_symbols()
692
692
  if not symbols:
@@ -695,7 +695,7 @@ class DataPump:
695
695
 
696
696
  # - determine start time: extend into past if new symbols need warmup
697
697
  new_symbols = [s for s in symbols if s not in self._warmed]
698
- read_start = pd.Timestamp(current_time)
698
+ read_start = to_timestamp(current_time)
699
699
 
700
700
  if new_symbols and self._warmup_period:
701
701
  read_start = read_start - self._warmup_period
@@ -846,9 +846,17 @@ class SimulatedDataIterator(Iterator):
846
846
  self._slicer = None
847
847
  self._slicing_iterator = None
848
848
  self._current_time = None
849
+ self._first_data_time: int | None = None
849
850
  self._start = None
850
851
  self._stop = None
851
852
 
853
+ @property
854
+ def data_time_range(self) -> tuple[int, int] | None:
855
+ """Time range (ns) of historical data actually processed: (first, last), or None if no data arrived."""
856
+ if self._first_data_time is not None and self._current_time is not None:
857
+ return (self._first_data_time, self._current_time)
858
+ return None
859
+
852
860
  @property
853
861
  def emulation_time_indent_seconds(self) -> float:
854
862
  """
@@ -876,7 +884,7 @@ class SimulatedDataIterator(Iterator):
876
884
  def set_warmup_period(self, subscription: str, warmup_period: str | None = None):
877
885
  if warmup_period:
878
886
  access_key, _, _ = self._parse_subscription_spec(subscription)
879
- self._warmups[access_key] = pd.Timedelta(warmup_period)
887
+ self._warmups[access_key] = to_timedelta(warmup_period)
880
888
 
881
889
  def _parse_subscription_spec(self, subscription: str) -> tuple[str, str, dict[str, object]]:
882
890
  _subtype, _params = DataType.from_str(subscription)
@@ -999,7 +1007,7 @@ class SimulatedDataIterator(Iterator):
999
1007
 
1000
1008
  # - if simulation is running, restart read to include new symbols
1001
1009
  if self.is_running and new_instruments:
1002
- new_mem_readers = pump.restart_read(pd.Timestamp(self._current_time, unit="ns"), self._stop) # type: ignore
1010
+ new_mem_readers = pump.restart_read(to_timestamp(self._current_time, unit="ns"), self._stop) # type: ignore
1003
1011
  if new_mem_readers and self._slicer is not None:
1004
1012
  self._slicer.put(new_mem_readers)
1005
1013
 
@@ -1101,16 +1109,17 @@ class SimulatedDataIterator(Iterator):
1101
1109
  return self._current_time is not None
1102
1110
 
1103
1111
  def create_iterable(self, start: str | pd.Timestamp, stop: str | pd.Timestamp) -> Iterator:
1104
- self._start = pd.Timestamp(start)
1105
- self._stop = pd.Timestamp(stop)
1112
+ self._start = to_timestamp(start)
1113
+ self._stop = to_timestamp(stop)
1106
1114
  self._current_time = None
1115
+ self._first_data_time = None
1107
1116
  self._slicer = IteratedDataStreamsSlicer()
1108
1117
  return self
1109
1118
 
1110
1119
  def __iter__(self) -> Iterator:
1111
1120
  assert self._start is not None and self._stop is not None
1112
1121
  assert self._slicer is not None
1113
- self._current_time = int(pd.Timestamp(self._start).timestamp() * 1e9)
1122
+ self._current_time = int(to_timestamp(self._start).timestamp() * 1e9)
1114
1123
 
1115
1124
  # - initial read for all pumps
1116
1125
  for pump in self._pumps.values():
@@ -1137,6 +1146,8 @@ class SimulatedDataIterator(Iterator):
1137
1146
  if t < self._current_time:
1138
1147
  is_historical = True
1139
1148
  else:
1149
+ if self._first_data_time is None:
1150
+ self._first_data_time = t
1140
1151
  self._current_time = t
1141
1152
 
1142
1153
  return instr, data_type, v, is_historical
@@ -1160,7 +1171,7 @@ class SimulatedDataIterator(Iterator):
1160
1171
  assert isinstance(t_records, Transformable)
1161
1172
 
1162
1173
  return self._process_bar_records(
1163
- t_records.transform(TypedRecords()), time_as_nsec(start), pd.Timedelta(timeframe).asm8.item()
1174
+ t_records.transform(TypedRecords()), time_as_nsec(start), to_timedelta(timeframe).asm8.item()
1164
1175
  )
1165
1176
 
1166
1177
  def _process_bar_records(self, records: list[Bar], cut_time_ns: int, timeframe_ns: int) -> list[Bar]:
@@ -25,6 +25,7 @@ from qubx.core.utils import time_delta_to_str
25
25
  from qubx.data.storage import IStorage
26
26
  from qubx.utils.misc import safe_dtype_timeframe
27
27
  from qubx.utils.runner.configs import PrefetchConfig
28
+ from qubx.utils.time import to_timedelta
28
29
 
29
30
  SymbolOrInstrument_t: TypeAlias = str | Instrument
30
31
  ExchangeName_t: TypeAlias = str
@@ -45,7 +46,7 @@ StrategiesDecls_t: TypeAlias = (
45
46
 
46
47
 
47
48
  def _timedelta_to_numpy(x: str | int) -> int:
48
- return pd.Timedelta(x).to_numpy().item()
49
+ return to_timedelta(x).to_numpy().item()
49
50
 
50
51
 
51
52
  DEFAULT_DAILY_SESSION = (_timedelta_to_numpy("00:00:00.100"), _timedelta_to_numpy("23:59:59.900"))
@@ -98,6 +99,13 @@ class SimulationSetup:
98
99
  accurate_stop_orders_execution: bool = False
99
100
  enable_funding: bool = False
100
101
 
102
+ def __post_init__(self) -> None:
103
+ # Normalize capital to a per-exchange dict: split evenly when a single float is given
104
+ if isinstance(self.capital, (int, float)):
105
+ n = len(self.exchanges)
106
+ per_exchange = float(self.capital) / n if n > 0 else float(self.capital)
107
+ self.capital = {exchange: per_exchange for exchange in self.exchanges}
108
+
101
109
  def __str__(self) -> str:
102
110
  return f"{self.name} {self.setup_type} capital {self.capital} {self.base_currency} for [{','.join(map(lambda x: x.symbol, self.instruments))}] @ {self.exchanges}[{self.commissions}]"
103
111
 
@@ -133,10 +141,11 @@ class SimulatedLogFormatter:
133
141
  else:
134
142
  now = self.time_provider.time().astype("datetime64[us]").item().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
135
143
 
136
- from qubx import format_platform_identity
144
+ from qubx import format_phase, format_platform_identity
137
145
 
138
146
  identity = format_platform_identity(record)
139
- prefix = f"<lc>{now}</lc> [<level>{record['level'].icon}</level>] {identity}<cyan>({{module}})</cyan> "
147
+ phase = format_phase(record)
148
+ prefix = f"<lc>{now}</lc> [<level>{record['level'].icon}</level>] {identity}{phase}<cyan>({{module}})</cyan> "
140
149
 
141
150
  if record["exception"] is not None:
142
151
  record["extra"]["stack"] = stackprinter.format(record["exception"], style="darkbg3")
@@ -521,15 +530,15 @@ def _adjust_open_close_time_indent_secs(timeframe: pd.Timedelta | None, original
521
530
  return original_indent_secs
522
531
 
523
532
  # - if it triggers at daily+ bar let's assume this bar is 'closed' 5 min before exact closing time
524
- if timeframe >= pd.Timedelta("1d"):
533
+ if timeframe >= to_timedelta("1d"):
525
534
  return max(original_indent_secs, 5 * 60)
526
535
 
527
536
  # - if it triggers at 1Min+ bar let's assume this bar is 'closed' 5 sec before exact closing time
528
- if timeframe >= pd.Timedelta("1min"):
537
+ if timeframe >= to_timedelta("1min"):
529
538
  return max(original_indent_secs, 5)
530
539
 
531
540
  # - for all sub-minute timeframes just use 1 sec shift
532
- if timeframe > pd.Timedelta("1s"):
541
+ if timeframe > to_timedelta("1s"):
533
542
  return max(original_indent_secs, 1)
534
543
 
535
544
  # - for rest just keep original indent
@@ -538,9 +547,9 @@ def _adjust_open_close_time_indent_secs(timeframe: pd.Timedelta | None, original
538
547
 
539
548
  def _get_default_warmup_period(base_subscription: str, in_timeframe: pd.Timedelta | None) -> pd.Timedelta:
540
549
  if in_timeframe is None or base_subscription in [DataType.QUOTE, DataType.TRADE, DataType.ORDERBOOK]:
541
- return pd.Timedelta("1Min")
550
+ return to_timedelta("1Min")
542
551
 
543
- if in_timeframe < pd.Timedelta("1h"):
552
+ if in_timeframe < to_timedelta("1h"):
544
553
  return 5 * in_timeframe
545
554
 
546
555
  return 2 * in_timeframe
@@ -230,15 +230,6 @@ def run(
230
230
  @click.option(
231
231
  "--report", "-r", default=None, type=str, help="Output directory for simulation reports.", show_default=True
232
232
  )
233
- @click.option(
234
- "--log",
235
- "-L",
236
- "log_to_file",
237
- is_flag=True,
238
- default=False,
239
- help="Write simulation logs to a file in the output directory.",
240
- show_default=True,
241
- )
242
233
  @click.option(
243
234
  "--name",
244
235
  "-n",
@@ -261,7 +252,6 @@ def simulate(
261
252
  end: str | None,
262
253
  output: str | None,
263
254
  report: str | None,
264
- log_to_file: bool,
265
255
  name: str | None,
266
256
  log_file: str | None,
267
257
  ):
@@ -276,7 +266,7 @@ def simulate(
276
266
  logo()
277
267
  logger.info(f"Process PID: <g>{os.getpid()}</g>")
278
268
  simulate_strategy(
279
- config_file, output, start, end, report, log_to_file, name=name, log_file=log_file,
269
+ config_file, output, start, end, report, name=name, log_file=log_file,
280
270
  )
281
271
 
282
272