Qubx 1.0.0.dev2__tar.gz → 1.0.0.dev4__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 (267) hide show
  1. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/PKG-INFO +4 -1
  2. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/pyproject.toml +4 -0
  3. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/__init__.py +7 -1
  4. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/_version.py +2 -2
  5. qubx-1.0.0.dev4/src/qubx/backtester/__init__.py +4 -0
  6. qubx-1.0.0.dev4/src/qubx/backtester/management.py +506 -0
  7. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/runner.py +56 -18
  8. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/simulator.py +23 -0
  9. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/cli/commands.py +100 -13
  10. qubx-1.0.0.dev4/src/qubx/cli/theme.py +61 -0
  11. qubx-1.0.0.dev4/src/qubx/cli/tui.py +988 -0
  12. qubx-1.0.0.dev4/src/qubx/config.py +139 -0
  13. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/basics.py +18 -2
  14. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/context.py +7 -1
  15. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/detectors/stale.py +4 -61
  16. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/interfaces.py +13 -7
  17. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/lookups.py +16 -14
  18. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/metrics.py +129 -17
  19. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/mixins/market.py +40 -15
  20. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/mixins/processing.py +36 -21
  21. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/cache.py +60 -24
  22. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/storage.py +6 -0
  23. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/storages/ccxt.py +169 -130
  24. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/storages/multi.py +10 -0
  25. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/storages/questdb.py +40 -1
  26. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/misc.py +1 -1
  27. qubx-1.0.0.dev4/src/qubx/utils/results.py +1029 -0
  28. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/runner.py +103 -39
  29. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/app.py +6 -1
  30. qubx-1.0.0.dev4/src/qubx/utils/runner/textual/styles.tcss +196 -0
  31. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/time.py +31 -0
  32. qubx-1.0.0.dev2/src/qubx/backtester/__init__.py +0 -5
  33. qubx-1.0.0.dev2/src/qubx/backtester/management.py +0 -522
  34. qubx-1.0.0.dev2/src/qubx/cli/tui.py +0 -458
  35. qubx-1.0.0.dev2/src/qubx/utils/runner/textual/styles.tcss +0 -134
  36. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/.gitignore +0 -0
  37. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/LICENSE +0 -0
  38. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/README.md +0 -0
  39. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/hatch_build.py +0 -0
  40. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/_nb_magic.py +0 -0
  41. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/account.py +0 -0
  42. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/broker.py +0 -0
  43. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/data.py +0 -0
  44. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/iteratedstream.py +0 -0
  45. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/ome.py +0 -0
  46. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/optimization.py +0 -0
  47. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/sentinels.py +0 -0
  48. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/simulated_data.py +0 -0
  49. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/simulated_exchange.py +0 -0
  50. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/transfers.py +0 -0
  51. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/backtester/utils.py +0 -0
  52. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/cli/__init__.py +0 -0
  53. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/cli/deploy.py +0 -0
  54. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/cli/misc.py +0 -0
  55. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/cli/release.py +0 -0
  56. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/__init__.py +0 -0
  57. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/__init__.py +0 -0
  58. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/account.py +0 -0
  59. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  60. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  61. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/broker.py +0 -0
  62. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  63. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/data.py +0 -0
  64. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  65. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  66. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  67. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  68. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  69. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  70. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  71. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  72. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  73. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  74. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  75. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  76. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  77. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  78. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  79. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/factory.py +0 -0
  80. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  81. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  82. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  83. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  84. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  85. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  86. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  87. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  88. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  89. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  90. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  91. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  92. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  93. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/utils.py +0 -0
  94. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  95. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/registry.py +0 -0
  96. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/tardis/data.py +0 -0
  97. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/connectors/tardis/utils.py +0 -0
  98. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/__init__.py +0 -0
  99. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/account.py +0 -0
  100. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/detectors/__init__.py +0 -0
  101. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/detectors/delisting.py +0 -0
  102. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/errors.py +0 -0
  103. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/exceptions.py +0 -0
  104. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/helpers.py +0 -0
  105. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/initializer.py +0 -0
  106. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/loggers.py +0 -0
  107. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/mixins/__init__.py +0 -0
  108. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/mixins/subscription.py +0 -0
  109. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/mixins/trading.py +0 -0
  110. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/mixins/universe.py +0 -0
  111. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/mixins/utils.py +0 -0
  112. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/series.pxd +0 -0
  113. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/series.pyi +0 -0
  114. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/series.pyx +0 -0
  115. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/utils.pyi +0 -0
  116. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/core/utils.pyx +0 -0
  117. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/__init__.py +0 -0
  118. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/containers.py +0 -0
  119. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/guards.py +0 -0
  120. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/registry.py +0 -0
  121. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/storages/csv.py +0 -0
  122. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/storages/handy.py +0 -0
  123. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/storages/stub.py +0 -0
  124. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/storages/utils.py +0 -0
  125. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/data/transformers.py +0 -0
  126. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/emitters/__init__.py +0 -0
  127. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/emitters/base.py +0 -0
  128. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/emitters/composite.py +0 -0
  129. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/emitters/csv.py +0 -0
  130. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/emitters/indicator.py +0 -0
  131. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/emitters/inmemory.py +0 -0
  132. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/emitters/prometheus.py +0 -0
  133. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/emitters/questdb.py +0 -0
  134. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/__init__.py +0 -0
  135. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/composite.py +0 -0
  136. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/formatters/__init__.py +0 -0
  137. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/formatters/base.py +0 -0
  138. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/formatters/incremental.py +0 -0
  139. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/formatters/slack.py +0 -0
  140. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/formatters/target_position.py +0 -0
  141. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/redis_streams.py +0 -0
  142. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/exporters/slack.py +0 -0
  143. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/gathering/simplest.py +0 -0
  144. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/health/__init__.py +0 -0
  145. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/health/base.py +0 -0
  146. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/health/dummy.py +0 -0
  147. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/loggers/__init__.py +0 -0
  148. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/loggers/csv.py +0 -0
  149. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/loggers/factory.py +0 -0
  150. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/loggers/inmemory.py +0 -0
  151. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/loggers/mongo.py +0 -0
  152. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/notifications/__init__.py +0 -0
  153. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/notifications/composite.py +0 -0
  154. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/notifications/slack.py +0 -0
  155. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/notifications/throttler.py +0 -0
  156. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/pandaz/__init__.py +0 -0
  157. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/pandaz/stats.py +0 -0
  158. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/pandaz/ta.py +0 -0
  159. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/pandaz/utils.py +0 -0
  160. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/plugins/__init__.py +0 -0
  161. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/plugins/loader.py +0 -0
  162. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/_build.py +0 -0
  163. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/crypto-fees.ini +0 -0
  164. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  165. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  166. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  167. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  168. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  169. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  170. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  171. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  172. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  173. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  174. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  175. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restarts/__init__.py +0 -0
  176. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restarts/state_resolvers.py +0 -0
  177. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restarts/time_finders.py +0 -0
  178. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restorers/__init__.py +0 -0
  179. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restorers/balance.py +0 -0
  180. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restorers/factory.py +0 -0
  181. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restorers/interfaces.py +0 -0
  182. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restorers/position.py +0 -0
  183. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restorers/signal.py +0 -0
  184. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restorers/state.py +0 -0
  185. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/restorers/utils.py +0 -0
  186. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/state/__init__.py +0 -0
  187. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/state/dummy.py +0 -0
  188. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/state/redis.py +0 -0
  189. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/ta/__init__.py +0 -0
  190. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/ta/indicators.pxd +0 -0
  191. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/ta/indicators.pyi +0 -0
  192. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/ta/indicators.pyx +0 -0
  193. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/__init__.py +0 -0
  194. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/base.py +0 -0
  195. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  196. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/project/config.yml.j2 +0 -0
  197. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  198. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  199. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  200. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  201. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  202. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/project/template.yml +0 -0
  203. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  204. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  205. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/simple/config.yml.j2 +0 -0
  206. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  207. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  208. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  209. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/templates/simple/template.yml +0 -0
  210. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/trackers/__init__.py +0 -0
  211. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/trackers/advanced.py +0 -0
  212. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/trackers/composite.py +0 -0
  213. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/trackers/rebalancers.py +0 -0
  214. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/trackers/riskctrl.py +0 -0
  215. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/trackers/sizers.py +0 -0
  216. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/__init__.py +0 -0
  217. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/_pyxreloader.py +0 -0
  218. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/charting/lookinglass.py +0 -0
  219. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  220. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/charting/orderbook.py +0 -0
  221. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/collections.py +0 -0
  222. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/hft/__init__.py +0 -0
  223. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/hft/numba_utils.py +0 -0
  224. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/hft/orderbook.pyi +0 -0
  225. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/hft/orderbook.pyx +0 -0
  226. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/marketdata/binance.py +0 -0
  227. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/marketdata/ccxt.py +0 -0
  228. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/marketdata/dukas.py +0 -0
  229. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/nonce.py +0 -0
  230. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/ntp.py +0 -0
  231. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/numbers_utils.py +0 -0
  232. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/orderbook.py +0 -0
  233. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/plotting/__init__.py +0 -0
  234. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/plotting/dashboard.py +0 -0
  235. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/plotting/data.py +0 -0
  236. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/plotting/interfaces.py +0 -0
  237. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  238. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  239. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/questdb.py +0 -0
  240. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/rate_limiter.py +0 -0
  241. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/ringbuffer.pxd +0 -0
  242. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/ringbuffer.pyi +0 -0
  243. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/ringbuffer.pyx +0 -0
  244. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/__init__.py +0 -0
  245. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  246. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/accounts.py +0 -0
  247. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/configs.py +0 -0
  248. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/factory.py +0 -0
  249. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/kernel_service.py +0 -0
  250. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/__init__.py +0 -0
  251. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/handlers.py +0 -0
  252. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/init_code.py +0 -0
  253. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/kernel.py +0 -0
  254. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  255. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  256. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  257. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  258. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  259. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  260. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  261. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/slack.py +0 -0
  262. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/throttler.py +0 -0
  263. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/src/qubx/utils/websocket_manager.py +0 -0
  264. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  265. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  266. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  267. {qubx-1.0.0.dev2 → qubx-1.0.0.dev4}/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.0.0.dev2
3
+ Version: 1.0.0.dev4
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
@@ -33,6 +33,7 @@ Requires-Dist: psycopg-binary<4,>=3.1.19
33
33
  Requires-Dist: psycopg-pool<4,>=3.2.2
34
34
  Requires-Dist: psycopg<4,>=3.1.18
35
35
  Requires-Dist: pyarrow>=15.0.0
36
+ Requires-Dist: pydantic-settings<3,>=2.4.0
36
37
  Requires-Dist: pydantic<3,>=2.9.2
37
38
  Requires-Dist: pymongo<5,>=4.6.1
38
39
  Requires-Dist: python-binance<2,>=1.0.19
@@ -57,6 +58,8 @@ Requires-Dist: uvloop<1,>=0.22.1; sys_platform != 'win32'
57
58
  Requires-Dist: websockets==15.0.1
58
59
  Provides-Extra: k8
59
60
  Requires-Dist: prometheus-client<1,>=0.21.1; extra == 'k8'
61
+ Provides-Extra: storage
62
+ Requires-Dist: duckdb>=1.0.0; extra == 'storage'
60
63
  Description-Content-Type: text/markdown
61
64
 
62
65
  # Qubx - Quantitative Trading Framework
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "importlib-metadata",
35
35
  "stackprinter>=0.2.10,<1",
36
36
  "pydantic>=2.9.2,<3",
37
+ "pydantic-settings>=2.4.0,<3",
37
38
  "python-dotenv>=1.0.0,<2",
38
39
  "cython==3.0.8",
39
40
  "croniter>=2.0.5,<3",
@@ -66,6 +67,8 @@ docs = "https://xlydiansoftware.github.io/Qubx"
66
67
  [project.optional-dependencies]
67
68
  # Runtime optional features only (shipped with package)
68
69
  k8 = ["prometheus-client>=0.21.1,<1"]
70
+ # Parquet-based backtest storage with DuckDB search and cloud (S3/GCS/Azure) support
71
+ storage = ["duckdb>=1.0.0"]
69
72
 
70
73
  [project.scripts]
71
74
  qubx = "qubx.cli.commands:main"
@@ -177,4 +180,5 @@ dev = [
177
180
  "mongomock>=4.3.0,<5",
178
181
  "pytest-textual-snapshot>=1.1.0,<2",
179
182
  "git-cliff>=2.0.0",
183
+ "duckdb>=1.0.0"
180
184
  ]
@@ -57,7 +57,13 @@ def formatter(record):
57
57
  class QubxLogConfig:
58
58
  @staticmethod
59
59
  def get_log_level():
60
- return os.getenv("QUBX_LOG_LEVEL", "WARNING")
60
+ # Env var takes priority (for CLI --log-level override), then settings
61
+ env_level = os.getenv("QUBX_LOG_LEVEL")
62
+ if env_level:
63
+ return env_level
64
+ from qubx.config import settings
65
+
66
+ return settings.log_level
61
67
 
62
68
  @staticmethod
63
69
  def set_log_level(level: str):
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.0.0.dev2'
32
- __version_tuple__ = version_tuple = (1, 0, 0, 'dev2')
31
+ __version__ = version = '1.0.0.dev4'
32
+ __version_tuple__ = version_tuple = (1, 0, 0, 'dev4')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,4 @@
1
+ __all__ = ["BacktestStorage", "variate"]
2
+
3
+ from .management import BacktestStorage
4
+ from .optimization import variate
@@ -0,0 +1,506 @@
1
+ """
2
+ Parquet-based backtest storage utilities — schemas, constants, and write helpers.
3
+
4
+ Used by:
5
+ - qubx.core.metrics.TradingSessionResult (result model)
6
+ - qubx.backtester.management.BacktestStorage (query interface)
7
+ - qubx.utils.results.SimulationResultsSaver (save / load)
8
+ - qubx.utils.runner.runner.simulate_strategy (cloud detection, tag helpers)
9
+
10
+ Storage layout (single run)::
11
+
12
+ {base_path}/
13
+ └── {yaml.name}/ # from cfg.name field (required)
14
+ └── {ShortClass}/ # short strategy class name(s), multi joined with '+'
15
+ └── YYYYMMDD_HHMMSS/ # unique per run
16
+ ├── _status.parquet # written first, updated live during simulation
17
+ ├── _metadata.parquet # written on completion (all perf metrics)
18
+ ├── portfolio.parquet
19
+ ├── executions.parquet
20
+ ├── signals.parquet
21
+ ├── targets.parquet
22
+ └── config.yaml # attached config file
23
+
24
+ Storage layout (variation set)::
25
+
26
+ {base_path}/
27
+ └── {yaml.name}/
28
+ └── {ShortClass}/
29
+ └── YYYYMMDD_HHMMSS/
30
+ ├── _status.parquet
31
+ ├── _metadata.parquet # N rows, one per variation — searchable by DuckDB
32
+ ├── var_000/
33
+ │ ├── portfolio.parquet
34
+ │ ├── executions.parquet
35
+ │ ├── signals.parquet
36
+ │ └── targets.parquet
37
+ ├── var_001/
38
+ │ └── ...
39
+ └── config.yaml
40
+
41
+ DuckDB examples (via BacktestStorage)::
42
+
43
+ storage.search("sharpe > 2 AND mdd_pct < 25 AND list_contains(tags, 'momentum')")
44
+ storage.status("running")
45
+ storage.get_portfolio("my_strat/Nimble/20240301_120000", symbol="BTCUSDT", start="2024-01-01")
46
+ """
47
+
48
+ import pandas as pd
49
+
50
+ from qubx.core.metrics import TradingSessionResult
51
+ from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
52
+ from qubx.utils.results import SimulationResultsSaver, is_cloud_path, resolve_s3_storage_options
53
+
54
+
55
+ class BacktestStorage:
56
+ """
57
+ Query interface for parquet-based backtest storage.
58
+ Supports local directories and cloud paths (S3, GCS, Azure).
59
+
60
+ Uses DuckDB for fast metadata search and data queries across all stored backtests.
61
+
62
+ Storage layout (single run)::
63
+
64
+ {base_path}/
65
+ └── {yaml.name}/ # from cfg.name field (required)
66
+ └── {ShortClass}/ # short strategy class name(s)
67
+ └── YYYYMMDD_HHMMSS/
68
+ ├── _status.parquet # live progress, written by SimulationResultsSaver
69
+ ├── _metadata.parquet # completion metrics
70
+ ├── portfolio.parquet
71
+ ├── executions.parquet
72
+ ├── signals.parquet
73
+ ├── targets.parquet
74
+ ├── emitter_data.parquet
75
+ ├── transfers.parquet
76
+ └── config.yaml
77
+
78
+ Examples::
79
+
80
+ # - local storage
81
+ storage = BacktestStorage("/backtests/")
82
+
83
+ # - S3 storage (creds from env: QUBX_S3_KEY / AWS_ACCESS_KEY_ID)
84
+ storage = BacktestStorage("s3://my-bucket/backtests/")
85
+
86
+ # - search: full DuckDB SQL WHERE clause
87
+ df = storage.search("sharpe > 2 AND mdd_pct < 25")
88
+ df = storage.search("list_contains(tags, 'momentum') AND cagr > 0.3")
89
+ df = storage.search("json_extract(parameters, '$.fast_period')::int > 10")
90
+ df = storage.search() # - all results
91
+
92
+ # - live status dashboard
93
+ df = storage.status("running")
94
+
95
+ # - load result
96
+ result = storage.load("my_strat/Nimble/20240301_120000")
97
+
98
+ # - best variation from a variation set
99
+ result = storage.load_best_variation("my_strat/Nimble/20240301_130000", by="sharpe")
100
+ """
101
+
102
+ def __init__(self, base_path: str, storage_options: dict | None = None):
103
+ """
104
+ Initialize BacktestStorage.
105
+
106
+ Args:
107
+ base_path: Root path for backtest storage (local dir or cloud URI)
108
+ storage_options: Cloud storage credentials dict. None = uses default_s3_account from settings.
109
+ """
110
+ try:
111
+ import duckdb
112
+
113
+ self._duckdb = duckdb
114
+ except ImportError:
115
+ raise ImportError(
116
+ "duckdb is required for BacktestStorage. "
117
+ "Install with: pip install 'qubx[storage]' or pip install duckdb"
118
+ )
119
+ self.base_path = base_path.rstrip("/") + "/"
120
+ self._is_cloud = is_cloud_path(base_path)
121
+
122
+ # - for cloud paths: resolve credentials once (env vars → explicit dict)
123
+ self._storage_options: dict | None = resolve_s3_storage_options(storage_options) if self._is_cloud else None
124
+ self._conn = self._duckdb.connect()
125
+
126
+ if self._is_cloud:
127
+ self._setup_cloud_duckdb()
128
+
129
+ def _setup_cloud_duckdb(self) -> None:
130
+ """Configure DuckDB httpfs extension for cloud storage access."""
131
+ self._conn.execute("INSTALL httpfs; LOAD httpfs;")
132
+
133
+ # - _storage_options is already resolved at __init__ for cloud paths
134
+ opts = self._storage_options or {}
135
+ if "key" in opts:
136
+ self._conn.execute(f"SET s3_access_key_id='{opts['key']}';")
137
+ if "secret" in opts:
138
+ self._conn.execute(f"SET s3_secret_access_key='{opts['secret']}';")
139
+ if "endpoint_url" in opts:
140
+ # - strip protocol prefix — DuckDB expects hostname only
141
+ endpoint = opts["endpoint_url"].removeprefix("https://").removeprefix("http://")
142
+ self._conn.execute(f"SET s3_endpoint='{endpoint}';")
143
+ if "client_kwargs" in opts:
144
+ region = opts["client_kwargs"].get("region_name")
145
+ if region:
146
+ self._conn.execute(f"SET s3_region='{region}';")
147
+
148
+ def _glob(self, filename: str) -> str:
149
+ """
150
+ Build recursive glob pattern for a filename within base_path.
151
+ """
152
+ return f"{self.base_path}**/{filename}"
153
+
154
+ def search(
155
+ self,
156
+ where: str | None = None,
157
+ order_by: str = "sharpe DESC",
158
+ limit: int | None = None,
159
+ ) -> pd.DataFrame:
160
+ """
161
+ Search backtest metadata across all stored results using DuckDB SQL.
162
+
163
+ The WHERE clause has full DuckDB SQL power — no restrictions::
164
+
165
+ "sharpe > 2 AND mdd_pct < 25 AND author = 'alice'"
166
+ "list_contains(tags, 'momentum') AND cagr > 0.3"
167
+ "json_extract(parameters, '$.fast_period')::int > 10"
168
+ "is_variation = false"
169
+ "strategy_class LIKE '%Nimble%'"
170
+ "start >= '2024-01-01' AND sharpe BETWEEN 1.5 AND 4.0"
171
+
172
+ Regular backtests: one row per run.
173
+ Variation sets: N rows per set (one per variation), all with is_variation=true.
174
+
175
+ Args:
176
+ where: DuckDB SQL WHERE clause, or None to return all results
177
+ order_by: ORDER BY clause (default: "sharpe DESC")
178
+ limit: Maximum rows to return
179
+
180
+ Returns:
181
+ pd.DataFrame with matching metadata rows
182
+ """
183
+ glob = self._glob(SimulationResultsSaver.METADATA_FILE)
184
+ sql = f"SELECT * FROM read_parquet('{glob}', union_by_name=true)"
185
+ if where:
186
+ sql += f" WHERE {where}"
187
+ if order_by:
188
+ sql += f" ORDER BY {order_by}"
189
+ if limit is not None:
190
+ sql += f" LIMIT {limit}"
191
+ return self._conn.execute(sql).df()
192
+
193
+ def status(self, filter_status: str | None = None) -> pd.DataFrame:
194
+ """
195
+ Get status of all simulations (running, completed, failed, pending).
196
+
197
+ Reads _status.parquet files written by SimulationResultsSaver.
198
+ Works in real-time — running simulations update their status every 1%.
199
+
200
+ Args:
201
+ filter_status: Filter by status value ('running', 'completed', 'failed', 'pending'),
202
+ or None to return all simulations
203
+
204
+ Returns:
205
+ pd.DataFrame with status rows, ordered by started_at DESC
206
+ """
207
+ glob = self._glob(SimulationResultsSaver.STATUS_FILE)
208
+ sql = f"SELECT * FROM read_parquet('{glob}', union_by_name=true)"
209
+ if filter_status:
210
+ sql += f" WHERE status = '{filter_status}'"
211
+ sql += " ORDER BY started_at DESC"
212
+ return self._conn.execute(sql).df()
213
+
214
+ def _load_from_path(self, run_path: str) -> TradingSessionResult:
215
+ """
216
+ Load a TradingSessionResult from parquet storage (local or cloud).
217
+
218
+ Delegates to SimulationResultsSaver.load() which reads data files
219
+ in parallel using a ThreadPoolExecutor.
220
+ """
221
+ return SimulationResultsSaver.load(run_path, self._storage_options)
222
+
223
+ def load(self, backtest_id: str) -> TradingSessionResult:
224
+ """
225
+ Load a TradingSessionResult by backtest_id.
226
+
227
+ Args:
228
+ backtest_id: Relative path within base_path,
229
+ e.g. "my_strategy/Nimble/20240301_120000"
230
+
231
+ Returns:
232
+ TradingSessionResult with all data loaded from parquet
233
+ """
234
+ return self._load_from_path(f"{self.base_path}{backtest_id.strip('/')}/")
235
+
236
+ def load_best_variation(
237
+ self,
238
+ variation_set_id: str,
239
+ by: str = "sharpe",
240
+ ascending: bool = False,
241
+ ) -> TradingSessionResult:
242
+ """
243
+ Load the best-performing variation from a variation set.
244
+
245
+ The variation set _metadata.parquet has one row per variation.
246
+ Finds the best row by the given metric, then loads its data.
247
+
248
+ Args:
249
+ variation_set_id: Relative path to variation set root,
250
+ e.g. "my_strategy/Nimble/20240301_130000"
251
+ by: Metric column to rank by (default: "sharpe")
252
+ ascending: If True, load minimum instead of maximum (default: False)
253
+
254
+ Returns:
255
+ TradingSessionResult of the best variation
256
+ """
257
+ meta_path = f"{self.base_path}{variation_set_id.strip('/')}/{SimulationResultsSaver.METADATA_FILE}"
258
+ order = "ASC" if ascending else "DESC"
259
+ row = self._conn.execute(f"SELECT * FROM read_parquet('{meta_path}') ORDER BY {by} {order} LIMIT 1").df()
260
+
261
+ if row.empty:
262
+ raise ValueError(f"No variations found at '{variation_set_id}'")
263
+
264
+ var_id = row["variation_id"].iloc[0]
265
+ run_path = f"{self.base_path}{variation_set_id.strip('/')}/{var_id}/"
266
+ return self._load_from_path(run_path)
267
+
268
+ def get_portfolio(
269
+ self,
270
+ backtest_id: str,
271
+ symbol: str | None = None,
272
+ start: str | None = None,
273
+ stop: str | None = None,
274
+ ) -> pd.DataFrame:
275
+ """
276
+ Get portfolio log data for a backtest.
277
+
278
+ Portfolio is stored in wide format: one column per symbol metric
279
+ (e.g. "BINANCE.UM:BTCUSDT_PnL", "BINANCE.UM:BTCUSDT_Commission").
280
+
281
+ Args:
282
+ backtest_id: Relative path within base_path
283
+ symbol: If set, returns only columns containing this symbol name (case-insensitive)
284
+ start: Start timestamp filter (inclusive)
285
+ stop: Stop timestamp filter (inclusive)
286
+
287
+ Returns:
288
+ pd.DataFrame with portfolio data
289
+ """
290
+ path = f"{self.base_path}{backtest_id.strip('/')}/{SimulationResultsSaver.DATA_FILES['portfolio']}"
291
+ return self._query_wide(path, symbol=symbol, start=start, stop=stop)
292
+
293
+ def get_executions(
294
+ self,
295
+ backtest_id: str,
296
+ symbol: str | None = None,
297
+ start: str | None = None,
298
+ stop: str | None = None,
299
+ ) -> pd.DataFrame:
300
+ """
301
+ Get execution log data for a backtest.
302
+
303
+ Args:
304
+ backtest_id: Relative path within base_path
305
+ symbol: Filter rows by instrument column (case-insensitive match)
306
+ start: Start timestamp filter (inclusive)
307
+ stop: Stop timestamp filter (inclusive)
308
+
309
+ Returns:
310
+ pd.DataFrame with execution data
311
+ """
312
+ path = f"{self.base_path}{backtest_id.strip('/')}/{SimulationResultsSaver.DATA_FILES['executions']}"
313
+ return self._query_long(path, symbol=symbol, start=start, stop=stop)
314
+
315
+ def get_signals(
316
+ self,
317
+ backtest_id: str,
318
+ symbol: str | None = None,
319
+ start: str | None = None,
320
+ stop: str | None = None,
321
+ ) -> pd.DataFrame:
322
+ """
323
+ Get signals log data for a backtest.
324
+
325
+ Args:
326
+ backtest_id: Relative path within base_path
327
+ symbol: Filter rows by instrument column (case-insensitive match)
328
+ start: Start timestamp filter (inclusive)
329
+ stop: Stop timestamp filter (inclusive)
330
+
331
+ Returns:
332
+ pd.DataFrame with signals data
333
+ """
334
+ path = f"{self.base_path}{backtest_id.strip('/')}/{SimulationResultsSaver.DATA_FILES['signals']}"
335
+ return self._query_long(path, symbol=symbol, start=start, stop=stop)
336
+
337
+ def _query_wide(
338
+ self,
339
+ path: str,
340
+ symbol: str | None = None,
341
+ start: str | None = None,
342
+ stop: str | None = None,
343
+ ) -> pd.DataFrame:
344
+ """
345
+ Query a wide-format parquet (portfolio log) with optional column/time filtering.
346
+ Symbol filtering selects columns containing the symbol string using DuckDB COLUMNS().
347
+ """
348
+ conditions = []
349
+ if start:
350
+ conditions.append(f"timestamp >= '{start}'")
351
+ if stop:
352
+ conditions.append(f"timestamp <= '{stop}'")
353
+ where_clause = f" WHERE {' AND '.join(conditions)}" if conditions else ""
354
+
355
+ if symbol:
356
+ sym = symbol.upper()
357
+ # - DuckDB COLUMNS() lambda: select timestamp + _backtest_id + symbol columns
358
+ sql = f"""
359
+ SELECT COLUMNS(c -> c = 'timestamp' OR c = '_backtest_id'
360
+ OR contains(upper(c), '{sym}'))
361
+ FROM read_parquet('{path}'){where_clause}
362
+ """
363
+ else:
364
+ sql = f"SELECT * FROM read_parquet('{path}'){where_clause}"
365
+
366
+ return self._conn.execute(sql).df()
367
+
368
+ def _query_long(
369
+ self,
370
+ path: str,
371
+ symbol: str | None = None,
372
+ start: str | None = None,
373
+ stop: str | None = None,
374
+ symbol_col: str = "symbol",
375
+ ) -> pd.DataFrame:
376
+ """
377
+ Query a long-format parquet (executions, signals) with optional row filtering.
378
+ Symbol filtering matches rows where symbol_col contains the symbol string.
379
+ """
380
+ conditions = []
381
+ if start:
382
+ conditions.append(f"timestamp >= '{start}'")
383
+ if stop:
384
+ conditions.append(f"timestamp <= '{stop}'")
385
+ if symbol:
386
+ conditions.append(f"contains(upper({symbol_col}), '{symbol.upper()}')")
387
+ where_clause = f" WHERE {' AND '.join(conditions)}" if conditions else ""
388
+ sql = f"SELECT * FROM read_parquet('{path}'){where_clause}"
389
+ return self._conn.execute(sql).df()
390
+
391
+ def print(
392
+ self,
393
+ where: str | None = None,
394
+ order_by: str = "creation_time DESC",
395
+ limit: int | None = None,
396
+ params: bool = False,
397
+ ) -> None:
398
+ """
399
+ Pretty-print a colored list of backtests stored at base_path.
400
+
401
+ Matches the style of the old BacktestsResultsManager.list() — header line,
402
+ description, strategy / interval / capital / instruments, full metrics table.
403
+
404
+ Args:
405
+ where: DuckDB WHERE clause to filter (e.g. ``"sharpe > 2"``). None = all.
406
+ order_by: ORDER BY clause (default: ``"creation_time DESC"``).
407
+ limit: Maximum number of results to display.
408
+ params: If True, print strategy parameters below the metrics table.
409
+ """
410
+ df = self.search(where=where, order_by=order_by, limit=limit)
411
+
412
+ if df.empty:
413
+ print("No backtests found.")
414
+ return
415
+
416
+ _l = lambda v: [] if v is None else list(v) # noqa: E731 — numpy array → Python list
417
+ _METRIC_COLS = ["gain", "cagr", "sharpe", "qr", "mdd_pct", "mdd_usd", "fees", "execs"]
418
+
419
+ for _, row in df.iterrows():
420
+ _id = row.get("backtest_id", "")
421
+ _name = row.get("name", "")
422
+ _cls = str(row.get("strategy_class", "")).split(".")[-1]
423
+ _created = pd.Timestamp(row.get("creation_time")).strftime("%Y-%m-%d %H:%M:%S")
424
+ _author = row.get("author", "")
425
+ _start = pd.Timestamp(row.get("start")).strftime("%Y-%m-%d")
426
+ _stop = pd.Timestamp(row.get("stop")).strftime("%Y-%m-%d")
427
+ _capital = row.get("capital", "")
428
+ _ccy = row.get("base_currency", "")
429
+ _comm = row.get("commissions", "")
430
+ _dscr = row.get("description", "") or ""
431
+ _tags = _l(row.get("tags"))
432
+ _symbols = ", ".join(_l(row.get("symbols")))
433
+ _is_var = row.get("is_variation", False)
434
+
435
+ # - header: id :: name ::: created by author
436
+ _s = f"{yellow(_id)} :: {red(_name)}"
437
+ if _is_var:
438
+ _var_id = row.get("variation_id", "")
439
+ _var_params = row.get("variation_params", "") or ""
440
+ _s += f" [{cyan(_var_id)}] {magenta(_var_params)}"
441
+ _s += f" ::: {magenta(_created)} by {cyan(_author)}"
442
+
443
+ # - description lines
444
+ if _dscr:
445
+ for _d in _dscr.split("\n"):
446
+ if _d.strip():
447
+ _s += f"\n\t{magenta('# ' + _d)}"
448
+
449
+ _s += f"\n\tstrategy: {green(_cls)}"
450
+ _s += f"\n\tinterval: {blue(_start)} - {blue(_stop)}"
451
+ _s += f"\n\tcapital: {blue(str(_capital))} {_ccy} ({_comm})"
452
+ _s += f"\n\tinstruments: {blue(_symbols)}"
453
+ if _tags:
454
+ _s += f"\n\ttags: {cyan(str(_tags))}"
455
+
456
+ print(_s)
457
+
458
+ # - performance metrics table (red header, cyan values — same as old manager)
459
+ _metrics = {
460
+ c: (int(row.get(c) or 0) if c == "execs" else round(float(row.get(c) or 0.0), 3))
461
+ for c in _METRIC_COLS
462
+ if c in row
463
+ }
464
+ _m_df = pd.DataFrame([_metrics])
465
+ _m_str = _m_df.to_string(index=False)
466
+ _h, _v = _m_str.split("\n")
467
+ print("\t " + red(_h))
468
+ print("\t " + cyan(_v))
469
+
470
+ # - optional parameters
471
+ if params:
472
+ import json as _json
473
+
474
+ _p = _json.loads(row.get("parameters") or "{}")
475
+ if _p:
476
+ for k, v in _p.items():
477
+ print(f"\t {yellow(k)}: {cyan(str(v))}")
478
+
479
+ print()
480
+
481
+ def list(
482
+ self,
483
+ where: str | None = None,
484
+ order_by: str = "creation_time DESC",
485
+ limit: int | None = None,
486
+ ) -> list[str]:
487
+ """
488
+ Return a list of backtest IDs matching the given filter.
489
+
490
+ Args:
491
+ where: Optional SQL WHERE clause to filter results.
492
+ order_by: SQL ORDER BY clause (default: ``creation_time DESC``).
493
+ limit: Maximum number of IDs to return.
494
+
495
+ Returns:
496
+ List of backtest_id strings, e.g.
497
+ ``["my_strategy/Nimble/20240301_120000", ...]``
498
+ """
499
+ df = self.search(where=where, order_by=order_by, limit=limit)
500
+ if df.empty or "backtest_id" not in df.columns:
501
+ return []
502
+ return df["backtest_id"].tolist()
503
+
504
+ def export_backtests_to_markdown(self, backtest_id: str, path: str, tags: tuple[str] | None = None):
505
+ if tsr := self.load(backtest_id):
506
+ tsr.to_markdown(path, list(tags) if tags else None)