Qubx 1.1.2.dev1__tar.gz → 1.1.2.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 (295) hide show
  1. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/PKG-INFO +1 -1
  2. qubx-1.1.2.dev3/src/qubx/_version.py +24 -0
  3. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/management.py +29 -4
  4. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/tui.py +35 -5
  5. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/okx/account.py +51 -1
  6. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/basics.py +11 -3
  7. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/metrics.py +91 -8
  8. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/cache.py +4 -2
  9. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/questdb.py +4 -0
  10. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/transformers.py +1 -1
  11. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/app.py +26 -2
  12. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/handlers.py +8 -3
  13. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/init_code.py +16 -0
  14. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/styles.tcss +15 -0
  15. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/__init__.py +2 -1
  16. qubx-1.1.2.dev3/src/qubx/utils/runner/textual/widgets/account_summary.py +47 -0
  17. qubx-1.1.2.dev1/src/qubx/_version.py +0 -34
  18. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/.gitignore +0 -0
  19. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/LICENSE +0 -0
  20. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/README.md +0 -0
  21. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/pyproject.toml +0 -0
  22. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/scripts/build.py +0 -0
  23. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/__init__.py +0 -0
  24. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/_nb_magic.py +0 -0
  25. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/__init__.py +0 -0
  26. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/account.py +0 -0
  27. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/broker.py +0 -0
  28. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/data.py +0 -0
  29. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/iteratedstream.py +0 -0
  30. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/ome.py +0 -0
  31. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/optimization.py +0 -0
  32. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/runner.py +0 -0
  33. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/sentinels.py +0 -0
  34. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/simulated_data.py +0 -0
  35. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/simulated_exchange.py +0 -0
  36. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/simulator.py +0 -0
  37. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/transfers.py +0 -0
  38. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/backtester/utils.py +0 -0
  39. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/__init__.py +0 -0
  40. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/commands.py +0 -0
  41. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/deploy.py +0 -0
  42. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/misc.py +0 -0
  43. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/release.py +0 -0
  44. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/resolver.py +0 -0
  45. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/s3.py +0 -0
  46. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/theme.py +0 -0
  47. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/cli/user_config.py +0 -0
  48. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/config.py +0 -0
  49. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/__init__.py +0 -0
  50. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/__init__.py +0 -0
  51. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/account.py +0 -0
  52. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  53. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  54. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/broker.py +0 -0
  55. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  56. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/data.py +0 -0
  57. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  58. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  59. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  60. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  61. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  62. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  63. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  64. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  65. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  66. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  67. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  68. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  69. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  70. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  71. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  72. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
  73. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/okx/broker.py +0 -0
  74. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
  75. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/factory.py +0 -0
  76. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  77. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  78. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  79. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  80. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  81. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  82. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  83. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  84. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  85. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  86. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  87. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  88. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  89. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/utils.py +0 -0
  90. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  91. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/registry.py +0 -0
  92. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/tardis/data.py +0 -0
  93. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/connectors/tardis/utils.py +0 -0
  94. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/__init__.py +0 -0
  95. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/account.py +0 -0
  96. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/context.py +0 -0
  97. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/detectors/__init__.py +0 -0
  98. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/detectors/delisting.py +0 -0
  99. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/detectors/stale.py +0 -0
  100. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/errors.py +0 -0
  101. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/exceptions.py +0 -0
  102. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/helpers.py +0 -0
  103. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/initializer.py +0 -0
  104. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/interfaces.py +0 -0
  105. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/loggers.py +0 -0
  106. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/lookups.py +0 -0
  107. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/__init__.py +0 -0
  108. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/market.py +0 -0
  109. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/processing.py +0 -0
  110. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/subscription.py +0 -0
  111. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/trading.py +0 -0
  112. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/universe.py +2 -2
  113. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/mixins/utils.py +0 -0
  114. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/series.pxd +0 -0
  115. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/series.pyi +0 -0
  116. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/series.pyx +0 -0
  117. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/utils.pyi +0 -0
  118. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/core/utils.pyx +0 -0
  119. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/__init__.py +0 -0
  120. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/containers.py +0 -0
  121. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/guards.py +0 -0
  122. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/registry.py +0 -0
  123. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storage.py +0 -0
  124. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/ccxt.py +0 -0
  125. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/csv.py +0 -0
  126. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/handy.py +0 -0
  127. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/multi.py +0 -0
  128. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/stub.py +0 -0
  129. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/data/storages/utils.py +0 -0
  130. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/__init__.py +0 -0
  131. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/base.py +0 -0
  132. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/composite.py +0 -0
  133. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/csv.py +0 -0
  134. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/indicator.py +0 -0
  135. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/inmemory.py +0 -0
  136. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/prometheus.py +0 -0
  137. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/emitters/questdb.py +0 -0
  138. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/__init__.py +0 -0
  139. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/composite.py +0 -0
  140. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/__init__.py +0 -0
  141. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/base.py +0 -0
  142. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/incremental.py +0 -0
  143. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/slack.py +0 -0
  144. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/formatters/target_position.py +0 -0
  145. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/redis_streams.py +0 -0
  146. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/exporters/slack.py +0 -0
  147. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/gathering/simplest.py +0 -0
  148. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/health/__init__.py +0 -0
  149. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/health/base.py +0 -0
  150. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/health/dummy.py +0 -0
  151. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/health/server.py +0 -0
  152. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/__init__.py +0 -0
  153. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/csv.py +0 -0
  154. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/factory.py +0 -0
  155. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/inmemory.py +0 -0
  156. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/loggers/mongo.py +0 -0
  157. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/notifications/__init__.py +0 -0
  158. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/notifications/composite.py +0 -0
  159. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/notifications/slack.py +0 -0
  160. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/notifications/throttler.py +0 -0
  161. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/pandaz/__init__.py +0 -0
  162. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/pandaz/stats.py +0 -0
  163. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/pandaz/ta.py +0 -0
  164. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/pandaz/utils.py +0 -0
  165. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/plugins/__init__.py +0 -0
  166. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/plugins/loader.py +0 -0
  167. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/_build.py +0 -0
  168. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/crypto-fees.ini +0 -0
  169. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  170. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  171. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  172. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  173. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  174. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  175. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  176. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  177. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  178. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  179. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  180. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restarts/__init__.py +0 -0
  181. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restarts/state_resolvers.py +0 -0
  182. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restarts/time_finders.py +0 -0
  183. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/__init__.py +0 -0
  184. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/balance.py +0 -0
  185. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/factory.py +0 -0
  186. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/interfaces.py +0 -0
  187. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/position.py +0 -0
  188. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/signal.py +0 -0
  189. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/state.py +0 -0
  190. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/restorers/utils.py +0 -0
  191. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/state/__init__.py +0 -0
  192. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/state/dummy.py +0 -0
  193. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/state/redis.py +0 -0
  194. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/ta/__init__.py +0 -0
  195. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/ta/indicators.pxd +0 -0
  196. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/ta/indicators.pyi +0 -0
  197. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/ta/indicators.pyx +0 -0
  198. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/__init__.py +0 -0
  199. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/base.py +0 -0
  200. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  201. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/config.yml.j2 +0 -0
  202. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  203. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  204. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  205. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  206. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  207. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/project/template.yml +0 -0
  208. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
  209. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
  210. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
  211. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
  212. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
  213. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.gitignore.j2 +0 -0
  214. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.python-version +0 -0
  215. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.vscode/launch.json +0 -0
  216. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.vscode/settings.json +0 -0
  217. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
  218. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
  219. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/README.md.j2 +0 -0
  220. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/configs/.gitkeep +0 -0
  221. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/justfile.j2 +0 -0
  222. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
  223. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/research/.gitkeep +0 -0
  224. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  225. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
  226. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  227. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/template.yml +0 -0
  228. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
  229. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  230. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  231. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/config.yml.j2 +0 -0
  232. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  233. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  234. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  235. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/templates/simple/template.yml +0 -0
  236. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/__init__.py +0 -0
  237. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/advanced.py +0 -0
  238. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/composite.py +0 -0
  239. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/rebalancers.py +0 -0
  240. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/riskctrl.py +0 -0
  241. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/trackers/sizers.py +0 -0
  242. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/__init__.py +0 -0
  243. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/_pyxreloader.py +0 -0
  244. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/charting/lookinglass.py +0 -0
  245. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  246. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/charting/orderbook.py +0 -0
  247. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/collections.py +0 -0
  248. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/hft/__init__.py +0 -0
  249. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/hft/numba_utils.py +0 -0
  250. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/hft/orderbook.pyi +0 -0
  251. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/hft/orderbook.pyx +0 -0
  252. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/marketdata/binance.py +0 -0
  253. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/marketdata/ccxt.py +0 -0
  254. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/marketdata/dukas.py +0 -0
  255. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/misc.py +0 -0
  256. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/nonce.py +0 -0
  257. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/ntp.py +0 -0
  258. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/numbers_utils.py +0 -0
  259. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/orderbook.py +0 -0
  260. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/__init__.py +0 -0
  261. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/dashboard.py +0 -0
  262. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/data.py +0 -0
  263. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/interfaces.py +0 -0
  264. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  265. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  266. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/questdb.py +0 -0
  267. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/rate_limiter.py +0 -0
  268. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/results.py +0 -0
  269. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/ringbuffer.pxd +0 -0
  270. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/ringbuffer.pyi +0 -0
  271. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/ringbuffer.pyx +0 -0
  272. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/__init__.py +0 -0
  273. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  274. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/accounts.py +0 -0
  275. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/configs.py +0 -0
  276. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/factory.py +0 -0
  277. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/kernel_service.py +0 -0
  278. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/runner.py +0 -0
  279. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/__init__.py +0 -0
  280. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/kernel.py +0 -0
  281. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  282. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  283. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  284. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  285. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  286. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  287. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/s3.py +0 -0
  288. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/slack.py +0 -0
  289. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/throttler.py +0 -0
  290. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/time.py +0 -0
  291. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/src/qubx/utils/websocket_manager.py +0 -0
  292. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  293. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  294. {qubx-1.1.2.dev1 → qubx-1.1.2.dev3}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  295. {qubx-1.1.2.dev1 → qubx-1.1.2.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.1.2.dev1
3
+ Version: 1.1.2.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
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.1.2.dev3'
22
+ __version_tuple__ = version_tuple = (1, 1, 2, 'dev3')
23
+
24
+ __commit_id__ = commit_id = None
@@ -573,7 +573,7 @@ class BacktestStorage:
573
573
  if self._is_cloud:
574
574
  self._setup_cloud_duckdb()
575
575
 
576
- def get_log(self, backtest_id: str, config_name: str | None = None) -> str | None:
576
+ def get_log(self, backtest_id: str, config_name: str | None = None, max_lines: int = 1000) -> tuple[str, bool] | None:
577
577
  """
578
578
  Read the log file for a backtest run.
579
579
 
@@ -581,14 +581,29 @@ class BacktestStorage:
581
581
  backtest_id: Relative path within base_path.
582
582
  config_name: Config name (used for log filename).
583
583
  If None, searches for any .log file.
584
+ max_lines: Maximum number of lines to return (from the end). 0 = unlimited.
584
585
 
585
586
  Returns:
586
- Log file content as string, or None if not found.
587
+ (log_content, truncated) tuple, or None if not found.
587
588
  """
588
589
  run_dir = f"{self.base_path}{backtest_id.strip('/')}/"
589
590
 
590
591
  if config_name:
591
592
  log_path = f"{run_dir}{config_name}.log"
593
+ elif self._is_cloud:
594
+ try:
595
+ from pyarrow.fs import FileSelector, FileType
596
+
597
+ from qubx.utils.s3 import strip_scheme
598
+
599
+ client = S3Client(storage_options=self._storage_options)
600
+ entries = client.fs.get_file_info(FileSelector(strip_scheme(run_dir)))
601
+ log_files = [e.path for e in entries if e.type == FileType.File and e.path.endswith(".log")]
602
+ if not log_files:
603
+ return None
604
+ log_path = f"s3://{log_files[0]}"
605
+ except Exception:
606
+ return None
592
607
  else:
593
608
  try:
594
609
  files_df = self._conn.execute(f"SELECT file FROM glob('{run_dir}*.log')").df()
@@ -598,20 +613,30 @@ class BacktestStorage:
598
613
  except Exception:
599
614
  return None
600
615
 
616
+ def _tail(content: str) -> tuple[str, bool]:
617
+ if not max_lines:
618
+ return content, False
619
+ lines = content.splitlines()
620
+ if len(lines) <= max_lines:
621
+ return content, False
622
+ return "\n".join(lines[-max_lines:]), True
623
+
601
624
  if self._is_cloud:
602
625
  try:
603
626
  from qubx.utils.s3 import strip_scheme
604
627
 
605
628
  client = S3Client(storage_options=self._storage_options)
606
629
  with client.fs.open_input_stream(strip_scheme(log_path)) as f:
607
- return f.read().decode("utf-8")
630
+ return _tail(f.read().decode("utf-8"))
608
631
  except Exception:
609
632
  return None
610
633
  else:
611
634
  from pathlib import Path
612
635
 
613
636
  p = Path(log_path)
614
- return p.read_text(encoding="utf-8", errors="replace") if p.is_file() else None
637
+ if not p.is_file():
638
+ return None
639
+ return _tail(p.read_text(encoding="utf-8", errors="replace"))
615
640
 
616
641
  def export_backtests_to_markdown(self, backtest_id: str, path: str, tags: tuple[str] | None = None):
617
642
  if tsr := self.load(backtest_id):
@@ -844,6 +844,8 @@ class DetailScreen(Screen):
844
844
  self._result_info = result_info
845
845
  self._storage = storage
846
846
  self._load_id = result_info.get("load_id", "")
847
+ self._pending_copy: bool = False
848
+ self._pending_copy_timer: Timer | None = None
847
849
 
848
850
  def compose(self) -> ComposeResult:
849
851
  name = self._result_info.get("name", self._load_id)
@@ -854,7 +856,7 @@ class DetailScreen(Screen):
854
856
  with VerticalScroll(id="detail-scroll"):
855
857
  yield Markdown("*Loading…*", id="detail-report")
856
858
  with TabPane("Logs", id="tab-logs"):
857
- yield RichLog(id="detail-log", wrap=True, highlight=True)
859
+ yield RichLog(id="detail-log", wrap=True, highlight=True, markup=True)
858
860
  yield Footer()
859
861
 
860
862
  def on_mount(self) -> None:
@@ -875,16 +877,19 @@ class DetailScreen(Screen):
875
877
 
876
878
  # - load log file
877
879
  config_name = self._result_info.get("config_name")
878
- log_content = self._storage.get_log(self._load_id, config_name)
880
+ log_result = self._storage.get_log(self._load_id, config_name)
879
881
 
880
- self.app.call_from_thread(self._populate, report, log_content)
882
+ self.app.call_from_thread(self._populate, report, log_result)
881
883
 
882
- def _populate(self, report: str, log_content: str | None) -> None:
884
+ def _populate(self, report: str, log_result: tuple[str, bool] | None) -> None:
883
885
  """UI-thread callback: fill in the tabs."""
884
886
  self.query_one("#detail-report", Markdown).update(report)
885
887
 
886
888
  log_widget = self.query_one("#detail-log", RichLog)
887
- if log_content:
889
+ if log_result:
890
+ log_content, truncated = log_result
891
+ if truncated:
892
+ log_widget.write("[bold yellow]Log truncated — showing last 1000 lines[/]\n")
888
893
  for line in log_content.splitlines():
889
894
  log_widget.write(line)
890
895
  else:
@@ -910,6 +915,31 @@ class DetailScreen(Screen):
910
915
  def action_go_back(self) -> None:
911
916
  self.app.pop_screen()
912
917
 
918
+ def on_key(self, event) -> None:
919
+ if event.key == "c":
920
+ if self._pending_copy:
921
+ self._pending_copy = False
922
+ if self._pending_copy_timer:
923
+ self._pending_copy_timer.stop()
924
+ self._pending_copy_timer = None
925
+ self.app.copy_to_clipboard(str(self._load_id))
926
+ self.app.notify(f"Copied: {self._load_id}", timeout=3)
927
+ else:
928
+ self._pending_copy = True
929
+ self._pending_copy_timer = self.set_timer(
930
+ 0.5, self._cancel_pending_copy, name="cc_timeout"
931
+ )
932
+ event.prevent_default()
933
+ event.stop()
934
+ elif self._pending_copy:
935
+ self._cancel_pending_copy()
936
+
937
+ def _cancel_pending_copy(self) -> None:
938
+ self._pending_copy = False
939
+ if self._pending_copy_timer:
940
+ self._pending_copy_timer.stop()
941
+ self._pending_copy_timer = None
942
+
913
943
 
914
944
  class BacktestBrowserApp(App):
915
945
  """Main TUI application for browsing backtest results"""
@@ -7,7 +7,7 @@ from qubx.connectors.ccxt.utils import (
7
7
  ccxt_extract_deals_from_exec,
8
8
  ccxt_find_instrument,
9
9
  )
10
- from qubx.core.basics import CtrlChannel, Instrument
10
+ from qubx.core.basics import AssetBalance, CtrlChannel, Instrument
11
11
 
12
12
 
13
13
  class OkxAccountProcessor(CcxtAccountProcessor):
@@ -57,6 +57,56 @@ class OkxAccountProcessor(CcxtAccountProcessor):
57
57
  ),
58
58
  )
59
59
 
60
+ async def _update_balance(self) -> None:
61
+ """OKX balance override: use cashBal instead of eq (equity) to avoid double-counting unrealized PnL.
62
+
63
+ CCXT maps OKX's `eq` (equity = cashBal + unrealizedPnL) to balance `total`.
64
+ Since get_total_capital() adds position market values (= unrealized PnL) on top,
65
+ using `eq` would double-count unrealized PnL. We use `cashBal` instead.
66
+ """
67
+ balances_raw = await self.exchange_manager.exchange.fetch_balance()
68
+
69
+ # Extract cashBal from raw OKX response instead of CCXT-parsed eq
70
+ balances: list[AssetBalance] = []
71
+ raw_details = balances_raw.get("info", {}).get("data", [{}])[0].get("details", [])
72
+ for detail in raw_details:
73
+ cash_bal = float(detail.get("cashBal", 0) or 0)
74
+ if not cash_bal:
75
+ continue
76
+ frozen_bal = float(detail.get("frozenBal", 0) or 0)
77
+ currency = detail["ccy"]
78
+ balances.append(
79
+ AssetBalance(
80
+ exchange=self.exchange,
81
+ currency=currency,
82
+ free=cash_bal - frozen_bal,
83
+ locked=frozen_bal,
84
+ total=cash_bal,
85
+ )
86
+ )
87
+
88
+ current_balances = self.get_balances()
89
+
90
+ # remove balances that are not there anymore
91
+ current_currencies = {b.currency for b in current_balances}
92
+ new_currencies = {b.currency for b in balances}
93
+ for currency in current_currencies - new_currencies:
94
+ self.update_balance(currency, 0, 0)
95
+
96
+ # update current balances
97
+ for balance in balances:
98
+ self.update_balance(currency=balance.currency, total=balance.total, locked=balance.locked)
99
+
100
+ # update required instruments that we need to subscribe to
101
+ currencies = [b.currency for b in self.get_balances()]
102
+ instruments = [
103
+ self._get_instrument_for_currency(c) for c in currencies if c.upper() != self.base_currency.upper()
104
+ ]
105
+ self._required_instruments.update(instruments)
106
+
107
+ # fetch tickers for instruments that don't have recent price updates
108
+ await self._fetch_missing_tickers(instruments)
109
+
60
110
  async def _subscribe_instruments(self, instruments: list) -> None:
61
111
  """Skip SPOT instruments on OKX.F - futures connector only handles swap contracts."""
62
112
  await super()._subscribe_instruments([i for i in instruments if i.is_futures()])
@@ -936,7 +936,8 @@ class Position:
936
936
 
937
937
  if not np.isnan(price):
938
938
  u_pnl = self.unrealized_pnl()
939
- self.pnl = u_pnl + self.r_pnl + self.cumulative_funding
939
+ # r_pnl already includes cumulative funding
940
+ self.pnl = u_pnl + self.r_pnl
940
941
  if self.instrument.is_futures():
941
942
  # for derivatives market value of the position is the current unrealized PnL
942
943
  self.market_value = u_pnl
@@ -955,7 +956,12 @@ class Position:
955
956
 
956
957
  def unrealized_pnl(self) -> float:
957
958
  if not np.isnan(self.last_update_price):
958
- return self.quantity * self._qty_multiplier * (self.last_update_price - self.position_avg_price) / self.last_update_conversion_rate # type: ignore
959
+ return (
960
+ self.quantity
961
+ * self._qty_multiplier
962
+ * (self.last_update_price - self.position_avg_price)
963
+ / self.last_update_conversion_rate
964
+ ) # type: ignore
959
965
  return 0.0
960
966
 
961
967
  def apply_funding_payment(self, funding_payment: FundingPayment, mark_price: float) -> float:
@@ -1021,7 +1027,9 @@ class Position:
1021
1027
  funds_release = self.market_value_funds
1022
1028
  if to_remain != 0 and self.quantity != 0 and np.sign(to_remain) == d:
1023
1029
  qty_to_release = max(self.quantity - to_remain, 0) if d > 0 else min(self.quantity - to_remain, 0)
1024
- funds_release = qty_to_release * self._qty_multiplier * self.last_update_price / self.last_update_conversion_rate
1030
+ funds_release = (
1031
+ qty_to_release * self._qty_multiplier * self.last_update_price / self.last_update_conversion_rate
1032
+ )
1025
1033
  return abs(funds_release)
1026
1034
 
1027
1035
  @staticmethod
@@ -685,6 +685,18 @@ class TradingSessionResult:
685
685
  return df
686
686
  return df.loc[start:stop]
687
687
 
688
+ def _rebase_cumulative_columns(pfl: pd.DataFrame) -> pd.DataFrame:
689
+ """Rebase cumulative counter columns (_Funding, _Commissions) so the slice starts from zero.
690
+ Note: _PnL is NOT rebased because it includes unrealized PnL which depends on current positions."""
691
+ if pfl is None or pfl.empty:
692
+ return pfl
693
+ pfl = pfl.copy()
694
+ for pattern in ["_Funding", "_Commissions"]:
695
+ cols = [c for c in pfl.columns if c.endswith(pattern)]
696
+ if cols:
697
+ pfl[cols] = pfl[cols] - pfl[cols].iloc[0]
698
+ return pfl
699
+
688
700
  start = pd.Timestamp(key.start) if key.start is not None else None
689
701
  stop = pd.Timestamp(key.stop) if key.stop is not None else None
690
702
 
@@ -696,6 +708,10 @@ class TradingSessionResult:
696
708
  else:
697
709
  capital = self.capital
698
710
 
711
+ sliced_portfolio = _slice_df(self.portfolio_log)
712
+ if start is not None and sliced_portfolio is not None and not sliced_portfolio.empty:
713
+ sliced_portfolio = _rebase_cumulative_columns(sliced_portfolio)
714
+
699
715
  tsr = TradingSessionResult(
700
716
  id=self.id,
701
717
  name=self.name,
@@ -706,7 +722,7 @@ class TradingSessionResult:
706
722
  capital=capital,
707
723
  base_currency=self.base_currency,
708
724
  commissions=self.commissions,
709
- portfolio_log=_slice_df(self.portfolio_log),
725
+ portfolio_log=sliced_portfolio,
710
726
  executions_log=_slice_df(self.executions_log),
711
727
  signals_log=_slice_df(self.signals_log),
712
728
  targets_log=_slice_df(self.targets_log),
@@ -719,9 +735,7 @@ class TradingSessionResult:
719
735
  emitter_data=_slice_df(self.emitter_data.set_index("timestamp")).reset_index()
720
736
  if self.emitter_data is not None
721
737
  else None,
722
- transfers_log=_slice_df(self.transfers_log.set_index("timestamp")).reset_index()
723
- if self.transfers_log is not None
724
- else None,
738
+ transfers_log=_slice_df(self.transfers_log) if self.transfers_log is not None else None,
725
739
  )
726
740
  tsr.qubx_version = self.qubx_version
727
741
  tsr.description = self.description
@@ -904,10 +918,75 @@ class TradingSessionResult:
904
918
  return pd.DataFrame(dtype=float)
905
919
  return calculate_funding_per_symbol(self.portfolio_log, start, stop)
906
920
 
907
- def get_funding_per_asset(self, start: OptTimestamp = None, stop: OptTimestamp = None) -> pd.DataFrame:
921
+ def get_funding_per_asset(
922
+ self, start: OptTimestamp = None, stop: OptTimestamp = None, filter_zero: bool = True
923
+ ) -> pd.DataFrame:
908
924
  if self.portfolio_log.empty:
909
925
  return pd.DataFrame(dtype=float)
910
- return calculate_funding_per_asset(self.portfolio_log, start, stop)
926
+ return calculate_funding_per_asset(self.portfolio_log, start, stop, filter_zero)
927
+
928
+ def get_funding_total(self, start: OptTimestamp = None, stop: OptTimestamp = None) -> pd.Series:
929
+ return self.get_funding_per_asset(start, stop).sum(axis=1).rename("funding_total")
930
+
931
+ def get_funding_pnl(self, start: OptTimestamp = None, stop: OptTimestamp = None) -> pd.Series:
932
+ return self.get_funding_per_asset(start, stop).iloc[-1].sort_values(ascending=False).rename("funding_pnl")
933
+
934
+ def get_asset_pnl(
935
+ self,
936
+ asset: str,
937
+ start: OptTimestamp = None,
938
+ stop: OptTimestamp = None,
939
+ commission_factor: float = 1.0,
940
+ pct_from_initial_capital: bool = False,
941
+ include_funding: bool = False,
942
+ ) -> pd.Series:
943
+ portfolio = self.portfolio_log
944
+ start = start or self.start
945
+ stop = stop or self.stop
946
+ init_capital = self.get_total_capital()
947
+ pnl = portfolio.filter(regex=f".*:{asset}USD.*_PnL").loc[start:stop].sum(axis=1).cumsum()
948
+ if not include_funding:
949
+ funding = portfolio.filter(regex=f".*:{asset}USD.*_Funding").loc[start:stop].sum(axis=1)
950
+ if len(funding) > 0:
951
+ funding_delta = funding.diff().fillna(funding.iloc[0])
952
+ pnl -= funding_delta.cumsum()
953
+ if commission_factor:
954
+ comm = portfolio.filter(regex=f".*{asset}.*_Commissions").loc[start:stop].sum(axis=1).cumsum()
955
+ pnl -= comm * commission_factor
956
+ return pnl / init_capital * 100 if pct_from_initial_capital else pnl
957
+
958
+ def get_pnl_per_asset(
959
+ self,
960
+ start: OptTimestamp = None,
961
+ stop: OptTimestamp = None,
962
+ pct_from_initial_capital: bool = False,
963
+ drop_zero: bool = True,
964
+ include_funding: bool = False,
965
+ ) -> pd.Series:
966
+ start = start or self.start
967
+ stop = stop or self.stop
968
+ assets = set()
969
+ for symbol in self.symbols:
970
+ parts = symbol.split(":")
971
+ if len(parts) != 2:
972
+ continue
973
+ exchange, symbol = parts
974
+ instrument = lookup.find_symbol(exchange, symbol)
975
+ if instrument:
976
+ assets.add(instrument.asset)
977
+ asset_to_pnl = {}
978
+ for asset in assets:
979
+ pnl = self.get_asset_pnl(asset, start, stop, pct_from_initial_capital=pct_from_initial_capital, include_funding=include_funding)
980
+ if pnl is not None and not pnl.empty:
981
+ last_pnl = pnl.iloc[-1]
982
+ if drop_zero and last_pnl == 0:
983
+ continue
984
+ asset_to_pnl[asset] = last_pnl
985
+ return pd.Series(asset_to_pnl).sort_values(ascending=False)
986
+
987
+ def get_position_asset(self, asset: str, start: OptTimestamp = None, stop: OptTimestamp = None) -> pd.DataFrame:
988
+ pos = self.portfolio_log.filter(regex=f".*{asset}.*_Pos")
989
+ return pos.rename(columns=lambda x: x.replace("_Pos", ""))
911
990
 
912
991
  def performance(self) -> dict[str, float]:
913
992
  """
@@ -2023,7 +2102,7 @@ def calculate_leverage_per_symbol(
2023
2102
  df.columns = [col.split(":")[-1] for col in df.columns]
2024
2103
 
2025
2104
  if drop_zero_leverage:
2026
- df = df.loc[:, df.gt(0).any(axis=0)]
2105
+ df = df.loc[:, df.ne(0).any(axis=0)]
2027
2106
 
2028
2107
  return cast(pd.DataFrame, df)
2029
2108
 
@@ -2105,6 +2184,7 @@ def calculate_funding_per_asset(
2105
2184
  portfolio_log: pd.DataFrame,
2106
2185
  start: OptTimestamp = None,
2107
2186
  stop: OptTimestamp = None,
2187
+ filter_zero: bool = True,
2108
2188
  ) -> pd.DataFrame:
2109
2189
  """
2110
2190
  Calculate funding for each asset in the trading session.
@@ -2120,7 +2200,10 @@ def calculate_funding_per_asset(
2120
2200
  col_to_asset[col] = instr.asset if instr else col
2121
2201
 
2122
2202
  # Group by asset and sum
2123
- return df.rename(columns=col_to_asset).T.groupby(level=0).sum().T
2203
+ df = df.rename(columns=col_to_asset).T.groupby(level=0).sum().T
2204
+ if filter_zero:
2205
+ df = df.loc[:, df.ne(0).any(axis=0)]
2206
+ return df
2124
2207
 
2125
2208
 
2126
2209
  def calculate_turnover(
@@ -270,16 +270,18 @@ def _merge_batches(existing: pa.RecordBatch, incoming: pa.RecordBatch, time_col_
270
270
  [
271
271
  pa.Table.from_batches([existing]),
272
272
  pa.Table.from_batches([incoming]),
273
- ]
273
+ ],
274
+ promote_options="permissive",
274
275
  )
275
276
  time_col_name = tbl.schema.field(time_col_idx).name
276
277
  tbl = tbl.sort_by(time_col_name)
277
278
 
279
+ merged_schema = tbl.schema
278
280
  pdf = tbl.to_pandas()
279
281
  dedup_cols = [time_col_name] + [f.name for f in existing.schema if pa.types.is_string(f.type) or pa.types.is_large_string(f.type)]
280
282
  pdf = pdf.drop_duplicates(subset=dedup_cols, keep="last")
281
283
 
282
- batch = pa.RecordBatch.from_pandas(pdf, schema=existing.schema, preserve_index=False)
284
+ batch = pa.RecordBatch.from_pandas(pdf, schema=merged_schema, preserve_index=False)
283
285
  return batch
284
286
 
285
287
 
@@ -318,6 +318,10 @@ class xLTableMetaInfo:
318
318
  ("binance", "cmfutures"): ("binance.cm", "future"),
319
319
  ("binance", "margin"): ("binance.pm", "swap"),
320
320
  ("gateio", "swap"): ("gateio.f", "swap"),
321
+ ("kraken", "swap"): ("kraken.f", "swap"),
322
+ ("hyperliquid", "swap"): ("hyperliquid.f", "swap"),
323
+ ("bybit", "swap"): ("bybit.f", "swap"),
324
+ ("okx", "swap"): ("okx.f", "swap"),
321
325
  }
322
326
 
323
327
  _DTYPE_FIX = {
@@ -119,7 +119,7 @@ class PandasFrame(IDataTransformer):
119
119
  tbl = tbl.append_column(pa.field("symbol", pa.large_string()), sym_col)
120
120
  tables.append(tbl)
121
121
 
122
- tbl = pa.concat_tables(tables)
122
+ tbl = pa.concat_tables(tables, promote_options="permissive")
123
123
  df = tbl.to_pandas()
124
124
  df[t_name] = pd.to_datetime(df[t_name])
125
125
  return df.set_index([t_name, "symbol"]).sort_index()
@@ -7,6 +7,7 @@ import concurrent.futures
7
7
  import sys
8
8
  from pathlib import Path
9
9
 
10
+ import yaml
10
11
  from rich.text import Text
11
12
  from textual.app import App, ComposeResult, ScreenStackError
12
13
  from textual.binding import Binding
@@ -20,7 +21,7 @@ from qubx.cli.theme import QUBX_DARK
20
21
  from .handlers import KernelEventHandler
21
22
  from .init_code import generate_init_code, generate_mock_init_code
22
23
  from .kernel import IPyKernel
23
- from .widgets import CommandInput, DebugLog, OrdersTable, PositionsTable, QuotesTable, ReplOutput
24
+ from .widgets import AccountSummary, CommandInput, DebugLog, OrdersTable, PositionsTable, QuotesTable, ReplOutput
24
25
 
25
26
 
26
27
  class TextualStrategyApp(App[None]):
@@ -80,8 +81,13 @@ class TextualStrategyApp(App[None]):
80
81
  self.no_notifiers = no_notifiers
81
82
  self.no_exporters = no_exporters
82
83
  self.kernel = kernel if kernel is not None else IPyKernel()
84
+
85
+ # Derive app title from config name or strategy class
86
+ self.title = self._resolve_title(config_file)
87
+
83
88
  self.output: ReplOutput
84
89
  self.input: CommandInput
90
+ self.account_summary: AccountSummary
85
91
  self.positions_panel: Vertical
86
92
  self.positions_table: PositionsTable
87
93
  self.orders_panel: Vertical
@@ -102,13 +108,29 @@ class TextualStrategyApp(App[None]):
102
108
  self._original_stderr = None
103
109
  self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
104
110
 
111
+ @staticmethod
112
+ def _resolve_title(config_file: Path) -> str:
113
+ """Derive window title from config 'name' field, falling back to strategy class name."""
114
+ try:
115
+ with open(config_file) as f:
116
+ cfg = yaml.safe_load(f) or {}
117
+ if name := cfg.get("name"):
118
+ return str(name)
119
+ if strategy := cfg.get("strategy"):
120
+ return str(strategy).rsplit(".", 1)[-1]
121
+ except Exception:
122
+ pass
123
+ return config_file.stem
124
+
105
125
  async def on_mount(self) -> None:
106
126
  """Initialize the app when mounted."""
107
127
  self.register_theme(QUBX_DARK)
108
128
  self.theme = QUBX_DARK.name
109
129
 
110
130
  # Setup event handler
111
- self.event_handler = KernelEventHandler(self.output, self.positions_table, self.orders_table, self.quotes_table)
131
+ self.event_handler = KernelEventHandler(
132
+ self.output, self.positions_table, self.orders_table, self.quotes_table, self.account_summary
133
+ )
112
134
 
113
135
  # Add welcome message
114
136
  self.output.write(Text("Qubx Strategy Runner", style="bold cyan"))
@@ -239,6 +261,8 @@ class TextualStrategyApp(App[None]):
239
261
  def compose(self) -> ComposeResult:
240
262
  """Compose the TUI layout."""
241
263
  yield Header(show_clock=True)
264
+ self.account_summary = AccountSummary(id="account-summary")
265
+ yield self.account_summary
242
266
  with Vertical(id="content-wrapper"):
243
267
  with Horizontal(id="main-container"):
244
268
  # Output on the left
@@ -9,7 +9,7 @@ from rich.text import Text
9
9
 
10
10
  from qubx import logger
11
11
 
12
- from .widgets import OrdersTable, PositionsTable, QuotesTable, ReplOutput
12
+ from .widgets import AccountSummary, OrdersTable, PositionsTable, QuotesTable, ReplOutput
13
13
 
14
14
 
15
15
  class KernelEventHandler:
@@ -21,6 +21,7 @@ class KernelEventHandler:
21
21
  positions_table: PositionsTable,
22
22
  orders_table: OrdersTable,
23
23
  quotes_table: QuotesTable,
24
+ account_summary: AccountSummary,
24
25
  ):
25
26
  """
26
27
  Initialize the event handler.
@@ -28,13 +29,15 @@ class KernelEventHandler:
28
29
  Args:
29
30
  output: REPL output widget for displaying text
30
31
  positions_table: Positions table widget for displaying positions data
31
- orders_table: Orders table widget for displaying orders data (optional)
32
- quotes_table: Quotes table widget for displaying market quotes (optional)
32
+ orders_table: Orders table widget for displaying orders data
33
+ quotes_table: Quotes table widget for displaying market quotes
34
+ account_summary: Account summary bar widget
33
35
  """
34
36
  self.output = output
35
37
  self.positions_table = positions_table
36
38
  self.orders_table = orders_table
37
39
  self.quotes_table = quotes_table
40
+ self.account_summary = account_summary
38
41
  self._dashboard_busy = False
39
42
  self._on_dashboard_update: Callable[[], None] | None = None
40
43
 
@@ -60,9 +63,11 @@ class KernelEventHandler:
60
63
  positions = payload.get("positions", [])
61
64
  orders = payload.get("orders", [])
62
65
  quotes = payload.get("quotes", {})
66
+ account_summary = payload.get("account_summary", [])
63
67
  self.positions_table.update_positions(positions)
64
68
  self.orders_table.update_orders(orders)
65
69
  self.quotes_table.update_quotes(quotes)
70
+ self.account_summary.update_summary(account_summary)
66
71
  except Exception as e:
67
72
  logger.error(f"Dashboard update error: {e}")
68
73
  self.output.write(f"Dashboard update failed: {e}")
@@ -210,6 +210,21 @@ def _quotes_as_records():
210
210
  pass # Context not ready yet
211
211
  return quotes
212
212
 
213
+ def _account_summary():
214
+ \"\"\"Collect per-exchange account summary (total capital, leverage).\"\"\"
215
+ summary = []
216
+ try:
217
+ for exch in ctx.exchanges:
218
+ summary.append({{
219
+ "exchange": exch,
220
+ "total_capital": _sanitize_number(round(ctx.get_total_capital(exch), 2)),
221
+ "net_leverage": _sanitize_number(round(ctx.get_net_leverage(exch), 4)),
222
+ "gross_leverage": _sanitize_number(round(ctx.get_gross_leverage(exch), 4)),
223
+ }})
224
+ except Exception:
225
+ pass
226
+ return summary
227
+
213
228
  def emit_dashboard(all=True, debug=False):
214
229
  \"\"\"Publish unified dashboard data via custom MIME for Textual to capture.\"\"\"
215
230
  try:
@@ -217,6 +232,7 @@ def emit_dashboard(all=True, debug=False):
217
232
  "positions": _positions_as_records(all=all),
218
233
  "orders": _orders_as_records(),
219
234
  "quotes": _quotes_as_records(),
235
+ "account_summary": _account_summary(),
220
236
  }}
221
237
 
222
238
  # Let strategy inject custom data
@@ -19,6 +19,21 @@ Header {
19
19
  text-style: bold;
20
20
  }
21
21
 
22
+ #account-summary {
23
+ height: auto;
24
+ background: $panel;
25
+ padding: 0 2;
26
+ }
27
+
28
+ #account-summary-bar {
29
+ height: auto;
30
+ }
31
+
32
+ #account-summary-content {
33
+ height: auto;
34
+ color: $foreground;
35
+ }
36
+
22
37
  Footer {
23
38
  dock: bottom;
24
39
  height: 1;
@@ -1,5 +1,6 @@
1
1
  """Textual UI widgets for strategy runner."""
2
2
 
3
+ from .account_summary import AccountSummary
3
4
  from .command_input import CommandInput
4
5
  from .debug_log import DebugLog, TextualLogHandler
5
6
  from .orders_table import OrdersTable
@@ -7,4 +8,4 @@ from .positions_table import PositionsTable
7
8
  from .quotes_table import QuotesTable
8
9
  from .repl_output import ReplOutput
9
10
 
10
- __all__ = ["ReplOutput", "PositionsTable", "OrdersTable", "QuotesTable", "CommandInput", "DebugLog", "TextualLogHandler"]
11
+ __all__ = ["AccountSummary", "ReplOutput", "PositionsTable", "OrdersTable", "QuotesTable", "CommandInput", "DebugLog", "TextualLogHandler"]