Qubx 1.0.6.dev6__tar.gz → 1.0.6.dev7__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 (271) hide show
  1. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/PKG-INFO +1 -1
  2. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/_version.py +2 -2
  3. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/data.py +3 -8
  4. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/management.py +6 -5
  5. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/runner.py +1 -1
  6. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/commands.py +101 -4
  7. qubx-1.0.6.dev7/src/qubx/cli/s3.py +124 -0
  8. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/tui.py +85 -3
  9. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/config.py +1 -0
  10. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/interfaces.py +5 -1
  11. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/loggers.py +1 -1
  12. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/metrics.py +80 -19
  13. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/subscription.py +5 -1
  14. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/cache.py +74 -47
  15. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/guards.py +45 -0
  16. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/sizers.py +6 -2
  17. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/results.py +10 -116
  18. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/runner.py +78 -75
  19. qubx-1.0.6.dev7/src/qubx/utils/s3.py +310 -0
  20. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/.gitignore +0 -0
  21. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/LICENSE +0 -0
  22. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/README.md +0 -0
  23. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/pyproject.toml +0 -0
  24. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/scripts/build.py +0 -0
  25. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/__init__.py +0 -0
  26. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/_nb_magic.py +0 -0
  27. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/__init__.py +0 -0
  28. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/account.py +0 -0
  29. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/broker.py +0 -0
  30. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/iteratedstream.py +0 -0
  31. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/ome.py +0 -0
  32. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/optimization.py +0 -0
  33. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/sentinels.py +0 -0
  34. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/simulated_data.py +0 -0
  35. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/simulated_exchange.py +0 -0
  36. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/simulator.py +0 -0
  37. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/transfers.py +0 -0
  38. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/backtester/utils.py +0 -0
  39. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/__init__.py +0 -0
  40. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/deploy.py +0 -0
  41. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/misc.py +0 -0
  42. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/release.py +0 -0
  43. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/resolver.py +0 -0
  44. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/cli/theme.py +0 -0
  45. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/__init__.py +0 -0
  46. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/__init__.py +0 -0
  47. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/account.py +0 -0
  48. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  49. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  50. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/broker.py +0 -0
  51. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  52. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/data.py +0 -0
  53. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  54. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  55. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  56. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  57. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  58. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  59. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  60. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  61. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  62. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  63. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  64. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  65. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  66. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  67. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  68. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  69. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/okx/account.py +0 -0
  70. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
  71. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  72. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/factory.py +0 -0
  73. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  74. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  75. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  76. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  77. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  78. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  79. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  80. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  81. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  82. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  83. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  84. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  85. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  86. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/utils.py +0 -0
  87. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  88. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/registry.py +0 -0
  89. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/tardis/data.py +0 -0
  90. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/connectors/tardis/utils.py +0 -0
  91. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/__init__.py +0 -0
  92. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/account.py +0 -0
  93. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/basics.py +0 -0
  94. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/context.py +0 -0
  95. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/detectors/__init__.py +0 -0
  96. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/detectors/delisting.py +0 -0
  97. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/detectors/stale.py +0 -0
  98. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/errors.py +0 -0
  99. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/exceptions.py +0 -0
  100. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/helpers.py +0 -0
  101. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/initializer.py +0 -0
  102. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/lookups.py +0 -0
  103. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/__init__.py +0 -0
  104. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/market.py +0 -0
  105. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/processing.py +0 -0
  106. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/trading.py +0 -0
  107. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/universe.py +0 -0
  108. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/mixins/utils.py +0 -0
  109. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/series.pxd +0 -0
  110. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/series.pyi +0 -0
  111. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/series.pyx +0 -0
  112. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/utils.pyi +0 -0
  113. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/core/utils.pyx +0 -0
  114. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/__init__.py +0 -0
  115. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/containers.py +0 -0
  116. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/registry.py +0 -0
  117. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storage.py +0 -0
  118. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/ccxt.py +0 -0
  119. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/csv.py +0 -0
  120. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/handy.py +0 -0
  121. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/multi.py +0 -0
  122. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/questdb.py +0 -0
  123. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/stub.py +0 -0
  124. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/storages/utils.py +0 -0
  125. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/data/transformers.py +0 -0
  126. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/__init__.py +0 -0
  127. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/base.py +0 -0
  128. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/composite.py +0 -0
  129. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/csv.py +0 -0
  130. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/indicator.py +0 -0
  131. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/inmemory.py +0 -0
  132. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/prometheus.py +0 -0
  133. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/emitters/questdb.py +0 -0
  134. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/__init__.py +0 -0
  135. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/composite.py +0 -0
  136. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/__init__.py +0 -0
  137. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/base.py +0 -0
  138. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/incremental.py +0 -0
  139. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/slack.py +0 -0
  140. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/formatters/target_position.py +0 -0
  141. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/redis_streams.py +0 -0
  142. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/exporters/slack.py +0 -0
  143. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/gathering/simplest.py +0 -0
  144. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/health/__init__.py +0 -0
  145. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/health/base.py +0 -0
  146. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/health/dummy.py +0 -0
  147. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/health/server.py +0 -0
  148. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/__init__.py +0 -0
  149. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/csv.py +0 -0
  150. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/factory.py +0 -0
  151. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/inmemory.py +0 -0
  152. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/loggers/mongo.py +0 -0
  153. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/notifications/__init__.py +0 -0
  154. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/notifications/composite.py +0 -0
  155. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/notifications/slack.py +0 -0
  156. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/notifications/throttler.py +0 -0
  157. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/pandaz/__init__.py +0 -0
  158. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/pandaz/stats.py +0 -0
  159. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/pandaz/ta.py +0 -0
  160. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/pandaz/utils.py +0 -0
  161. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/plugins/__init__.py +0 -0
  162. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/plugins/loader.py +0 -0
  163. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/_build.py +0 -0
  164. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/crypto-fees.ini +0 -0
  165. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  166. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  167. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  168. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  169. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  170. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  171. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  172. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  173. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  174. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  175. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  176. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restarts/__init__.py +0 -0
  177. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restarts/state_resolvers.py +0 -0
  178. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restarts/time_finders.py +0 -0
  179. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/__init__.py +0 -0
  180. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/balance.py +0 -0
  181. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/factory.py +0 -0
  182. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/interfaces.py +0 -0
  183. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/position.py +0 -0
  184. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/signal.py +0 -0
  185. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/state.py +0 -0
  186. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/restorers/utils.py +0 -0
  187. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/state/__init__.py +0 -0
  188. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/state/dummy.py +0 -0
  189. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/state/redis.py +0 -0
  190. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/ta/__init__.py +0 -0
  191. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/ta/indicators.pxd +0 -0
  192. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/ta/indicators.pyi +0 -0
  193. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/ta/indicators.pyx +0 -0
  194. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/__init__.py +0 -0
  195. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/base.py +0 -0
  196. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  197. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/config.yml.j2 +0 -0
  198. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  199. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  200. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  201. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  202. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  203. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/project/template.yml +0 -0
  204. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  205. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  206. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/config.yml.j2 +0 -0
  207. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  208. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  209. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  210. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/templates/simple/template.yml +0 -0
  211. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/__init__.py +0 -0
  212. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/advanced.py +0 -0
  213. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/composite.py +0 -0
  214. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/rebalancers.py +0 -0
  215. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/trackers/riskctrl.py +0 -0
  216. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/__init__.py +0 -0
  217. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/_pyxreloader.py +0 -0
  218. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/charting/lookinglass.py +0 -0
  219. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  220. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/charting/orderbook.py +0 -0
  221. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/collections.py +0 -0
  222. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/hft/__init__.py +0 -0
  223. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/hft/numba_utils.py +0 -0
  224. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/hft/orderbook.pyi +0 -0
  225. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/hft/orderbook.pyx +0 -0
  226. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/marketdata/binance.py +0 -0
  227. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/marketdata/ccxt.py +0 -0
  228. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/marketdata/dukas.py +0 -0
  229. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/misc.py +0 -0
  230. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/nonce.py +0 -0
  231. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/ntp.py +0 -0
  232. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/numbers_utils.py +0 -0
  233. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/orderbook.py +0 -0
  234. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/__init__.py +0 -0
  235. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/dashboard.py +0 -0
  236. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/data.py +0 -0
  237. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/interfaces.py +0 -0
  238. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  239. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  240. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/questdb.py +0 -0
  241. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/rate_limiter.py +0 -0
  242. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/ringbuffer.pxd +0 -0
  243. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/ringbuffer.pyi +0 -0
  244. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/ringbuffer.pyx +0 -0
  245. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/__init__.py +0 -0
  246. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  247. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/accounts.py +0 -0
  248. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/configs.py +0 -0
  249. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/factory.py +0 -0
  250. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/kernel_service.py +0 -0
  251. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/__init__.py +0 -0
  252. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/app.py +0 -0
  253. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/handlers.py +0 -0
  254. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/init_code.py +0 -0
  255. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/kernel.py +0 -0
  256. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  257. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  258. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  259. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  260. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  261. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  262. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  263. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  264. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/slack.py +0 -0
  265. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/throttler.py +0 -0
  266. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/time.py +0 -0
  267. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/src/qubx/utils/websocket_manager.py +0 -0
  268. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  269. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  270. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  271. {qubx-1.0.6.dev6 → qubx-1.0.6.dev7}/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.6.dev6
3
+ Version: 1.0.6.dev7
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
@@ -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.6.dev6'
32
- __version_tuple__ = version_tuple = (1, 0, 6, 'dev6')
31
+ __version__ = version = '1.0.6.dev7'
32
+ __version_tuple__ = version_tuple = (1, 0, 6, 'dev7')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -102,19 +102,14 @@ class SimulatedDataProvider(IDataProvider):
102
102
  # - try to peek most recent market data
103
103
  h_data = self._data_source.peek_historical_data(i, subscription_type)
104
104
  if h_data:
105
- # _s_type = DataType.from_str(subscription_type)[0]
105
+ self.channel.send((i, subscription_type, h_data, True))
106
+
106
107
  last_update = h_data[-1]
107
108
  if last_quote := self._account._exchange.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
108
- # - send historical data to the channel
109
- self.channel.send((i, subscription_type, h_data, True))
110
-
111
- # - set last quote
112
109
  self._last_quotes[i] = last_quote
113
-
114
- # - also need to pass this quote to OME !
115
110
  self._account.process_market_data(last_quote.time, i, last_quote) # type: ignore
116
111
 
117
- logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
112
+ logger.debug(f" | subscribed {subscription_type} {i} -> {len(h_data)} records")
118
113
 
119
114
  def unsubscribe(self, subscription_type: str, instruments: set[Instrument] | Instrument | None = None) -> None:
120
115
  # logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
@@ -49,7 +49,8 @@ import pandas as pd
49
49
 
50
50
  from qubx.core.metrics import TradingSessionResult
51
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
52
+ from qubx.utils.results import SimulationResultsSaver
53
+ from qubx.utils.s3 import S3Client, is_cloud_path
53
54
 
54
55
 
55
56
  class BacktestStorage:
@@ -119,8 +120,10 @@ class BacktestStorage:
119
120
  self.base_path = base_path.rstrip("/") + "/"
120
121
  self._is_cloud = is_cloud_path(base_path)
121
122
 
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
123
+ # - for cloud paths: resolve credentials once
124
+ self._storage_options: dict | None = (
125
+ S3Client(storage_options=storage_options).storage_options if self._is_cloud else None
126
+ )
124
127
  self._conn = self._duckdb.connect()
125
128
 
126
129
  if self._is_cloud:
@@ -130,14 +133,12 @@ class BacktestStorage:
130
133
  """Configure DuckDB httpfs extension for cloud storage access."""
131
134
  self._conn.execute("INSTALL httpfs; LOAD httpfs;")
132
135
 
133
- # - _storage_options is already resolved at __init__ for cloud paths
134
136
  opts = self._storage_options or {}
135
137
  if "key" in opts:
136
138
  self._conn.execute(f"SET s3_access_key_id='{opts['key']}';")
137
139
  if "secret" in opts:
138
140
  self._conn.execute(f"SET s3_secret_access_key='{opts['secret']}';")
139
141
  if "endpoint_url" in opts:
140
- # - strip protocol prefix — DuckDB expects hostname only
141
142
  endpoint = opts["endpoint_url"].removeprefix("https://").removeprefix("http://")
142
143
  self._conn.execute(f"SET s3_endpoint='{endpoint}';")
143
144
  if "client_kwargs" in opts:
@@ -437,7 +437,7 @@ class SimulationRunner:
437
437
  # - TimeGuard is applied by callers that need it (aux/strategy-facing access).
438
438
  # - Main simulation data (DataPump) must NOT be time-guarded: DataPump provides
439
439
  # its own explicit [start, end] window and needs to read the full range upfront.
440
- _prefetch_period = str(max(self.stop - self.start, pd.Timedelta(prefetch_cfg.prefetch_period)))
440
+ _prefetch_period = str(min(self.stop - self.start, pd.Timedelta(prefetch_cfg.prefetch_period)))
441
441
  return CachedStorage(
442
442
  storage, prefetch_period=_prefetch_period, cache_factory=lambda: MemoryCache(prefetch_cfg.cache_size_mb)
443
443
  )
@@ -248,6 +248,13 @@ def run(
248
248
  "Overrides the 'name:' field from the config file.",
249
249
  show_default=True,
250
250
  )
251
+ @click.option(
252
+ "--log-file",
253
+ default=None,
254
+ type=click.Path(dir_okay=False),
255
+ help="Write simulation logs to a specific file path.",
256
+ show_default=True,
257
+ )
251
258
  def simulate(
252
259
  config_file: Path,
253
260
  start: str | None,
@@ -256,6 +263,7 @@ def simulate(
256
263
  report: str | None,
257
264
  log_to_file: bool,
258
265
  name: str | None,
266
+ log_file: str | None,
259
267
  ):
260
268
  """
261
269
  Simulates the strategy with the given configuration file.
@@ -267,7 +275,9 @@ def simulate(
267
275
  add_project_to_system_path(str(config_file.parent))
268
276
  logo()
269
277
  logger.info(f"Process PID: <g>{os.getpid()}</g>")
270
- simulate_strategy(config_file, output, start, end, report, log_to_file, name=name)
278
+ simulate_strategy(
279
+ config_file, output, start, end, report, log_to_file, name=name, log_file=log_file,
280
+ )
271
281
 
272
282
 
273
283
  @main.command()
@@ -467,9 +477,10 @@ def deploy(zip_file: str, output_dir: str | None, force: bool, system: bool):
467
477
  @click.argument(
468
478
  "results-path",
469
479
  type=str,
470
- default="results",
480
+ default=None,
481
+ required=False,
471
482
  )
472
- def browse(results_path: str):
483
+ def browse(results_path: str | None):
473
484
  """
474
485
  Browse backtest results using an interactive TUI.
475
486
 
@@ -487,10 +498,14 @@ def browse(results_path: str):
487
498
 
488
499
  qubx browse s3://my-bucket/backtests/
489
500
  """
490
- from qubx.utils.results import is_cloud_path
501
+ from qubx.config import get_settings
502
+ from qubx.utils.s3 import is_cloud_path
491
503
 
492
504
  from .tui import run_backtest_browser
493
505
 
506
+ if results_path is None:
507
+ results_path = get_settings().default_browse_path or "results"
508
+
494
509
  # - only resolve local paths; cloud URIs (s3://, gs://, az://) are passed as-is
495
510
  if not is_cloud_path(results_path):
496
511
  results_path = os.path.abspath(os.path.expanduser(results_path))
@@ -755,5 +770,87 @@ def backtests(storage_path: str, where: str | None, order_by: str, limit: int |
755
770
  bs.print(where=where, order_by=order_by, limit=limit, params=params)
756
771
 
757
772
 
773
+ @main.group()
774
+ def s3():
775
+ """
776
+ Operate on S3-compatible storage using named accounts.
777
+
778
+ Paths use the format: account:bucket/path
779
+
780
+ Examples:\n
781
+ qubx s3 ls hetzner:frab/spreads/\n
782
+ qubx s3 rm hetzner:frab/spreads/ --recursive\n
783
+ qubx s3 cp hetzner:frab/spreads/data.parquet ./local_copy.parquet
784
+ """
785
+ pass
786
+
787
+
788
+ @s3.command("accounts")
789
+ def s3_accounts_cmd():
790
+ """List configured S3 accounts."""
791
+ from .s3 import s3_accounts
792
+
793
+ s3_accounts()
794
+
795
+
796
+ @s3.command("ls")
797
+ @click.argument("path", type=str)
798
+ @click.option("--recursive", "-r", is_flag=True, help="List files recursively.")
799
+ @click.option("--long", "-l", is_flag=True, help="Show detailed info (size, type).")
800
+ def s3_ls_cmd(path: str, recursive: bool, long: bool):
801
+ """
802
+ List files in an S3 path.
803
+
804
+ Examples:\n
805
+ qubx s3 ls hetzner:frab/\n
806
+ qubx s3 ls hetzner:frab/spreads/ -r -l
807
+ """
808
+ from .s3 import s3_ls
809
+
810
+ s3_ls(path, recursive=recursive, long=long)
811
+
812
+
813
+ @s3.command("rm")
814
+ @click.argument("path", type=str)
815
+ @click.option("--recursive", "-r", is_flag=True, help="Delete recursively.")
816
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
817
+ def s3_rm_cmd(path: str, recursive: bool, yes: bool):
818
+ """
819
+ Delete files from S3.
820
+
821
+ Examples:\n
822
+ qubx s3 rm hetzner:frab/spreads/long_exchange=BINANCE.UM/asset=BTC/data.parquet\n
823
+ qubx s3 rm hetzner:frab/spreads/ -r
824
+ """
825
+ if not yes:
826
+ label = f"{path} (recursive)" if recursive else path
827
+ click.confirm(f"Delete {label}?", abort=True)
828
+
829
+ from .s3 import s3_rm
830
+
831
+ s3_rm(path, recursive=recursive)
832
+
833
+
834
+ @s3.command("cp")
835
+ @click.argument("src", type=str)
836
+ @click.argument("dst", type=str)
837
+ @click.option("--recursive", "-r", is_flag=True, help="Copy recursively.")
838
+ def s3_cp_cmd(src: str, dst: str, recursive: bool):
839
+ """
840
+ Copy files to/from S3.
841
+
842
+ At least one of SRC or DST must be an S3 path (account:bucket/path).
843
+ The other can be a local path for upload/download.
844
+
845
+ Examples:\n
846
+ qubx s3 cp hetzner:frab/data.parquet ./local.parquet\n
847
+ qubx s3 cp ./local.parquet hetzner:frab/data.parquet\n
848
+ qubx s3 cp hetzner:frab/spreads/ ./spreads/ -r
849
+ """
850
+ from .s3 import s3_cp
851
+
852
+ s3_cp(src, dst, recursive=recursive)
853
+
854
+
758
855
  if __name__ == "__main__":
759
856
  main()
@@ -0,0 +1,124 @@
1
+ """S3 storage operations using named accounts from Qubx settings.
2
+
3
+ Uses :class:`qubx.utils.s3.S3Client` for all operations.
4
+ """
5
+
6
+ import click
7
+ from pyarrow.fs import FileType, LocalFileSystem, copy_files
8
+
9
+ from qubx.config import get_settings
10
+ from qubx.utils.s3 import S3Client, parse_uri
11
+
12
+
13
+ def _human_size(nbytes: int) -> str:
14
+ for unit in ("B", "KB", "MB", "GB", "TB"):
15
+ if abs(nbytes) < 1024:
16
+ return f"{nbytes:6.1f} {unit}"
17
+ nbytes /= 1024 # type: ignore[assignment]
18
+ return f"{nbytes:.1f} PB"
19
+
20
+
21
+ def s3_ls(path: str, recursive: bool = False, long: bool = False) -> None:
22
+ try:
23
+ client, s3_path = S3Client.from_uri(path)
24
+ except ValueError as e:
25
+ raise click.BadParameter(str(e))
26
+
27
+ try:
28
+ entries = client.ls(s3_path, recursive=recursive)
29
+ except Exception as e:
30
+ click.echo(click.style(f"Error listing {s3_path}: {e}", fg="red"), err=True)
31
+ raise click.Abort()
32
+
33
+ if not entries:
34
+ click.echo(click.style(f"No files found under: {s3_path}", fg="yellow"))
35
+ return
36
+
37
+ for info in sorted(entries, key=lambda e: e.path):
38
+ if long:
39
+ size = _human_size(info.size) if info.type == FileType.File else " -"
40
+ kind = "DIR " if info.type == FileType.Directory else "FILE"
41
+ click.echo(f" {kind} {size} {info.path}")
42
+ else:
43
+ click.echo(f" {info.path}")
44
+
45
+
46
+ def s3_rm(path: str, recursive: bool = False) -> None:
47
+ try:
48
+ client, s3_path = S3Client.from_uri(path)
49
+ except ValueError as e:
50
+ raise click.BadParameter(str(e))
51
+
52
+ try:
53
+ if recursive:
54
+ entries = client.ls(s3_path, recursive=True)
55
+ files = [e for e in entries if e.type == FileType.File]
56
+ if not files:
57
+ click.echo(click.style(f"No files found under: {s3_path}", fg="yellow"))
58
+ return
59
+ click.echo(f"Deleting {len(files)} file(s) under {s3_path} ...")
60
+ client.rm(s3_path, recursive=recursive)
61
+ click.echo(click.style(f"✓ Deleted {s3_path}", fg="green"))
62
+ except Exception as e:
63
+ click.echo(click.style(f"Error deleting {s3_path}: {e}", fg="red"), err=True)
64
+ raise click.Abort()
65
+
66
+
67
+ def s3_cp(src: str, dst: str, recursive: bool = False) -> None:
68
+ src_is_s3 = ":" in src
69
+ dst_is_s3 = ":" in dst
70
+
71
+ if src_is_s3 and dst_is_s3:
72
+ try:
73
+ src_account, src_path = parse_uri(src)
74
+ dst_account, dst_path = parse_uri(dst)
75
+ except ValueError as e:
76
+ raise click.BadParameter(str(e))
77
+
78
+ if src_account != dst_account:
79
+ click.echo(
80
+ click.style("Cross-account copy is not supported yet.", fg="red"),
81
+ err=True,
82
+ )
83
+ raise click.Abort()
84
+ client = S3Client(account=src_account)
85
+ client.copy_s3(src_path, dst_path, recursive=recursive)
86
+ click.echo(click.style(f"✓ Copied {src_path} → {dst_path}", fg="green"))
87
+
88
+ elif src_is_s3 and not dst_is_s3:
89
+ try:
90
+ client, src_path = S3Client.from_uri(src)
91
+ except ValueError as e:
92
+ raise click.BadParameter(str(e))
93
+ copy_files(src_path, dst, source_filesystem=client.fs, destination_filesystem=LocalFileSystem())
94
+ click.echo(click.style(f"✓ Downloaded {src_path} → {dst}", fg="green"))
95
+
96
+ elif not src_is_s3 and dst_is_s3:
97
+ try:
98
+ client, dst_path = S3Client.from_uri(dst)
99
+ except ValueError as e:
100
+ raise click.BadParameter(str(e))
101
+ copy_files(src, dst_path, source_filesystem=LocalFileSystem(), destination_filesystem=client.fs)
102
+ click.echo(click.style(f"✓ Uploaded {src} → {dst_path}", fg="green"))
103
+
104
+ else:
105
+ click.echo(
106
+ click.style("At least one path must be an S3 path (account:bucket/path).", fg="red"),
107
+ err=True,
108
+ )
109
+ raise click.Abort()
110
+
111
+
112
+ def s3_accounts() -> None:
113
+ s = get_settings()
114
+ if not s.s3:
115
+ click.echo("No S3 accounts configured.")
116
+ return
117
+
118
+ click.echo(click.style("Configured S3 accounts:", fg="cyan", bold=True))
119
+ for name, acct in s.s3.items():
120
+ default_marker = " (default)" if name == s.default_s3_account else ""
121
+ click.echo(f" {click.style(name, fg='yellow')}{default_marker}")
122
+ click.echo(f" endpoint: {acct.endpoint_url}")
123
+ if acct.region:
124
+ click.echo(f" region: {acct.region}")
@@ -10,6 +10,8 @@ from textual import on, work
10
10
  from textual.app import App, ComposeResult
11
11
  from textual.binding import Binding
12
12
  from textual.containers import Horizontal, Vertical
13
+ from textual.coordinate import Coordinate
14
+ from textual.timer import Timer
13
15
  from textual.widgets import (
14
16
  Button,
15
17
  DataTable,
@@ -699,6 +701,7 @@ class BacktestBrowserApp(App):
699
701
  DARK = True
700
702
 
701
703
  BINDINGS = [
704
+ Binding("q", "quit", "Quit", show=True),
702
705
  Binding("j", "vim_down", "Down", show=False),
703
706
  Binding("k", "vim_up", "Up", show=False),
704
707
  Binding("g", "vim_home", "Top", show=False),
@@ -707,6 +710,7 @@ class BacktestBrowserApp(App):
707
710
  Binding("S", "sort_by_key_sharpe", "Sort Sharpe", show=True),
708
711
  Binding("C", "sort_by_key_cagr", "Sort CAGR", show=True),
709
712
  Binding("G", "vim_end", "Sort Gain", show=True), # - Tree: scroll end; Table: sort gain
713
+ Binding("T", "sort_by_key_created", "Sort Created", show=True),
710
714
  ]
711
715
 
712
716
  CSS = """
@@ -865,6 +869,9 @@ class BacktestBrowserApp(App):
865
869
  self._selected_result: dict[str, Any] | None = None
866
870
  # - tracks which node is currently being loaded to discard stale callbacks
867
871
  self._loading_node: BacktestTreeNode | None = None
872
+ # - cc (copy path) state machine
873
+ self._pending_copy: bool = False
874
+ self._pending_copy_timer: Timer | None = None
868
875
 
869
876
  def compose(self) -> ComposeResult:
870
877
  """Compose the TUI layout"""
@@ -880,6 +887,7 @@ class BacktestBrowserApp(App):
880
887
  yield Button("Sort by Sharpe", id="sort-sharpe")
881
888
  yield Button("Sort by CAGR", id="sort-cagr")
882
889
  yield Button("Sort by Gain", id="sort-gain")
890
+ yield Button("Sort by Created", id="sort-created")
883
891
  yield Button("Open HTML", id="open-html")
884
892
  yield Button("Refresh", id="refresh")
885
893
 
@@ -910,7 +918,7 @@ class BacktestBrowserApp(App):
910
918
  if node_data.backtest_results:
911
919
  # - already loaded: show immediately (cached after first click)
912
920
  self.current_results = node_data.backtest_results
913
- self._update_table()
921
+ self._sort_table("creation_time")
914
922
  self.query_one("#metrics-table", MetricsTable).focus()
915
923
 
916
924
  elif node_data.pending_ids:
@@ -1011,7 +1019,7 @@ class BacktestBrowserApp(App):
1011
1019
 
1012
1020
  self.current_results = node_data.backtest_results
1013
1021
  if self.current_results:
1014
- self._update_table()
1022
+ self._sort_table("creation_time")
1015
1023
  table.focus()
1016
1024
  else:
1017
1025
  self.call_later(
@@ -1034,6 +1042,11 @@ class BacktestBrowserApp(App):
1034
1042
  """Sort results by total gain"""
1035
1043
  self._sort_table("gain")
1036
1044
 
1045
+ @on(Button.Pressed, "#sort-created")
1046
+ def sort_by_created(self) -> None:
1047
+ """Sort results by creation time"""
1048
+ self._sort_table("creation_time")
1049
+
1037
1050
  @on(Button.Pressed, "#open-html")
1038
1051
  def open_html_tearsheet(self) -> None:
1039
1052
  """Open full HTML tearsheet in browser for the selected result"""
@@ -1071,7 +1084,14 @@ class BacktestBrowserApp(App):
1071
1084
  ``_fv`` substitutes 0.0 for NaN / None so those rows sink to the bottom.
1072
1085
  """
1073
1086
  if self.current_results:
1074
- self.current_results.sort(key=lambda x: _fv(x.get(metric)), reverse=True)
1087
+ if metric == "creation_time":
1088
+ # - timestamps cannot go through _fv (float coercion); use pd.Timestamp
1089
+ self.current_results.sort(
1090
+ key=lambda x: pd.Timestamp(x.get("creation_time") or "1970-01-01"),
1091
+ reverse=True,
1092
+ )
1093
+ else:
1094
+ self.current_results.sort(key=lambda x: _fv(x.get(metric)), reverse=True)
1075
1095
  self._update_table()
1076
1096
 
1077
1097
  def _update_table(self) -> None:
@@ -1163,6 +1183,68 @@ class BacktestBrowserApp(App):
1163
1183
  if not isinstance(self.focused, Tree):
1164
1184
  self._sort_table("cagr")
1165
1185
 
1186
+ def action_sort_by_key_created(self) -> None:
1187
+ """Sort table by creation time (T — right pane only)."""
1188
+ if not isinstance(self.focused, Tree):
1189
+ self._sort_table("creation_time")
1190
+
1191
+ # ── cc copy path ─────────────────────────────────────────────────────────
1192
+
1193
+ def on_key(self, event) -> None:
1194
+ """Handle key events for multi-key bindings (cc = copy path to clipboard)."""
1195
+ if event.key == "c":
1196
+ if not isinstance(self.focused, DataTable):
1197
+ # - only intercept c when DataTable is focused
1198
+ return
1199
+ if self._pending_copy:
1200
+ # - second 'c' within timeout: complete the copy
1201
+ self._pending_copy = False
1202
+ if self._pending_copy_timer:
1203
+ self._pending_copy_timer.stop()
1204
+ self._pending_copy_timer = None
1205
+ self._copy_current_path()
1206
+ event.prevent_default()
1207
+ event.stop()
1208
+ else:
1209
+ # - first 'c': enter pending state, start timeout
1210
+ self._pending_copy = True
1211
+ self._pending_copy_timer = self.set_timer(
1212
+ 0.5, self._cancel_pending_copy, name="cc_timeout"
1213
+ )
1214
+ event.prevent_default()
1215
+ event.stop()
1216
+ elif self._pending_copy:
1217
+ # - any other key cancels the pending copy
1218
+ self._cancel_pending_copy()
1219
+
1220
+ def _cancel_pending_copy(self) -> None:
1221
+ """Cancel the pending cc copy operation (timeout or different key)."""
1222
+ self._pending_copy = False
1223
+ if self._pending_copy_timer:
1224
+ self._pending_copy_timer.stop()
1225
+ self._pending_copy_timer = None
1226
+
1227
+ def _copy_current_path(self) -> None:
1228
+ """Copy the path of the currently highlighted table row to clipboard via OSC 52."""
1229
+ table = self.query_one("#metrics-table", MetricsTable)
1230
+ if table.row_count == 0:
1231
+ return
1232
+
1233
+ try:
1234
+ cursor_row = table.cursor_row
1235
+ cell_key = table.coordinate_to_cell_key(Coordinate(cursor_row, 0))
1236
+ load_id = cell_key.row_key.value
1237
+ except Exception:
1238
+ self.notify("No row selected", severity="warning", timeout=2)
1239
+ return
1240
+
1241
+ if not load_id:
1242
+ return
1243
+
1244
+ # - load_id is the backtest_id = relative path from storage root
1245
+ self.copy_to_clipboard(str(load_id))
1246
+ self.notify(f"Copied: {load_id}", timeout=3)
1247
+
1166
1248
  # ── tearsheet ────────────────────────────────────────────────────────────
1167
1249
 
1168
1250
  def _generate_html_tearsheet(self, result_info: dict[str, Any]) -> None:
@@ -93,6 +93,7 @@ class QubxSettings(BaseSettings):
93
93
  metrics_port: int | None = None
94
94
  health_port: int | None = None
95
95
  log_format: str = "text" # "text" or "json"
96
+ default_browse_path: str | None = None
96
97
 
97
98
  model_config = {"env_prefix": "QUBX_", "env_nested_delimiter": "__"}
98
99
 
@@ -148,7 +148,11 @@ class ITradeDataExport:
148
148
  pass
149
149
 
150
150
  def export_position_changes(
151
- self, time: dt_64, instrument: Instrument, price: float, account: "IAccountViewer",
151
+ self,
152
+ time: dt_64,
153
+ instrument: Instrument,
154
+ price: float,
155
+ account: "IAccountViewer",
152
156
  metadata: dict[str, Any] | None = None,
153
157
  ) -> None:
154
158
  """
@@ -137,7 +137,7 @@ class PortfolioLogger(PositionsDumper):
137
137
  "exchange": i.exchange,
138
138
  "market_type": i.market_type,
139
139
  "pnl_quoted": p.pnl,
140
- "quantity": p.quantity,
140
+ "quantity": p.quantity * i.quantity_multiplier,
141
141
  "realized_pnl_quoted": p.r_pnl,
142
142
  "avg_position_price": p.position_avg_price if p.quantity != 0.0 else 0.0,
143
143
  "current_price": p.last_update_price,