Qubx 0.6.85__tar.gz → 0.6.87__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.

Potentially problematic release.


This version of Qubx might be problematic. Click here for more details.

Files changed (216) hide show
  1. {qubx-0.6.85 → qubx-0.6.87}/PKG-INFO +1 -1
  2. {qubx-0.6.85 → qubx-0.6.87}/pyproject.toml +1 -1
  3. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/management.py +3 -2
  4. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/runner.py +1 -1
  5. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/cli/commands.py +46 -1
  6. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +1 -1
  7. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/funding_rate.py +3 -3
  8. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/reader.py +3 -2
  9. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/interfaces.py +7 -6
  10. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/metrics.py +74 -14
  11. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/emitters/base.py +23 -14
  12. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/emitters/composite.py +13 -0
  13. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/emitters/csv.py +2 -1
  14. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/emitters/indicator.py +4 -2
  15. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/emitters/inmemory.py +5 -4
  16. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/emitters/prometheus.py +2 -2
  17. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/emitters/questdb.py +16 -10
  18. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/exporters/formatters/__init__.py +8 -1
  19. qubx-0.6.87/src/qubx/exporters/formatters/target_position.py +78 -0
  20. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/health/base.py +7 -10
  21. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/runner/configs.py +120 -17
  22. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/runner/runner.py +6 -6
  23. {qubx-0.6.85 → qubx-0.6.87}/LICENSE +0 -0
  24. {qubx-0.6.85 → qubx-0.6.87}/README.md +0 -0
  25. {qubx-0.6.85 → qubx-0.6.87}/build.py +0 -0
  26. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/__init__.py +0 -0
  27. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/_nb_magic.py +0 -0
  28. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/__init__.py +0 -0
  29. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/account.py +0 -0
  30. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/broker.py +0 -0
  31. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/data.py +0 -0
  32. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/ome.py +0 -0
  33. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/optimization.py +0 -0
  34. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/sentinels.py +0 -0
  35. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/simulated_data.py +0 -0
  36. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/simulated_exchange.py +0 -0
  37. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/simulator.py +0 -0
  38. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/backtester/utils.py +0 -0
  39. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/cli/__init__.py +0 -0
  40. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/cli/deploy.py +0 -0
  41. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/cli/misc.py +0 -0
  42. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/cli/release.py +0 -0
  43. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/cli/tui.py +0 -0
  44. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/__init__.py +0 -0
  45. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/account.py +0 -0
  46. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  47. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  48. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/broker.py +0 -0
  49. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  50. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/data.py +0 -0
  51. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  52. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  53. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  54. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  55. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  56. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  57. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  58. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  59. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  60. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  61. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  62. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/factory.py +0 -0
  63. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  64. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  65. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  66. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  67. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  68. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  69. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  70. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  71. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  72. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  73. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  74. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  75. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/utils.py +0 -0
  76. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  77. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/tardis/data.py +0 -0
  78. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/connectors/tardis/utils.py +0 -0
  79. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/__init__.py +0 -0
  80. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/account.py +0 -0
  81. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/basics.py +0 -0
  82. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/context.py +0 -0
  83. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/deque.py +0 -0
  84. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/errors.py +0 -0
  85. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/exceptions.py +0 -0
  86. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/helpers.py +0 -0
  87. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/initializer.py +0 -0
  88. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/loggers.py +0 -0
  89. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/lookups.py +0 -0
  90. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/mixins/__init__.py +0 -0
  91. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/mixins/market.py +0 -0
  92. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/mixins/processing.py +0 -0
  93. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/mixins/subscription.py +0 -0
  94. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/mixins/trading.py +0 -0
  95. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/mixins/universe.py +0 -0
  96. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/mixins/utils.py +0 -0
  97. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/series.pxd +0 -0
  98. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/series.pyi +0 -0
  99. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/series.pyx +0 -0
  100. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/stale_data_detector.py +0 -0
  101. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/utils.pyi +0 -0
  102. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/core/utils.pyx +0 -0
  103. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/data/__init__.py +0 -0
  104. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/data/composite.py +0 -0
  105. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/data/helpers.py +0 -0
  106. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/data/hft.py +0 -0
  107. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/data/readers.py +0 -0
  108. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/data/registry.py +0 -0
  109. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/data/tardis.py +0 -0
  110. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/emitters/__init__.py +0 -0
  111. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/exporters/__init__.py +0 -0
  112. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/exporters/composite.py +0 -0
  113. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/exporters/formatters/base.py +0 -0
  114. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/exporters/formatters/incremental.py +0 -0
  115. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/exporters/formatters/slack.py +0 -0
  116. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/exporters/redis_streams.py +0 -0
  117. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/exporters/slack.py +0 -0
  118. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/features/__init__.py +0 -0
  119. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/features/core.py +0 -0
  120. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/features/orderbook.py +0 -0
  121. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/features/price.py +0 -0
  122. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/features/trades.py +0 -0
  123. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/features/utils.py +0 -0
  124. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/gathering/simplest.py +0 -0
  125. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/health/__init__.py +0 -0
  126. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/loggers/__init__.py +0 -0
  127. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/loggers/csv.py +0 -0
  128. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/loggers/factory.py +0 -0
  129. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/loggers/inmemory.py +0 -0
  130. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/loggers/mongo.py +0 -0
  131. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/math/__init__.py +0 -0
  132. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/math/stats.py +0 -0
  133. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/notifications/__init__.py +0 -0
  134. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/notifications/composite.py +0 -0
  135. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/notifications/slack.py +0 -0
  136. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/notifications/throttler.py +0 -0
  137. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/pandaz/__init__.py +0 -0
  138. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/pandaz/ta.py +0 -0
  139. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/pandaz/utils.py +0 -0
  140. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/_build.py +0 -0
  141. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/crypto-fees.ini +0 -0
  142. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  143. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  144. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  145. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  146. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  147. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  148. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  149. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  150. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  151. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  152. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  153. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restarts/__init__.py +0 -0
  154. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restarts/state_resolvers.py +0 -0
  155. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restarts/time_finders.py +0 -0
  156. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restorers/__init__.py +0 -0
  157. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restorers/balance.py +0 -0
  158. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restorers/factory.py +0 -0
  159. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restorers/interfaces.py +0 -0
  160. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restorers/position.py +0 -0
  161. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restorers/signal.py +0 -0
  162. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restorers/state.py +0 -0
  163. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/restorers/utils.py +0 -0
  164. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/ta/__init__.py +0 -0
  165. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/ta/indicators.pxd +0 -0
  166. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/ta/indicators.pyi +0 -0
  167. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/ta/indicators.pyx +0 -0
  168. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/__init__.py +0 -0
  169. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/base.py +0 -0
  170. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  171. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/project/config.yml.j2 +0 -0
  172. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  173. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  174. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  175. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  176. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  177. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/project/template.yml +0 -0
  178. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  179. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  180. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/simple/config.yml.j2 +0 -0
  181. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  182. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  183. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  184. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/templates/simple/template.yml +0 -0
  185. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/trackers/__init__.py +0 -0
  186. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/trackers/advanced.py +0 -0
  187. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/trackers/composite.py +0 -0
  188. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/trackers/rebalancers.py +0 -0
  189. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/trackers/riskctrl.py +0 -0
  190. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/trackers/sizers.py +0 -0
  191. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/__init__.py +0 -0
  192. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/_pyxreloader.py +0 -0
  193. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/charting/lookinglass.py +0 -0
  194. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  195. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/charting/orderbook.py +0 -0
  196. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/collections.py +0 -0
  197. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/marketdata/binance.py +0 -0
  198. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/marketdata/ccxt.py +0 -0
  199. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/marketdata/dukas.py +0 -0
  200. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/misc.py +0 -0
  201. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/ntp.py +0 -0
  202. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/numbers_utils.py +0 -0
  203. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/orderbook.py +0 -0
  204. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/plotting/__init__.py +0 -0
  205. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/plotting/dashboard.py +0 -0
  206. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/plotting/data.py +0 -0
  207. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/plotting/interfaces.py +0 -0
  208. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  209. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  210. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/questdb.py +0 -0
  211. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/runner/__init__.py +0 -0
  212. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  213. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/runner/accounts.py +0 -0
  214. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/runner/factory.py +0 -0
  215. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/time.py +0 -0
  216. {qubx-0.6.85 → qubx-0.6.87}/src/qubx/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.85
3
+ Version: 0.6.87
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  License-File: LICENSE
6
6
  Author: Dmitry Marienko
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.85"
7
+ version = "0.6.87"
8
8
  description = "Qubx - Quantitative Trading Framework"
9
9
  authors = [ "Dmitry Marienko <dmitry.marienko@xlydian.com>", "Yuriy Arabskyy <yuriy.arabskyy@xlydian.com>",]
10
10
  readme = "README.md"
@@ -327,10 +327,11 @@ class BacktestsResultsManager:
327
327
  if not as_table:
328
328
  print(_s)
329
329
 
330
+ dd_column = "max_dd_pct" if "max_dd_pct" in metrics else "mdd_pct"
330
331
  if with_metrics:
331
332
  _m_repr = (
332
333
  pd.DataFrame.from_dict(metrics, orient="index")
333
- .T[["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]]
334
+ .T[["gain", "cagr", "sharpe", "qr", dd_column, "mdd_usd", "fees", "execs"]]
334
335
  .astype(float)
335
336
  )
336
337
  _m_repr = _m_repr.round(3).to_string(index=False)
@@ -345,7 +346,7 @@ class BacktestsResultsManager:
345
346
  metrics = {
346
347
  m: round(v, 3)
347
348
  for m, v in metrics.items()
348
- if m in ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
349
+ if m in ["gain", "cagr", "sharpe", "qr", dd_column, "mdd_usd", "fees", "execs"]
349
350
  }
350
351
  _t_rep.append(
351
352
  {"Index": info.get("idx", ""), "Strategy": name}
@@ -478,7 +478,7 @@ class SimulationRunner:
478
478
  )
479
479
 
480
480
  if self.emitter is not None:
481
- self.emitter.set_time_provider(simulated_clock)
481
+ self.emitter.set_context(ctx)
482
482
 
483
483
  # - setup base subscription from spec
484
484
  if ctx.get_base_subscription() == DataType.NONE:
@@ -137,6 +137,51 @@ def ls(directory: str):
137
137
  ls_strats(directory)
138
138
 
139
139
 
140
+ @main.command()
141
+ @click.argument("config-file", type=Path, required=True)
142
+ @click.option(
143
+ "--no-check-imports",
144
+ is_flag=True,
145
+ default=False,
146
+ help="Skip checking if strategy class can be imported",
147
+ show_default=True,
148
+ )
149
+ def validate(config_file: Path, no_check_imports: bool):
150
+ """
151
+ Validates a strategy configuration file without running it.
152
+
153
+ Checks for:
154
+ - Valid YAML syntax
155
+ - Required configuration fields
156
+ - Strategy class exists and can be imported (unless --no-check-imports)
157
+ - Exchange configurations are valid
158
+ - Simulation parameters are valid (if present)
159
+
160
+ Returns exit code 0 if valid, 1 if invalid.
161
+ """
162
+ from qubx.utils.runner.configs import validate_strategy_config
163
+
164
+ result = validate_strategy_config(config_file, check_imports=not no_check_imports)
165
+
166
+ if result.valid:
167
+ click.echo(click.style("✓ Configuration is valid", fg="green", bold=True))
168
+ if result.warnings:
169
+ click.echo(click.style("\nWarnings:", fg="yellow", bold=True))
170
+ for warning in result.warnings:
171
+ click.echo(click.style(f" - {warning}", fg="yellow"))
172
+ raise SystemExit(0)
173
+ else:
174
+ click.echo(click.style("✗ Configuration is invalid", fg="red", bold=True))
175
+ click.echo(click.style("\nErrors:", fg="red", bold=True))
176
+ for error in result.errors:
177
+ click.echo(click.style(f" - {error}", fg="red"))
178
+ if result.warnings:
179
+ click.echo(click.style("\nWarnings:", fg="yellow", bold=True))
180
+ for warning in result.warnings:
181
+ click.echo(click.style(f" - {warning}", fg="yellow"))
182
+ raise SystemExit(1)
183
+
184
+
140
185
  @main.command()
141
186
  @click.argument(
142
187
  "directory",
@@ -358,7 +403,7 @@ def init(
358
403
  The generated strategy can be run immediately with:
359
404
  poetry run qubx run --config config.yml --paper
360
405
  """
361
- from qubx.templates import TemplateManager, TemplateError
406
+ from qubx.templates import TemplateError, TemplateManager
362
407
 
363
408
  try:
364
409
  manager = TemplateManager()
@@ -8,7 +8,7 @@ from ...adapters.polling_adapter import PollingConfig, PollingToWebSocketAdapter
8
8
  from ..base import CcxtFuturePatchMixin
9
9
 
10
10
  # Constants
11
- FUNDING_RATE_DEFAULT_POLL_MINUTES = 5
11
+ FUNDING_RATE_DEFAULT_POLL_MINUTES = 1
12
12
  FUNDING_RATE_HOUR_MS = 60 * 60 * 1000 # 1 hour in milliseconds
13
13
 
14
14
 
@@ -71,7 +71,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
71
71
  channel.send((instrument, DataType.FUNDING_RATE, funding_rate, False))
72
72
 
73
73
  # Emit payment if funding interval changed
74
- if self._should_emit_payment(instrument, funding_rate):
74
+ if self._should_emit_payment(instrument, funding_rate, current_time):
75
75
  payment = self._create_funding_payment(instrument)
76
76
  channel.send((instrument, DataType.FUNDING_PAYMENT, payment, False))
77
77
 
@@ -101,7 +101,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
101
101
  stream_name=name,
102
102
  )
103
103
 
104
- def _should_emit_payment(self, instrument: Instrument, rate: FundingRate) -> bool:
104
+ def _should_emit_payment(self, instrument: Instrument, rate: FundingRate, current_time: dt_64) -> bool:
105
105
  """
106
106
  Determine if a funding payment should be emitted.
107
107
 
@@ -132,7 +132,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
132
132
  return False
133
133
 
134
134
  # Emit if next_funding_time has advanced (new funding period started)
135
- if rate.next_funding_time > last_info["payment_time"]:
135
+ if rate.next_funding_time > last_info["payment_time"] and current_time > last_info["payment_time"]:
136
136
  # Store payment info for _create_funding_payment
137
137
  self._pending_funding_rates[f"{key}_payment"] = {
138
138
  "rate": last_info["rate"].rate,
@@ -20,7 +20,7 @@ from .utils import ccxt_find_instrument, instrument_to_ccxt_symbol
20
20
 
21
21
  @reader("ccxt")
22
22
  class CcxtDataReader(DataReader):
23
- SUPPORTED_DATA_TYPES = {"ohlc", "funding_payment"}
23
+ SUPPORTED_DATA_TYPES = {"ohlc"}
24
24
 
25
25
  _exchanges: dict[str, Exchange]
26
26
  _loop: AsyncThreadLoop
@@ -74,7 +74,8 @@ class CcxtDataReader(DataReader):
74
74
  if instrument is None:
75
75
  return []
76
76
 
77
- _timeframe = pd.Timedelta(timeframe or "1m")
77
+ timeframe = timeframe or "1m"
78
+ _timeframe = pd.Timedelta(timeframe)
78
79
  _start, _stop = self._get_start_stop(start, stop, _timeframe)
79
80
 
80
81
  if _start > _stop:
@@ -2050,7 +2050,7 @@ class IMetricEmitter:
2050
2050
  self,
2051
2051
  name: str,
2052
2052
  value: float,
2053
- tags: dict[str, str] | None = None,
2053
+ tags: dict[str, Any] | None = None,
2054
2054
  timestamp: dt_64 | None = None,
2055
2055
  instrument: Instrument | None = None,
2056
2056
  ) -> None:
@@ -2092,15 +2092,16 @@ class IMetricEmitter:
2092
2092
  """
2093
2093
  pass
2094
2094
 
2095
- def set_time_provider(self, time_provider: ITimeProvider) -> None:
2095
+ def set_context(self, context: "IStrategyContext") -> None:
2096
2096
  """
2097
- Set the time provider for the metric emitter.
2097
+ Set the strategy context for the metric emitter.
2098
2098
 
2099
- This method is used to set the time provider that will be used to get timestamps
2100
- when no explicit timestamp is provided in the emit method.
2099
+ This method is used to set the context that provides access to time and simulation state.
2100
+ The context is used to automatically add is_live tag and get timestamps when no explicit
2101
+ timestamp is provided in the emit method.
2101
2102
 
2102
2103
  Args:
2103
- time_provider: The time provider to use
2104
+ context: The strategy context to use
2104
2105
  """
2105
2106
  pass
2106
2107
 
@@ -175,7 +175,7 @@ def cagr(returns, periods=DAILY):
175
175
 
176
176
  cumrets = (returns + 1).cumprod(axis=0)
177
177
  years = len(cumrets) / float(periods)
178
- return (cumrets.iloc[-1] ** (1.0 / years)) - 1.0
178
+ return ((cumrets.iloc[-1] ** (1.0 / years)) - 1.0) * 100
179
179
 
180
180
 
181
181
  def calmar_ratio(returns, periods=DAILY):
@@ -747,6 +747,11 @@ class TradingSessionResult:
747
747
  """Get number of executions"""
748
748
  return len(self.executions_log)
749
749
 
750
+ @property
751
+ def turnover(self) -> float:
752
+ """Get average daily turnover as percentage of equity"""
753
+ return self.performance().get("avg_daily_turnover", 0.0)
754
+
750
755
  @property
751
756
  def leverage(self) -> pd.Series:
752
757
  """Get leverage over time"""
@@ -779,7 +784,7 @@ class TradingSessionResult:
779
784
  for k in [
780
785
  "equity", "drawdown_usd", "drawdown_pct",
781
786
  "compound_returns", "returns_daily", "returns", "monthly_returns",
782
- "rolling_sharpe", "long_value", "short_value",
787
+ "rolling_sharpe", "long_value", "short_value", "turnover",
783
788
  ]:
784
789
  self._metrics.pop(k, None)
785
790
  # fmt: on
@@ -1381,16 +1386,21 @@ def portfolio_metrics(
1381
1386
  execs = len(executions_log)
1382
1387
  mdd_pct = 100 * dd_data / equity.cummax() if execs > 0 else pd.Series(0, index=equity.index)
1383
1388
  sheet["equity"] = equity
1384
- sheet["gain"] = sheet["equity"].iloc[-1] - sheet["equity"].iloc[0]
1385
- sheet["cagr"] = cagr(returns_daily, performance_statistics_period)
1386
1389
  sheet["sharpe"] = sharpe_ratio(returns_daily, risk_free, performance_statistics_period)
1390
+ sheet["cagr"] = cagr(returns_daily, performance_statistics_period)
1391
+
1392
+ # turnover calculation
1393
+ symbols = list(set(portfolio_log.columns.str.split("_").str.get(0).values))
1394
+ turnover_series = calculate_turnover(portfolio_log, symbols, equity, resample="1d")
1395
+ sheet["turnover"] = turnover_series
1396
+ sheet["daily_turnover"] = turnover_series.mean() if len(turnover_series) > 0 else 0.0
1397
+
1387
1398
  sheet["qr"] = qr(equity) if execs > 0 else 0
1388
- sheet["drawdown_usd"] = dd_data
1399
+ sheet["mdd_pct"] = max(mdd_pct)
1389
1400
  sheet["drawdown_pct"] = mdd_pct
1401
+ sheet["drawdown_usd"] = dd_data
1390
1402
  # 25-May-2019: MDE fixed Max DD pct calculations
1391
- sheet["max_dd_pct"] = max(mdd_pct)
1392
1403
  # sheet["max_dd_pct_on_init"] = 100 * mdd / init_cash
1393
- sheet["mdd_usd"] = mdd
1394
1404
  sheet["mdd_start"] = equity.index[ddstart]
1395
1405
  sheet["mdd_peak"] = equity.index[ddpeak]
1396
1406
  sheet["mdd_recover"] = equity.index[ddrecover]
@@ -1403,12 +1413,12 @@ def portfolio_metrics(
1403
1413
  )
1404
1414
  sheet["calmar"] = calmar_ratio(returns_daily, performance_statistics_period)
1405
1415
  # sheet["ann_vol"] = annual_volatility(returns_daily)
1406
- sheet["tail_ratio"] = tail_ratio(returns_daily)
1407
- sheet["stability"] = stability_of_returns(returns_daily)
1416
+ # sheet["tail_ratio"] = tail_ratio(returns_daily)
1417
+ # sheet["stability"] = stability_of_returns(returns_daily)
1408
1418
  sheet["monthly_returns"] = aggregate_returns(returns_daily, convert_to="mon")
1409
1419
  r_m = np.mean(returns_daily)
1410
1420
  r_s = np.std(returns_daily)
1411
- sheet["var"] = var_cov_var(init_cash, r_m, r_s)
1421
+ # sheet["var"] = var_cov_var(init_cash, r_m, r_s)
1412
1422
  sheet["avg_return"] = 100 * r_m
1413
1423
 
1414
1424
  # portfolio market values
@@ -1416,6 +1426,8 @@ def portfolio_metrics(
1416
1426
  sheet["long_value"] = mkt_value[mkt_value > 0].sum(axis=1).fillna(0)
1417
1427
  sheet["short_value"] = mkt_value[mkt_value < 0].sum(axis=1).fillna(0)
1418
1428
 
1429
+ sheet["gain"] = sheet["equity"].iloc[-1] - sheet["equity"].iloc[0]
1430
+ sheet["mdd_usd"] = mdd
1419
1431
  # total commissions
1420
1432
  sheet["fees"] = pft_total["Total_Commissions"].iloc[-1]
1421
1433
 
@@ -1423,9 +1435,10 @@ def portfolio_metrics(
1423
1435
  funding_columns = pft_total.filter(regex=".*_Funding")
1424
1436
  if not funding_columns.empty:
1425
1437
  total_funding = funding_columns.sum(axis=1)
1426
- sheet["funding_pnl"] = 100 * total_funding.iloc[-1] / init_cash # as percentage of initial capital
1427
- else:
1428
- sheet["funding_pnl"] = 0.0
1438
+ if total_funding.iloc[-1] != 0:
1439
+ sheet["funding_pnl"] = 100 * total_funding.iloc[-1] / init_cash # as percentage of initial capital
1440
+ # else:
1441
+ # sheet["funding_pnl"] = 0.0
1429
1442
 
1430
1443
  # executions metrics
1431
1444
  sheet["execs"] = execs
@@ -1725,7 +1738,7 @@ def _tearsheet_single(
1725
1738
  ay = sbp(_n, 5)
1726
1739
  plt.plot(lev, c="c", lw=1.5, label="Leverage")
1727
1740
  plt.subplots_adjust(hspace=0)
1728
- return pd.DataFrame(report).T.round(3)
1741
+ return pd.DataFrame(report).T.round(2)
1729
1742
 
1730
1743
 
1731
1744
  def calculate_leverage(
@@ -1828,6 +1841,53 @@ def calculate_pnl_per_symbol(
1828
1841
  return df
1829
1842
 
1830
1843
 
1844
+ def calculate_turnover(
1845
+ portfolio_log: pd.DataFrame,
1846
+ symbols: list[str],
1847
+ equity: pd.Series,
1848
+ resample: str = "1d",
1849
+ ) -> pd.Series:
1850
+ """
1851
+ Calculate daily turnover as percentage of equity.
1852
+
1853
+ Turnover measures trading activity by calculating the absolute value of position changes
1854
+ multiplied by price, then dividing by equity.
1855
+
1856
+ Args:
1857
+ portfolio_log: Portfolio log dataframe with position and price columns
1858
+ symbols: List of symbols to calculate turnover for
1859
+ equity: Equity curve series
1860
+ resample: Resampling period for turnover calculation (default "1d")
1861
+
1862
+ Returns:
1863
+ pd.Series: Daily turnover as percentage of equity
1864
+ """
1865
+ position_diffs = []
1866
+
1867
+ for symbol in symbols:
1868
+ pos_col = f"{symbol}_Pos"
1869
+ price_col = f"{symbol}_Price"
1870
+
1871
+ if pos_col in portfolio_log.columns and price_col in portfolio_log.columns:
1872
+ # Calculate absolute position change multiplied by price (notional value)
1873
+ position_diff = portfolio_log[pos_col].diff().abs() * portfolio_log[price_col]
1874
+ position_diffs.append(position_diff)
1875
+
1876
+ if not position_diffs:
1877
+ return pd.Series(0, index=equity.index)
1878
+
1879
+ # Sum all position changes and resample to specified period
1880
+ notional_turnover = pd.concat(position_diffs, axis=1).sum(axis=1).resample(resample).sum()
1881
+
1882
+ # Resample equity to match turnover frequency
1883
+ equity_resampled = equity.resample(resample).last()
1884
+
1885
+ # Calculate turnover as percentage of equity
1886
+ daily_turnover = notional_turnover.div(equity_resampled).mul(100).fillna(0)
1887
+
1888
+ return daily_turnover
1889
+
1890
+
1831
1891
  def chart_signals(
1832
1892
  result: TradingSessionResult,
1833
1893
  symbol: str,
@@ -4,13 +4,13 @@ Base Metric Emitter.
4
4
  This module provides a base implementation of IMetricEmitter that can be extended by other emitters.
5
5
  """
6
6
 
7
- from typing import Dict, List, Optional, Set
7
+ from typing import Any, Dict, List, Optional, Set
8
8
 
9
9
  import pandas as pd
10
10
 
11
11
  from qubx import logger
12
12
  from qubx.core.basics import Instrument, Signal, TargetPosition, dt_64
13
- from qubx.core.interfaces import IAccountViewer, IMetricEmitter, IStrategyContext, ITimeProvider
13
+ from qubx.core.interfaces import IAccountViewer, IMetricEmitter, IStrategyContext
14
14
 
15
15
 
16
16
  class BaseMetricEmitter(IMetricEmitter):
@@ -35,7 +35,7 @@ class BaseMetricEmitter(IMetricEmitter):
35
35
  }
36
36
 
37
37
  def __init__(
38
- self, stats_to_emit: Optional[List[str]] = None, stats_interval: str = "1m", tags: dict[str, str] | None = None
38
+ self, stats_to_emit: Optional[List[str]] = None, stats_interval: str = "1m", tags: dict[str, Any] | None = None
39
39
  ):
40
40
  """
41
41
  Initialize the Base Metric Emitter.
@@ -49,18 +49,19 @@ class BaseMetricEmitter(IMetricEmitter):
49
49
  self._stats_interval = pd.Timedelta(stats_interval)
50
50
  self._default_tags = tags or {}
51
51
  self._last_emission_time = None
52
- self._time_provider = None
52
+ self._context = None
53
53
 
54
- def _merge_tags(self, tags: dict[str, str] | None = None, instrument: Instrument | None = None) -> dict[str, str]:
54
+ def _merge_tags(self, tags: dict[str, Any] | None = None, instrument: Instrument | None = None) -> dict[str, Any]:
55
55
  """
56
56
  Merge default tags with provided tags and instrument tags if provided.
57
+ Also automatically adds is_live tag based on context's simulation state.
57
58
 
58
59
  Args:
59
60
  tags: Additional tags to merge with default tags
60
61
  instrument: Optional instrument to add symbol and exchange tags from
61
62
 
62
63
  Returns:
63
- Dict[str, str]: Merged tags dictionary
64
+ Dict[str, Any]: Merged tags dictionary
64
65
  """
65
66
  result = self._default_tags.copy()
66
67
 
@@ -70,9 +71,13 @@ class BaseMetricEmitter(IMetricEmitter):
70
71
  if instrument:
71
72
  result.update({"symbol": instrument.symbol, "exchange": instrument.exchange})
72
73
 
74
+ # Add is_live tag based on context's simulation state
75
+ if self._context is not None:
76
+ result["is_live"] = not self._context.is_simulation
77
+
73
78
  return result
74
79
 
75
- def _emit_impl(self, name: str, value: float, tags: Dict[str, str], timestamp: dt_64 | None = None) -> None:
80
+ def _emit_impl(self, name: str, value: float, tags: Dict[str, Any], timestamp: dt_64 | None = None) -> None:
76
81
  """
77
82
  Implementation of emit to be overridden by subclasses.
78
83
 
@@ -88,7 +93,7 @@ class BaseMetricEmitter(IMetricEmitter):
88
93
  self,
89
94
  name: str,
90
95
  value: float,
91
- tags: dict[str, str] | None = None,
96
+ tags: dict[str, Any] | None = None,
92
97
  timestamp: dt_64 | None = None,
93
98
  instrument: Instrument | None = None,
94
99
  ) -> None:
@@ -102,19 +107,19 @@ class BaseMetricEmitter(IMetricEmitter):
102
107
  timestamp: Optional timestamp for the metric (defaults to current time)
103
108
  instrument: Optional instrument to add symbol and exchange tags from
104
109
  """
105
- if self._time_provider is not None and timestamp is None:
106
- timestamp = self._time_provider.time()
110
+ if self._context is not None and timestamp is None:
111
+ timestamp = self._context.time()
107
112
  merged_tags = self._merge_tags(tags, instrument)
108
113
  self._emit_impl(name, float(value), merged_tags, timestamp)
109
114
 
110
- def set_time_provider(self, time_provider: ITimeProvider) -> None:
115
+ def set_context(self, context: IStrategyContext) -> None:
111
116
  """
112
- Set the time provider for the metric emitter.
117
+ Set the strategy context for the metric emitter.
113
118
 
114
119
  Args:
115
- time_provider: The time provider to use
120
+ context: The strategy context to use
116
121
  """
117
- self._time_provider = time_provider
122
+ self._context = context
118
123
 
119
124
  def emit_strategy_stats(self, context: IStrategyContext) -> None:
120
125
  """
@@ -126,6 +131,10 @@ class BaseMetricEmitter(IMetricEmitter):
126
131
  Args:
127
132
  context: The strategy context to get statistics from
128
133
  """
134
+ # Store context to ensure is_live tag is added
135
+ if self._context is None:
136
+ self._context = context
137
+
129
138
  try:
130
139
  # Get current timestamp
131
140
  current_time = context.time()
@@ -85,6 +85,19 @@ class CompositeMetricEmitter(BaseMetricEmitter):
85
85
  except Exception as e:
86
86
  logger.error(f"Error emitting signals to {emitter.__class__.__name__}: {e}")
87
87
 
88
+ def set_context(self, context: IStrategyContext) -> None:
89
+ """
90
+ Set the strategy context for all child emitters.
91
+
92
+ Args:
93
+ context: The strategy context to use
94
+ """
95
+ for emitter in self._emitters:
96
+ try:
97
+ emitter.set_context(context)
98
+ except Exception as e:
99
+ logger.error(f"Error setting context on {emitter.__class__.__name__}: {e}")
100
+
88
101
  def notify(self, context: IStrategyContext) -> None:
89
102
  for emitter in self._emitters:
90
103
  try:
@@ -6,6 +6,7 @@ This module provides an implementation of IMetricEmitter that exports metrics to
6
6
 
7
7
  import os
8
8
  from pathlib import Path
9
+ from typing import Any
9
10
 
10
11
  from qubx import logger
11
12
  from qubx.core.basics import Signal, dt_64
@@ -27,7 +28,7 @@ class CSVMetricEmitter(BaseMetricEmitter):
27
28
  file_path: str | None = None,
28
29
  stats_to_emit: list[str] | None = None,
29
30
  stats_interval: str = "1m",
30
- tags: dict[str, str] | None = None,
31
+ tags: dict[str, Any] | None = None,
31
32
  ):
32
33
  """
33
34
  Initialize the CSV Metric Emitter.
@@ -5,6 +5,8 @@ This module provides the IndicatorEmitter class that can wrap around any indicat
5
5
  and automatically emit their values when there are updates.
6
6
  """
7
7
 
8
+ from typing import Any
9
+
8
10
  import numpy as np
9
11
  import pandas as pd
10
12
 
@@ -43,7 +45,7 @@ class IndicatorEmitter(Indicator):
43
45
  metric_emitter: IMetricEmitter,
44
46
  metric_name: str | None = None,
45
47
  instrument: Instrument | None = None,
46
- tags: dict[str, str] | None = None,
48
+ tags: dict[str, Any] | None = None,
47
49
  emit_on_new_item_only: bool = True,
48
50
  ):
49
51
  """
@@ -149,7 +151,7 @@ class IndicatorEmitter(Indicator):
149
151
  metric_emitter: IMetricEmitter,
150
152
  metric_name: str | None = None,
151
153
  instrument: Instrument | None = None,
152
- tags: dict[str, str] | None = None,
154
+ tags: dict[str, Any] | None = None,
153
155
  emit_on_new_item_only: bool = True,
154
156
  ) -> "Indicator":
155
157
  """
@@ -5,7 +5,7 @@ This module provides an implementation of IMetricEmitter that stores metrics in
5
5
  using a pandas DataFrame for easy access and analysis.
6
6
  """
7
7
 
8
- from typing import Optional
8
+ from typing import Any, cast
9
9
 
10
10
  import pandas as pd
11
11
 
@@ -28,7 +28,7 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
28
28
  self,
29
29
  stats_to_emit: list[str] | None = None,
30
30
  stats_interval: str = "1m",
31
- tags: dict[str, str] | None = None,
31
+ tags: dict[str, Any] | None = None,
32
32
  max_rows: int | None = None,
33
33
  ):
34
34
  """
@@ -135,7 +135,7 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
135
135
  if not self._rows:
136
136
  df = pd.DataFrame(columns=["timestamp", "name", "value", "symbol", "exchange"])
137
137
  else:
138
- df = pd.DataFrame(self._rows)
138
+ df = pd.DataFrame(self._rows.copy())
139
139
  # Ensure correct dtypes
140
140
  df = df.astype(
141
141
  {
@@ -163,7 +163,8 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
163
163
  df = df[df["timestamp"] >= start_time]
164
164
  if end_time is not None:
165
165
  df = df[df["timestamp"] <= end_time]
166
- return df
166
+
167
+ return cast(pd.DataFrame, df)
167
168
 
168
169
  def get_latest_metrics(
169
170
  self, instrument: Instrument | None = None, symbol: str | None = None, exchange: str | None = None
@@ -4,7 +4,7 @@ Prometheus Metric Emitter.
4
4
  This module provides an implementation of IMetricEmitter that exports metrics to Prometheus.
5
5
  """
6
6
 
7
- from typing import Dict, List, Literal, Optional
7
+ from typing import Any, Dict, List, Literal, Optional
8
8
 
9
9
  from prometheus_client import REGISTRY, Counter, Gauge, Summary, push_to_gateway
10
10
 
@@ -178,7 +178,7 @@ class PrometheusMetricEmitter(BaseMetricEmitter):
178
178
  self,
179
179
  name: str,
180
180
  value: float,
181
- tags: dict[str, str] | None = None,
181
+ tags: dict[str, Any] | None = None,
182
182
  timestamp: dt_64 | None = None,
183
183
  metric_type: MetricType = "gauge",
184
184
  ) -> None:
@@ -6,6 +6,7 @@ This module provides an implementation of IMetricEmitter that exports metrics to
6
6
 
7
7
  import datetime
8
8
  from concurrent.futures import ThreadPoolExecutor
9
+ from typing import Any
9
10
 
10
11
  import pandas as pd
11
12
  from questdb.ingress import Sender
@@ -24,6 +25,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
24
25
  This emitter sends metrics to QuestDB with custom timestamps and tags.
25
26
  """
26
27
 
28
+ SYMBOL_TAGS = ["symbol", "exchange", "type", "environment", "strategy"]
29
+
27
30
  def __init__(
28
31
  self,
29
32
  host: str = "localhost",
@@ -33,7 +36,7 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
33
36
  stats_to_emit: list[str] | None = None,
34
37
  stats_interval: str = "1m",
35
38
  flush_interval: str = "5s",
36
- tags: dict[str, str] | None = None,
39
+ tags: dict[str, Any] | None = None,
37
40
  max_workers: int = 1,
38
41
  ):
39
42
  """
@@ -143,10 +146,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
143
146
  return
144
147
 
145
148
  # Prepare symbols (tags) and columns (values)
146
- symbols = {"metric_name": name}
147
- symbols.update(tags) # Add all tags as symbols
148
-
149
- columns: dict = {"value": round(value, 5)} # Add the value as a column
149
+ symbols = self._pop_symbols(tags)
150
+ columns: dict = {"metric_name": name, "value": round(value, 5), **tags}
150
151
 
151
152
  # Use the provided timestamp if available, otherwise use current time
152
153
  dt_timestamp = self._convert_timestamp(timestamp) if timestamp is not None else datetime.datetime.now()
@@ -261,11 +262,7 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
261
262
 
262
263
  # Use _merge_tags to get properly merged tags
263
264
  merged_tags = self._merge_tags({}, signal.instrument)
264
-
265
- symbols = {
266
- "group_name": signal.group if signal.group else "",
267
- }
268
- symbols.update(merged_tags) # Add merged tags
265
+ symbols = self._pop_symbols(merged_tags)
269
266
 
270
267
  columns = {
271
268
  "signal": float(signal.signal),
@@ -277,6 +274,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
277
274
  "comment": signal.comment if signal.comment else "",
278
275
  # "options": json.dumps(signal.options) if signal.options else "{}",
279
276
  "is_service": bool(signal.is_service),
277
+ "group_name": signal.group if signal.group else "",
278
+ **merged_tags,
280
279
  }
281
280
 
282
281
  # Convert timestamp - signal.time is always dt_64, no need to check for string
@@ -287,3 +286,10 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
287
286
 
288
287
  except Exception as e:
289
288
  logger.error(f"[QuestDBMetricEmitter] Failed to emit signals to QuestDB: {e}")
289
+
290
+ def _pop_symbols(self, tags: dict[str, str]) -> dict[str, str]:
291
+ symbols = {}
292
+ for symbol_name in self.SYMBOL_TAGS:
293
+ if symbol_name in tags:
294
+ symbols[symbol_name] = tags.pop(symbol_name)
295
+ return symbols