Qubx 1.0.0.dev6__tar.gz → 1.0.1.dev2__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 (269) hide show
  1. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/PKG-INFO +1 -1
  2. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/pyproject.toml +1 -1
  3. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/__init__.py +76 -3
  4. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/_version.py +2 -2
  5. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/utils.py +4 -2
  6. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/cli/commands.py +20 -2
  7. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/cli/release.py +301 -27
  8. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/config.py +8 -0
  9. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/account.py +11 -1
  10. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/broker.py +29 -5
  11. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/connection_manager.py +10 -4
  12. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/data.py +5 -3
  13. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/__init__.py +29 -12
  14. qubx-1.0.1.dev2/src/qubx/connectors/ccxt/exchanges/okx/account.py +70 -0
  15. qubx-1.0.1.dev2/src/qubx/connectors/ccxt/exchanges/okx/broker.py +18 -0
  16. qubx-1.0.1.dev2/src/qubx/connectors/ccxt/exchanges/okx/okx.py +56 -0
  17. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/factory.py +9 -3
  18. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/ohlc.py +1 -3
  19. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/orderbook.py +30 -6
  20. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/utils.py +2 -2
  21. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/basics.py +19 -13
  22. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/context.py +3 -1
  23. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/mixins/market.py +3 -1
  24. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/mixins/processing.py +14 -14
  25. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/mixins/trading.py +2 -2
  26. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/storages/ccxt.py +5 -4
  27. qubx-1.0.1.dev2/src/qubx/health/__init__.py +5 -0
  28. qubx-1.0.1.dev2/src/qubx/health/server.py +67 -0
  29. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/crypto-fees.ini +14 -1
  30. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/ta/indicators.pxd +11 -1
  31. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/ta/indicators.pyi +4 -0
  32. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/ta/indicators.pyx +69 -1
  33. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/trackers/sizers.py +12 -10
  34. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/marketdata/ccxt.py +5 -1
  35. qubx-1.0.1.dev2/src/qubx/utils/runner/__init__.py +0 -0
  36. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/accounts.py +4 -0
  37. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/configs.py +26 -1
  38. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/factory.py +7 -1
  39. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/runner.py +59 -9
  40. qubx-1.0.0.dev6/src/qubx/health/__init__.py +0 -4
  41. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/.gitignore +0 -0
  42. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/LICENSE +0 -0
  43. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/README.md +0 -0
  44. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/hatch_build.py +0 -0
  45. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/_nb_magic.py +0 -0
  46. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/__init__.py +0 -0
  47. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/account.py +0 -0
  48. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/broker.py +0 -0
  49. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/data.py +0 -0
  50. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/iteratedstream.py +0 -0
  51. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/management.py +0 -0
  52. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/ome.py +0 -0
  53. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/optimization.py +0 -0
  54. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/runner.py +0 -0
  55. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/sentinels.py +0 -0
  56. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/simulated_data.py +0 -0
  57. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/simulated_exchange.py +0 -0
  58. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/simulator.py +0 -0
  59. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/backtester/transfers.py +0 -0
  60. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/cli/__init__.py +0 -0
  61. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/cli/deploy.py +0 -0
  62. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/cli/misc.py +0 -0
  63. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/cli/theme.py +0 -0
  64. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/cli/tui.py +0 -0
  65. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/__init__.py +0 -0
  66. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/__init__.py +0 -0
  67. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  68. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  69. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  70. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  71. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  72. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  73. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  74. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  75. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  76. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
  77. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
  78. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  79. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  80. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  81. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  82. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  83. {qubx-1.0.0.dev6/src/qubx/core → qubx-1.0.1.dev2/src/qubx/connectors/ccxt/exchanges/okx}/__init__.py +0 -0
  84. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  85. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  86. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  87. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  88. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  89. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  90. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  91. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  92. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  93. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  94. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  95. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  96. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/registry.py +0 -0
  97. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/tardis/data.py +0 -0
  98. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/connectors/tardis/utils.py +0 -0
  99. {qubx-1.0.0.dev6/src/qubx/restarts → qubx-1.0.1.dev2/src/qubx/core}/__init__.py +0 -0
  100. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/account.py +0 -0
  101. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/detectors/__init__.py +0 -0
  102. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/detectors/delisting.py +0 -0
  103. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/detectors/stale.py +0 -0
  104. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/errors.py +0 -0
  105. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/exceptions.py +0 -0
  106. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/helpers.py +0 -0
  107. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/initializer.py +0 -0
  108. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/interfaces.py +0 -0
  109. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/loggers.py +0 -0
  110. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/lookups.py +0 -0
  111. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/metrics.py +0 -0
  112. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/mixins/__init__.py +0 -0
  113. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/mixins/subscription.py +0 -0
  114. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/mixins/universe.py +0 -0
  115. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/mixins/utils.py +0 -0
  116. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/series.pxd +0 -0
  117. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/series.pyi +0 -0
  118. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/series.pyx +0 -0
  119. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/utils.pyi +0 -0
  120. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/core/utils.pyx +0 -0
  121. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/__init__.py +0 -0
  122. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/cache.py +0 -0
  123. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/containers.py +0 -0
  124. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/guards.py +0 -0
  125. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/registry.py +0 -0
  126. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/storage.py +0 -0
  127. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/storages/csv.py +0 -0
  128. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/storages/handy.py +0 -0
  129. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/storages/multi.py +0 -0
  130. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/storages/questdb.py +0 -0
  131. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/storages/stub.py +0 -0
  132. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/storages/utils.py +0 -0
  133. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/data/transformers.py +0 -0
  134. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/emitters/__init__.py +0 -0
  135. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/emitters/base.py +0 -0
  136. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/emitters/composite.py +0 -0
  137. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/emitters/csv.py +0 -0
  138. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/emitters/indicator.py +0 -0
  139. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/emitters/inmemory.py +0 -0
  140. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/emitters/prometheus.py +0 -0
  141. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/emitters/questdb.py +0 -0
  142. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/__init__.py +0 -0
  143. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/composite.py +0 -0
  144. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/formatters/__init__.py +0 -0
  145. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/formatters/base.py +0 -0
  146. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/formatters/incremental.py +0 -0
  147. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/formatters/slack.py +0 -0
  148. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/formatters/target_position.py +0 -0
  149. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/redis_streams.py +0 -0
  150. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/exporters/slack.py +0 -0
  151. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/gathering/simplest.py +0 -0
  152. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/health/base.py +0 -0
  153. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/health/dummy.py +0 -0
  154. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/loggers/__init__.py +0 -0
  155. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/loggers/csv.py +0 -0
  156. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/loggers/factory.py +0 -0
  157. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/loggers/inmemory.py +0 -0
  158. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/loggers/mongo.py +0 -0
  159. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/notifications/__init__.py +0 -0
  160. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/notifications/composite.py +0 -0
  161. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/notifications/slack.py +0 -0
  162. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/notifications/throttler.py +0 -0
  163. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/pandaz/__init__.py +0 -0
  164. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/pandaz/stats.py +0 -0
  165. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/pandaz/ta.py +0 -0
  166. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/pandaz/utils.py +0 -0
  167. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/plugins/__init__.py +0 -0
  168. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/plugins/loader.py +0 -0
  169. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/_build.py +0 -0
  170. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  171. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  172. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  173. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  174. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  175. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  176. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  177. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  178. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  179. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  180. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  181. {qubx-1.0.0.dev6/src/qubx/ta → qubx-1.0.1.dev2/src/qubx/restarts}/__init__.py +0 -0
  182. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restarts/state_resolvers.py +0 -0
  183. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restarts/time_finders.py +0 -0
  184. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restorers/__init__.py +0 -0
  185. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restorers/balance.py +0 -0
  186. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restorers/factory.py +0 -0
  187. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restorers/interfaces.py +0 -0
  188. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restorers/position.py +0 -0
  189. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restorers/signal.py +0 -0
  190. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restorers/state.py +0 -0
  191. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/restorers/utils.py +0 -0
  192. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/state/__init__.py +0 -0
  193. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/state/dummy.py +0 -0
  194. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/state/redis.py +0 -0
  195. {qubx-1.0.0.dev6/src/qubx/utils/plotting → qubx-1.0.1.dev2/src/qubx/ta}/__init__.py +0 -0
  196. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/__init__.py +0 -0
  197. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/base.py +0 -0
  198. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  199. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/project/config.yml.j2 +0 -0
  200. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  201. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  202. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  203. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  204. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  205. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/project/template.yml +0 -0
  206. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  207. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  208. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/simple/config.yml.j2 +0 -0
  209. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  210. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  211. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  212. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/templates/simple/template.yml +0 -0
  213. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/trackers/__init__.py +0 -0
  214. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/trackers/advanced.py +0 -0
  215. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/trackers/composite.py +0 -0
  216. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/trackers/rebalancers.py +0 -0
  217. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/trackers/riskctrl.py +0 -0
  218. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/__init__.py +0 -0
  219. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/_pyxreloader.py +0 -0
  220. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/charting/lookinglass.py +0 -0
  221. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  222. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/charting/orderbook.py +0 -0
  223. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/collections.py +0 -0
  224. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/hft/__init__.py +0 -0
  225. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/hft/numba_utils.py +0 -0
  226. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/hft/orderbook.pyi +0 -0
  227. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/hft/orderbook.pyx +0 -0
  228. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/marketdata/binance.py +0 -0
  229. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/marketdata/dukas.py +0 -0
  230. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/misc.py +0 -0
  231. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/nonce.py +0 -0
  232. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/ntp.py +0 -0
  233. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/numbers_utils.py +0 -0
  234. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/orderbook.py +0 -0
  235. {qubx-1.0.0.dev6/src/qubx/utils/plotting/renderers → qubx-1.0.1.dev2/src/qubx/utils/plotting}/__init__.py +0 -0
  236. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/plotting/dashboard.py +0 -0
  237. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/plotting/data.py +0 -0
  238. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/plotting/interfaces.py +0 -0
  239. {qubx-1.0.0.dev6/src/qubx/utils/runner → qubx-1.0.1.dev2/src/qubx/utils/plotting/renderers}/__init__.py +0 -0
  240. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  241. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/questdb.py +0 -0
  242. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/rate_limiter.py +0 -0
  243. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/results.py +0 -0
  244. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/ringbuffer.pxd +0 -0
  245. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/ringbuffer.pyi +0 -0
  246. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/ringbuffer.pyx +0 -0
  247. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  248. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/kernel_service.py +0 -0
  249. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/__init__.py +0 -0
  250. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/app.py +0 -0
  251. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/handlers.py +0 -0
  252. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/init_code.py +0 -0
  253. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/kernel.py +0 -0
  254. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  255. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  256. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  257. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  258. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  259. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  260. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  261. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  262. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/slack.py +0 -0
  263. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/throttler.py +0 -0
  264. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/time.py +0 -0
  265. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/src/qubx/utils/websocket_manager.py +0 -0
  266. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
  267. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
  268. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
  269. {qubx-1.0.0.dev6 → qubx-1.0.1.dev2}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 1.0.0.dev6
3
+ Version: 1.0.1.dev2
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
@@ -133,7 +133,7 @@ extend-select = ["I"]
133
133
  ignore = ["E731", "E722", "E741"]
134
134
 
135
135
  [tool.ruff.lint.extend-per-file-ignores]
136
- "*.ipynb" = [ "F405", "F401", "E701", "E402", "F403", "E401", "E702", "F821", "E731", ]
136
+ "*.ipynb" = [ "F405", "F401", "E701", "E402", "F403", "E401", "E702", "F821", "E731", "I001", ]
137
137
 
138
138
  [tool.pytest.ini_options]
139
139
  asyncio_mode = "auto"
@@ -1,3 +1,4 @@
1
+ import json as _json
1
2
  import os
2
3
  import sys
3
4
  from typing import Callable
@@ -32,15 +33,30 @@ def runtime_env():
32
33
  return "python" # Probably standard Python interpreter
33
34
 
34
35
 
36
+ def format_platform_identity(record) -> str:
37
+ """Return a colored identity prefix from record extras (bot_id / instance_id)."""
38
+ bot_id = record["extra"].get("bot_id")
39
+ instance_id = record["extra"].get("instance_id")
40
+ if bot_id or instance_id:
41
+ parts = []
42
+ if bot_id:
43
+ parts.append(f"bot={bot_id}")
44
+ if instance_id:
45
+ parts.append(f"inst={instance_id}")
46
+ return "<magenta>[%s]</magenta> " % " ".join(parts)
47
+ return ""
48
+
49
+
35
50
  def formatter(record):
36
51
  end = record["extra"].get("end", "\n")
37
52
  fmt = "<lvl>{message}</lvl>%s" % end
38
53
  if record["level"].name in {"WARNING", "SNAKY"}:
39
54
  fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
40
55
 
56
+ identity = format_platform_identity(record)
41
57
  prefix = (
42
- "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] <cyan>({module})</cyan> "
43
- % record["level"].icon
58
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] %s<cyan>({module})</cyan> "
59
+ % (record["level"].icon, identity)
44
60
  )
45
61
 
46
62
  if record["exception"] is not None:
@@ -70,6 +86,38 @@ class QubxLogConfig:
70
86
  os.environ["QUBX_LOG_LEVEL"] = level
71
87
  QubxLogConfig.setup_logger(level)
72
88
 
89
+ _COLOR_TAG_RE = None
90
+
91
+ @staticmethod
92
+ def _strip_color_tags(text: str) -> str:
93
+ """Remove loguru color markup tags like <yellow>...</yellow> from text."""
94
+ import re
95
+
96
+ if QubxLogConfig._COLOR_TAG_RE is None:
97
+ QubxLogConfig._COLOR_TAG_RE = re.compile(r"</?[a-z_]+>")
98
+ return QubxLogConfig._COLOR_TAG_RE.sub("", text)
99
+
100
+ @staticmethod
101
+ def _json_sink(message):
102
+ """Emit one JSON line per log record for Loki/Promtail ingestion."""
103
+ record = message.record
104
+ entry = {
105
+ "timestamp": record["time"].strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
106
+ "level": record["level"].name,
107
+ "module": record["module"],
108
+ "function": record["function"],
109
+ "line": record["line"],
110
+ "message": QubxLogConfig._strip_color_tags(record["message"]),
111
+ }
112
+ # Merge platform identity and any other extras (skip internal loguru keys)
113
+ for key, val in record["extra"].items():
114
+ if key not in ("user", "end", "stack"):
115
+ entry[key] = val
116
+ if record["exception"] is not None:
117
+ entry["exception"] = stackprinter.format(record["exception"], style="plaintext")
118
+ sys.stdout.write(_json.dumps(entry, default=str) + "\n")
119
+ sys.stdout.flush()
120
+
73
121
  @staticmethod
74
122
  def setup_logger(level: str | None = None, custom_formatter: Callable | None = None, colorize: bool = True):
75
123
  global logger
@@ -84,7 +132,21 @@ class QubxLogConfig:
84
132
  logger.remove(None)
85
133
 
86
134
  level = level or QubxLogConfig.get_log_level()
87
- # Add stdout handler with enqueue=True for thread/process safety
135
+
136
+ # Check if JSON format is requested (for Loki/container deployments)
137
+ log_format = os.getenv("QUBX_LOG_FORMAT", "text").lower()
138
+ if log_format == "json":
139
+ logger.add(
140
+ QubxLogConfig._json_sink,
141
+ level=level,
142
+ enqueue=True,
143
+ backtrace=True,
144
+ diagnose=True,
145
+ )
146
+ # No colorize opt needed for JSON
147
+ return
148
+
149
+ # Default: human-readable text format
88
150
  logger.add(
89
151
  sys.stdout,
90
152
  format=custom_formatter or formatter,
@@ -96,6 +158,17 @@ class QubxLogConfig:
96
158
  )
97
159
  logger = logger.opt(colors=colorize)
98
160
 
161
+ @staticmethod
162
+ def bind_platform_identity(bot_id: str | None = None, instance_id: str | None = None):
163
+ """Bind platform identity fields (bot_id, instance_id) to all log messages globally."""
164
+ def patcher(record):
165
+ if bot_id:
166
+ record["extra"]["bot_id"] = bot_id
167
+ if instance_id:
168
+ record["extra"]["instance_id"] = instance_id
169
+
170
+ logger.configure(patcher=patcher)
171
+
99
172
 
100
173
  QubxLogConfig.setup_logger()
101
174
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.0.0.dev6'
32
- __version_tuple__ = version_tuple = (1, 0, 0, 'dev6')
31
+ __version__ = version = '1.0.1.dev2'
32
+ __version_tuple__ = version_tuple = (1, 0, 1, 'dev2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -133,8 +133,10 @@ class SimulatedLogFormatter:
133
133
  else:
134
134
  now = self.time_provider.time().astype("datetime64[us]").item().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
135
135
 
136
- # prefix = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] " % record["level"].icon
137
- prefix = f"<lc>{now}</lc> [<level>{record['level'].icon}</level>] <cyan>({{module}})</cyan> "
136
+ from qubx import format_platform_identity
137
+
138
+ identity = format_platform_identity(record)
139
+ prefix = f"<lc>{now}</lc> [<level>{record['level'].icon}</level>] {identity}<cyan>({{module}})</cyan> "
138
140
 
139
141
  if record["exception"] is not None:
140
142
  record["extra"]["stack"] = stackprinter.format(record["exception"], style="darkbg3")
@@ -109,6 +109,7 @@ def main(debug: bool, debug_port: int, log_level: str):
109
109
  @click.option("--no-emission", is_flag=True, default=False, help="Disable metric emission.", show_default=True)
110
110
  @click.option("--no-notifiers", is_flag=True, default=False, help="Disable lifecycle notifiers.", show_default=True)
111
111
  @click.option("--no-exporters", is_flag=True, default=False, help="Disable trade exporters.", show_default=True)
112
+ @click.option("--override", type=Path, default=None, help="Sparse YAML file to deep-merge on top of config.", show_default=False)
112
113
  def run(
113
114
  config_file: Path,
114
115
  account_file: Path | None,
@@ -127,6 +128,7 @@ def run(
127
128
  no_emission: bool,
128
129
  no_notifiers: bool,
129
130
  no_exporters: bool,
131
+ override: Path | None,
130
132
  ):
131
133
  """
132
134
  Starts the strategy with the given configuration file. If paper mode is enabled, account is not required.
@@ -210,6 +212,7 @@ def run(
210
212
  no_emission=no_emission,
211
213
  no_notifiers=no_notifiers,
212
214
  no_exporters=no_exporters,
215
+ config_overrides=override,
213
216
  )
214
217
 
215
218
 
@@ -236,8 +239,23 @@ def run(
236
239
  help="Write simulation logs to a file in the output directory.",
237
240
  show_default=True,
238
241
  )
242
+ @click.option(
243
+ "--name",
244
+ "-n",
245
+ default=None,
246
+ type=str,
247
+ help="Override the run name used for output folder (e.g. 'smoketest', '01_reference'). "
248
+ "Overrides the 'name:' field from the config file.",
249
+ show_default=True,
250
+ )
239
251
  def simulate(
240
- config_file: Path, start: str | None, end: str | None, output: str | None, report: str | None, log_to_file: bool
252
+ config_file: Path,
253
+ start: str | None,
254
+ end: str | None,
255
+ output: str | None,
256
+ report: str | None,
257
+ log_to_file: bool,
258
+ name: str | None,
241
259
  ):
242
260
  """
243
261
  Simulates the strategy with the given configuration file.
@@ -249,7 +267,7 @@ def simulate(
249
267
  add_project_to_system_path(str(config_file.parent))
250
268
  logo()
251
269
  logger.info(f"Process PID: <g>{os.getpid()}</g>")
252
- simulate_strategy(config_file, output, start, end, report, log_to_file)
270
+ simulate_strategy(config_file, output, start, end, report, log_to_file, name=name)
253
271
 
254
272
 
255
273
  @main.command()
@@ -534,9 +534,9 @@ def release_strategy(
534
534
  config_file=config_file,
535
535
  )
536
536
  except ValueError as e:
537
- logger.error(f"<r>{str(e)}</r>")
537
+ logger.error(f"<r>{str(e).replace('<', chr(92) + '<')}</r>")
538
538
  except Exception as e:
539
- logger.error(f"<r>Error releasing strategy: {e}</r>")
539
+ logger.error(f"<r>Error releasing strategy: {str(e).replace('<', chr(92) + '<')}</r>")
540
540
 
541
541
 
542
542
  def create_released_pack(
@@ -814,7 +814,217 @@ def _create_metadata(stg_name: str, git_info: ReleaseInfo, release_dir: str) ->
814
814
  fs.write(f"Commit: {git_info.commit}\n")
815
815
 
816
816
 
817
- def _clean_pyproject_for_release(pyproject_data: dict) -> dict:
817
+ def _version_exists_on_pypi(pkg_name: str, version: str) -> bool:
818
+ """Check if a specific package version is available on public PyPI."""
819
+ import urllib.request
820
+
821
+ try:
822
+ url = f"https://pypi.org/pypi/{pkg_name}/{version}/json"
823
+ with urllib.request.urlopen(url, timeout=5) as response:
824
+ return response.status == 200
825
+ except Exception:
826
+ return False
827
+
828
+
829
+ def _get_index_url(index_name: str, pyproject_data: dict) -> str | None:
830
+ """Get URL for a named uv index from [[tool.uv.index]] entries."""
831
+ indexes = pyproject_data.get("tool", {}).get("uv", {}).get("index", [])
832
+ for idx in indexes:
833
+ if idx.get("name") == index_name:
834
+ return idx.get("url")
835
+ return None
836
+
837
+
838
+ def _find_version_in_pyproject(pkg_name: str, pyproject_data: dict) -> str | None:
839
+ """
840
+ Find a package's pinned version from pyproject.toml deps (regular + optional).
841
+ Returns the version string (e.g. "0.1.0") or None if not found.
842
+ """
843
+ all_deps: list[str] = list(pyproject_data.get("project", {}).get("dependencies", []))
844
+ for group_deps in pyproject_data.get("project", {}).get("optional-dependencies", {}).values():
845
+ all_deps.extend(group_deps)
846
+
847
+ for dep in all_deps:
848
+ dep_pkg = dep.split(">=")[0].split("==")[0].split("<")[0].strip()
849
+ if dep_pkg.lower() == pkg_name.lower():
850
+ if "==" in dep:
851
+ return dep.split("==")[1].strip()
852
+ if ">=" in dep:
853
+ return dep.split(">=")[1].split(",")[0].strip()
854
+ return None
855
+
856
+
857
+ def _bundle_source_overrides(
858
+ pyproject_data: dict,
859
+ pyproject_root: str,
860
+ release_dir: str,
861
+ required_packages: set[str],
862
+ ) -> list[str]:
863
+ """
864
+ For each [tool.uv.sources] entry that is required by this release:
865
+ - path source + installed version on public PyPI → skip (will resolve from PyPI)
866
+ - path source + NOT on public PyPI → build wheel from local path and bundle in wheels/
867
+ - index source (private registry) → download wheel from that index and bundle in wheels/
868
+
869
+ Returns list of bundled package names (lowercase).
870
+ """
871
+ import subprocess
872
+ from importlib.metadata import PackageNotFoundError
873
+ from importlib.metadata import version as get_version
874
+
875
+ sources = pyproject_data.get("tool", {}).get("uv", {}).get("sources", {})
876
+ if not sources:
877
+ return []
878
+
879
+ wheels_dir = os.path.join(release_dir, "wheels")
880
+ bundled: list[str] = []
881
+
882
+ for pkg_name, source in sources.items():
883
+ if pkg_name.lower() not in required_packages:
884
+ continue
885
+
886
+ if "path" in source:
887
+ local_path = os.path.normpath(os.path.join(pyproject_root, source["path"]))
888
+
889
+ try:
890
+ installed_ver = get_version(pkg_name)
891
+ except PackageNotFoundError:
892
+ logger.warning(f" {pkg_name} not installed, skipping bundle")
893
+ continue
894
+
895
+ if _version_exists_on_pypi(pkg_name, installed_ver):
896
+ logger.info(f" {pkg_name}=={installed_ver} found on public PyPI, will resolve from registry")
897
+ continue
898
+
899
+ logger.info(f" Bundling {pkg_name}=={installed_ver} from local path {local_path} ...")
900
+ os.makedirs(wheels_dir, exist_ok=True)
901
+ try:
902
+ # Run from the package's own directory so uv reads its [tool.uv.sources]
903
+ # (needed when the package has local-path build deps like qubx)
904
+ subprocess.run(
905
+ ["uv", "build", "--wheel", ".", "--out-dir", wheels_dir],
906
+ cwd=local_path,
907
+ check=True, capture_output=True, text=True,
908
+ )
909
+ # Warn if the wheel is platform-specific (compiled extensions)
910
+ for whl in os.listdir(wheels_dir):
911
+ if whl.lower().startswith(pkg_name.lower().replace("-", "_")) and "none-any" not in whl:
912
+ logger.warning(
913
+ f" {whl} is platform-specific. "
914
+ "Ensure the container architecture matches the build machine."
915
+ )
916
+ bundled.append(pkg_name.lower())
917
+ logger.info(f" Bundled {pkg_name}")
918
+ except subprocess.CalledProcessError as e:
919
+ err_msg = (e.stderr or str(e)).replace("<", r"\<")
920
+ logger.warning(f" Failed to build wheel for {pkg_name}: {err_msg}")
921
+
922
+ elif "index" in source:
923
+ index_name = source["index"]
924
+ index_url = _get_index_url(index_name, pyproject_data)
925
+ if not index_url:
926
+ logger.warning(f" Index '{index_name}' not found in [[tool.uv.index]] (needed for {pkg_name})")
927
+ continue
928
+
929
+ try:
930
+ installed_ver = get_version(pkg_name)
931
+ except PackageNotFoundError:
932
+ # Package not installed (e.g. optional dep not synced) — fall back to
933
+ # the version declared in pyproject optional-deps or regular deps
934
+ installed_ver = _find_version_in_pyproject(pkg_name, pyproject_data)
935
+ if not installed_ver:
936
+ logger.warning(
937
+ f" {pkg_name} not installed and no version found in pyproject deps, skipping bundle"
938
+ )
939
+ continue
940
+ logger.info(f" {pkg_name} not installed; using declared version {installed_ver} from pyproject")
941
+
942
+ if _version_exists_on_pypi(pkg_name, installed_ver):
943
+ logger.info(f" {pkg_name}=={installed_ver} found on public PyPI, will resolve from registry")
944
+ continue
945
+
946
+ logger.info(f" Downloading {pkg_name}=={installed_ver} from private index '{index_name}' ...")
947
+ os.makedirs(wheels_dir, exist_ok=True)
948
+ try:
949
+ import shutil
950
+
951
+ pip_exe = shutil.which("pip") or shutil.which("pip3")
952
+ if not pip_exe:
953
+ raise RuntimeError("pip not found — cannot download wheel from private index")
954
+ subprocess.run(
955
+ [
956
+ pip_exe, "download",
957
+ f"{pkg_name}=={installed_ver}",
958
+ "--index-url", index_url,
959
+ "--dest", wheels_dir,
960
+ "--no-deps",
961
+ "--quiet",
962
+ ],
963
+ check=True, capture_output=True, text=True,
964
+ )
965
+ bundled.append(pkg_name.lower())
966
+ logger.info(f" Downloaded {pkg_name}=={installed_ver}")
967
+ except subprocess.CalledProcessError as e:
968
+ err_msg = (e.stderr or str(e)).replace("<", r"\<")
969
+ logger.warning(f" Failed to download wheel for {pkg_name}: {err_msg}")
970
+
971
+ return bundled
972
+
973
+
974
+ def _get_plugin_deps(stg_config: "StrategyConfig", pyproject_data: dict) -> list[str]:
975
+ """
976
+ Extract package dependency specs for plugin modules listed in the strategy config.
977
+
978
+ Maps plugin module names (e.g. qubx_lighter) to package specs
979
+ (e.g. qubx-lighter==0.1.0) by looking in [project.optional-dependencies].
980
+ Falls back to the installed version if not found there.
981
+
982
+ Returns list of dep specs like ["qubx-lighter==0.1.0"].
983
+ """
984
+ from importlib.metadata import PackageNotFoundError
985
+ from importlib.metadata import version as get_version
986
+
987
+ if not stg_config.plugins or not stg_config.plugins.modules:
988
+ return []
989
+
990
+ optional_deps: dict[str, list[str]] = pyproject_data.get("project", {}).get("optional-dependencies", {})
991
+
992
+ plugin_deps: list[str] = []
993
+ for module_name in stg_config.plugins.modules:
994
+ pkg_name = module_name.replace("_", "-")
995
+
996
+ # Search in optional-dependency groups first
997
+ found = False
998
+ for group_name, group_deps in optional_deps.items():
999
+ for dep in group_deps:
1000
+ dep_pkg = dep.split(">=")[0].split("==")[0].split("<")[0].strip()
1001
+ if dep_pkg.lower() == pkg_name.lower():
1002
+ plugin_deps.append(dep)
1003
+ logger.info(f" Plugin '{module_name}' -> adding '{dep}' (from optional-deps [{group_name}])")
1004
+ found = True
1005
+ break
1006
+ if found:
1007
+ break
1008
+
1009
+ if not found:
1010
+ try:
1011
+ ver = get_version(pkg_name)
1012
+ spec = f"{pkg_name}>={ver}"
1013
+ plugin_deps.append(spec)
1014
+ logger.warning(
1015
+ f" Plugin '{module_name}' not in optional-deps; "
1016
+ f"adding '{spec}' from installed packages"
1017
+ )
1018
+ except PackageNotFoundError:
1019
+ logger.warning(
1020
+ f" Plugin '{module_name}' ({pkg_name}) not found in "
1021
+ "optional-deps or installed packages - skipping"
1022
+ )
1023
+
1024
+ return plugin_deps
1025
+
1026
+
1027
+ def _clean_pyproject_for_release(pyproject_data: dict, bundled_packages: list[str] | None = None) -> dict:
818
1028
  """
819
1029
  Remove dev-only and local-path-dependent sections from pyproject.toml for release.
820
1030
 
@@ -852,10 +1062,24 @@ def _clean_pyproject_for_release(pyproject_data: dict) -> dict:
852
1062
  del pyproject_data["tool"][key]
853
1063
  logger.debug(f"Removed [tool.{key}] from release pyproject.toml")
854
1064
 
1065
+ # If any wheels were bundled, add find-links so uv can resolve them
1066
+ if bundled_packages:
1067
+ if "tool" not in pyproject_data:
1068
+ pyproject_data["tool"] = {}
1069
+ if "uv" not in pyproject_data["tool"]:
1070
+ pyproject_data["tool"]["uv"] = {}
1071
+ pyproject_data["tool"]["uv"]["find-links"] = ["./wheels"]
1072
+ logger.debug("Added find-links = ['./wheels'] to [tool.uv]")
1073
+
855
1074
  return pyproject_data
856
1075
 
857
1076
 
858
- def _modify_pyproject_toml(pyproject_path: str, package_name: str) -> None:
1077
+ def _modify_pyproject_toml(
1078
+ pyproject_path: str,
1079
+ package_name: str,
1080
+ bundled_packages: list[str] | None = None,
1081
+ plugin_deps: list[str] | None = None,
1082
+ ) -> None:
859
1083
  """
860
1084
  Modify the pyproject.toml file to include the project package as a dependency.
861
1085
 
@@ -873,7 +1097,7 @@ def _modify_pyproject_toml(pyproject_path: str, package_name: str) -> None:
873
1097
  pyproject_data = toml.load(f)
874
1098
 
875
1099
  # Clean up dev-only and local-path-dependent sections
876
- pyproject_data = _clean_pyproject_for_release(pyproject_data)
1100
+ pyproject_data = _clean_pyproject_for_release(pyproject_data, bundled_packages)
877
1101
 
878
1102
  # Handle PEP 621 format
879
1103
  if "project" in pyproject_data:
@@ -886,15 +1110,29 @@ def _modify_pyproject_toml(pyproject_path: str, package_name: str) -> None:
886
1110
  python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
887
1111
  pyproject_data["project"]["requires-python"] = f">={python_version}"
888
1112
 
889
- # Special case when we have dev dependencies for Qubx or QuantKit
1113
+ # Pin qubx/quantkit versions only if bare (no version constraint specified)
890
1114
  deps = pyproject_data["project"]["dependencies"]
891
1115
  for i, dep in enumerate(deps):
892
1116
  dep_name = dep.split(">=")[0].split("==")[0].split("<")[0].strip()
893
1117
  if dep_name.lower().startswith("qubx") or dep_name.lower().startswith("quantkit"):
894
- try:
895
- deps[i] = f"{dep_name}>={version(dep_name)}"
896
- except Exception:
897
- pass
1118
+ if dep.strip() == dep_name:
1119
+ # Bare package name — add installed version
1120
+ try:
1121
+ deps[i] = f"{dep_name}>={version(dep_name)}"
1122
+ except Exception:
1123
+ pass
1124
+
1125
+ # Add plugin dependencies (from config's plugins.modules)
1126
+ if plugin_deps:
1127
+ for pdep in plugin_deps:
1128
+ pdep_pkg = pdep.split(">=")[0].split("==")[0].split("<")[0].strip().lower()
1129
+ already_present = any(
1130
+ d.split(">=")[0].split("==")[0].split("<")[0].strip().lower() == pdep_pkg
1131
+ for d in deps
1132
+ )
1133
+ if not already_present:
1134
+ deps.append(pdep)
1135
+ logger.debug(f"Added plugin dep: {pdep}")
898
1136
 
899
1137
  # Check if build-system section exists
900
1138
  if "build-system" not in pyproject_data:
@@ -1007,6 +1245,21 @@ def _configure_pyproject_for_external_deps(pyproject_path: str, packages: list[s
1007
1245
  logger.warning(f"Could not determine version for {package}: {e}")
1008
1246
  deps.append(package)
1009
1247
 
1248
+ # No custom code to package — disable package building
1249
+ if "tool" not in pyproject_data:
1250
+ pyproject_data["tool"] = {}
1251
+ if "uv" not in pyproject_data["tool"]:
1252
+ pyproject_data["tool"]["uv"] = {}
1253
+ pyproject_data["tool"]["uv"]["package"] = False
1254
+
1255
+ # Also disable poetry package mode if present
1256
+ if "poetry" in pyproject_data["tool"]:
1257
+ poetry_config = pyproject_data["tool"]["poetry"]
1258
+ poetry_config["package-mode"] = False
1259
+ if "packages" in poetry_config:
1260
+ del poetry_config["packages"]
1261
+ logger.debug("Set package = false (external deps only, no custom code)")
1262
+
1010
1263
  # Write updated pyproject.toml
1011
1264
  with open(pyproject_path, "w") as f:
1012
1265
  toml.dump(pyproject_data, f)
@@ -1095,13 +1348,10 @@ def _handle_project_files(
1095
1348
  ) -> None:
1096
1349
  """
1097
1350
  Handle project files like pyproject.toml and generate lock file.
1098
-
1099
- Args:
1100
- pyproject_root: Root directory containing pyproject.toml
1101
- release_dir: Destination directory for release
1102
- stg_info: Strategy info with config (optional, for dependency extraction)
1103
1351
  """
1104
- # Copy pyproject.toml if it exists
1352
+ import toml
1353
+
1354
+ # Copy pyproject.toml
1105
1355
  pyproject_src = os.path.join(pyproject_root, "pyproject.toml")
1106
1356
  if not os.path.exists(pyproject_src):
1107
1357
  raise FileNotFoundError(f"pyproject.toml not found in {pyproject_root}")
@@ -1110,25 +1360,51 @@ def _handle_project_files(
1110
1360
  logger.debug(f"Copying pyproject.toml from {pyproject_src} to {pyproject_dest}")
1111
1361
  shutil.copy2(pyproject_src, pyproject_dest)
1112
1362
 
1113
- # If no classes exist, configure pyproject.toml for external dependencies only
1363
+ # Read original data once for analysis (before any modifications)
1364
+ with open(pyproject_dest, "r") as f:
1365
+ pyproject_data = toml.load(f)
1366
+
1367
+ # --- Plugin deps from config ---
1368
+ plugin_deps: list[str] = []
1369
+ if stg_info and stg_info.config:
1370
+ plugin_deps = _get_plugin_deps(stg_info.config, pyproject_data)
1371
+ if plugin_deps:
1372
+ logger.info(f"Plugin dependencies from config: {plugin_deps}")
1373
+
1374
+ # --- Build required_packages set for targeted bundling ---
1375
+ # Start from declared project deps
1376
+ required_packages: set[str] = set()
1377
+ for dep in pyproject_data.get("project", {}).get("dependencies", []):
1378
+ name = dep.split(">=")[0].split("==")[0].split("<")[0].strip().lower()
1379
+ required_packages.add(name)
1380
+ # Add plugin packages
1381
+ for pdep in plugin_deps:
1382
+ name = pdep.split(">=")[0].split("==")[0].split("<")[0].strip().lower()
1383
+ required_packages.add(name)
1384
+
1385
+ # --- Bundle private/local wheels ---
1386
+ bundled_packages: list[str] = []
1387
+ if required_packages:
1388
+ logger.info("Resolving private/local source dependencies...")
1389
+ bundled_packages = _bundle_source_overrides(pyproject_data, pyproject_root, release_dir, required_packages)
1390
+ if bundled_packages:
1391
+ logger.info(f"Bundled {len(bundled_packages)} package(s): {', '.join(bundled_packages)}")
1392
+
1393
+ # --- Handle external-deps-only configs (no custom strategy classes) ---
1114
1394
  if stg_info and not stg_info.classes:
1115
1395
  current_package = get_project_package_name(pyproject_root)
1116
1396
  external_deps = extract_external_dependencies(stg_info.config, current_package)
1117
-
1118
1397
  if external_deps:
1119
1398
  logger.info(f"Configuring pyproject.toml for external dependencies: {external_deps}")
1120
1399
  _configure_pyproject_for_external_deps(pyproject_dest, external_deps)
1121
1400
 
1122
- # Copy build.py if it exists
1401
+ # --- Copy build.py ---
1123
1402
  build_src = os.path.join(pyproject_root, "build.py")
1124
1403
  if not os.path.exists(build_src):
1125
1404
  logger.info(f"build.py not found in {pyproject_root} using default one")
1126
1405
  build_src = load_qubx_resources_as_text("_build.py")
1127
-
1128
- # - setup project's name in default build.py
1129
1406
  prj_name = os.path.basename(pyproject_root)
1130
1407
  build_src = build_src.replace("<<PROJECT_NAME>>", prj_name)
1131
-
1132
1408
  with open(os.path.join(release_dir, "build.py"), "wt") as fs:
1133
1409
  fs.write(build_src)
1134
1410
  else:
@@ -1136,13 +1412,11 @@ def _handle_project_files(
1136
1412
  logger.debug(f"Copying build.py from {build_src} to {build_dest}")
1137
1413
  shutil.copy2(build_src, build_dest)
1138
1414
 
1139
- # Get the basename of the pyproject_root as the package name
1415
+ # --- Modify pyproject: clean sources, pin versions, add find-links + plugin deps ---
1140
1416
  package_name = os.path.basename(pyproject_root)
1417
+ _modify_pyproject_toml(pyproject_dest, package_name, bundled_packages=bundled_packages, plugin_deps=plugin_deps)
1141
1418
 
1142
- # Modify the pyproject.toml to include the project package
1143
- _modify_pyproject_toml(pyproject_dest, package_name)
1144
-
1145
- # Generate the uv.lock file
1419
+ # --- Generate lock file ---
1146
1420
  _generate_lock_file(release_dir)
1147
1421
 
1148
1422
 
@@ -86,6 +86,14 @@ class QubxSettings(BaseSettings):
86
86
  instrument_lookup: LookupConfig = LookupConfig()
87
87
  fees_lookup: LookupConfig = LookupConfig()
88
88
 
89
+ # Platform integration (all settable via QUBX_* env vars or ~/.qubx/config.json)
90
+ account_file: str | None = None
91
+ bot_id: str | None = None
92
+ instance_id: str | None = None
93
+ metrics_port: int | None = None
94
+ health_port: int | None = None
95
+ log_format: str = "text" # "text" or "json"
96
+
89
97
  model_config = {"env_prefix": "QUBX_", "env_nested_delimiter": "__"}
90
98
 
91
99
  @classmethod