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