Qubx 0.6.78__tar.gz → 0.6.84__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 (215) hide show
  1. {qubx-0.6.78 → qubx-0.6.84}/PKG-INFO +4 -2
  2. {qubx-0.6.78 → qubx-0.6.84}/pyproject.toml +1 -1
  3. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/management.py +12 -1
  4. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/runner.py +9 -3
  5. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/account.py +22 -4
  6. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchange_manager.py +66 -63
  7. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/account.py +2 -1
  8. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/basics.py +4 -0
  9. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/metrics.py +101 -2
  10. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/data/helpers.py +34 -12
  11. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/ta/indicators.pxd +5 -1
  12. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/ta/indicators.pyi +7 -2
  13. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/ta/indicators.pyx +136 -18
  14. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/trackers/riskctrl.py +83 -3
  15. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/charting/lookinglass.py +42 -0
  16. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/misc.py +23 -6
  17. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/runner/configs.py +1 -0
  18. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/runner/runner.py +27 -2
  19. {qubx-0.6.78 → qubx-0.6.84}/LICENSE +0 -0
  20. {qubx-0.6.78 → qubx-0.6.84}/README.md +0 -0
  21. {qubx-0.6.78 → qubx-0.6.84}/build.py +0 -0
  22. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/__init__.py +0 -0
  23. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/_nb_magic.py +0 -0
  24. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/__init__.py +0 -0
  25. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/account.py +0 -0
  26. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/broker.py +0 -0
  27. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/data.py +0 -0
  28. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/ome.py +0 -0
  29. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/optimization.py +0 -0
  30. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/sentinels.py +0 -0
  31. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/simulated_data.py +0 -0
  32. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/simulated_exchange.py +0 -0
  33. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/simulator.py +0 -0
  34. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/backtester/utils.py +0 -0
  35. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/cli/__init__.py +0 -0
  36. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/cli/commands.py +0 -0
  37. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/cli/deploy.py +0 -0
  38. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/cli/misc.py +0 -0
  39. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/cli/release.py +0 -0
  40. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/cli/tui.py +0 -0
  41. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/__init__.py +0 -0
  42. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  43. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  44. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/broker.py +0 -0
  45. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  46. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/data.py +0 -0
  47. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  48. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  49. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  50. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  51. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  52. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  53. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  54. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  55. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  56. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  57. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  58. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/factory.py +0 -0
  59. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  60. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  61. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  62. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  63. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  64. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  65. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  66. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  67. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  68. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  69. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/reader.py +0 -0
  70. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  71. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  72. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  73. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/utils.py +0 -0
  74. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  75. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/tardis/data.py +0 -0
  76. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/connectors/tardis/utils.py +0 -0
  77. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/__init__.py +0 -0
  78. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/context.py +0 -0
  79. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/deque.py +0 -0
  80. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/errors.py +0 -0
  81. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/exceptions.py +0 -0
  82. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/helpers.py +0 -0
  83. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/initializer.py +0 -0
  84. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/interfaces.py +0 -0
  85. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/loggers.py +0 -0
  86. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/lookups.py +0 -0
  87. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/mixins/__init__.py +0 -0
  88. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/mixins/market.py +0 -0
  89. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/mixins/processing.py +0 -0
  90. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/mixins/subscription.py +0 -0
  91. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/mixins/trading.py +0 -0
  92. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/mixins/universe.py +0 -0
  93. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/mixins/utils.py +0 -0
  94. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/series.pxd +0 -0
  95. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/series.pyi +0 -0
  96. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/series.pyx +0 -0
  97. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/stale_data_detector.py +0 -0
  98. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/utils.pyi +0 -0
  99. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/core/utils.pyx +0 -0
  100. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/data/__init__.py +0 -0
  101. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/data/composite.py +0 -0
  102. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/data/hft.py +0 -0
  103. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/data/readers.py +0 -0
  104. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/data/registry.py +0 -0
  105. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/data/tardis.py +0 -0
  106. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/emitters/__init__.py +0 -0
  107. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/emitters/base.py +0 -0
  108. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/emitters/composite.py +0 -0
  109. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/emitters/csv.py +0 -0
  110. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/emitters/indicator.py +0 -0
  111. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/emitters/inmemory.py +0 -0
  112. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/emitters/prometheus.py +0 -0
  113. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/emitters/questdb.py +0 -0
  114. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/exporters/__init__.py +0 -0
  115. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/exporters/composite.py +0 -0
  116. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/exporters/formatters/__init__.py +0 -0
  117. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/exporters/formatters/base.py +0 -0
  118. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/exporters/formatters/incremental.py +0 -0
  119. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/exporters/formatters/slack.py +0 -0
  120. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/exporters/redis_streams.py +0 -0
  121. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/exporters/slack.py +0 -0
  122. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/features/__init__.py +0 -0
  123. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/features/core.py +0 -0
  124. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/features/orderbook.py +0 -0
  125. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/features/price.py +0 -0
  126. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/features/trades.py +0 -0
  127. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/features/utils.py +0 -0
  128. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/gathering/simplest.py +0 -0
  129. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/health/__init__.py +0 -0
  130. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/health/base.py +0 -0
  131. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/loggers/__init__.py +0 -0
  132. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/loggers/csv.py +0 -0
  133. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/loggers/factory.py +0 -0
  134. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/loggers/inmemory.py +0 -0
  135. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/loggers/mongo.py +0 -0
  136. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/math/__init__.py +0 -0
  137. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/math/stats.py +0 -0
  138. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/notifications/__init__.py +0 -0
  139. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/notifications/composite.py +0 -0
  140. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/notifications/slack.py +0 -0
  141. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/notifications/throttler.py +0 -0
  142. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/pandaz/__init__.py +0 -0
  143. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/pandaz/ta.py +0 -0
  144. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/pandaz/utils.py +0 -0
  145. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/_build.py +0 -0
  146. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/crypto-fees.ini +0 -0
  147. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  148. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  149. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  150. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  151. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  152. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  153. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  154. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  155. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  156. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  157. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  158. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restarts/__init__.py +0 -0
  159. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restarts/state_resolvers.py +0 -0
  160. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restarts/time_finders.py +0 -0
  161. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restorers/__init__.py +0 -0
  162. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restorers/balance.py +0 -0
  163. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restorers/factory.py +0 -0
  164. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restorers/interfaces.py +0 -0
  165. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restorers/position.py +0 -0
  166. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restorers/signal.py +0 -0
  167. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restorers/state.py +0 -0
  168. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/restorers/utils.py +0 -0
  169. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/ta/__init__.py +0 -0
  170. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/__init__.py +0 -0
  171. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/base.py +0 -0
  172. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  173. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/project/config.yml.j2 +0 -0
  174. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  175. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  176. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  177. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  178. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  179. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/project/template.yml +0 -0
  180. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  181. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  182. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/simple/config.yml.j2 +0 -0
  183. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  184. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  185. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  186. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/templates/simple/template.yml +0 -0
  187. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/trackers/__init__.py +0 -0
  188. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/trackers/advanced.py +0 -0
  189. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/trackers/composite.py +0 -0
  190. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/trackers/rebalancers.py +0 -0
  191. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/trackers/sizers.py +0 -0
  192. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/__init__.py +0 -0
  193. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/_pyxreloader.py +0 -0
  194. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  195. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/charting/orderbook.py +0 -0
  196. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/collections.py +0 -0
  197. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/marketdata/binance.py +0 -0
  198. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/marketdata/ccxt.py +0 -0
  199. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/marketdata/dukas.py +0 -0
  200. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/ntp.py +0 -0
  201. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/numbers_utils.py +0 -0
  202. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/orderbook.py +0 -0
  203. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/plotting/__init__.py +0 -0
  204. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/plotting/dashboard.py +0 -0
  205. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/plotting/data.py +0 -0
  206. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/plotting/interfaces.py +0 -0
  207. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  208. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  209. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/questdb.py +0 -0
  210. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/runner/__init__.py +0 -0
  211. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  212. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/runner/accounts.py +0 -0
  213. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/runner/factory.py +0 -0
  214. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/time.py +0 -0
  215. {qubx-0.6.78 → qubx-0.6.84}/src/qubx/utils/version.py +0 -0
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.78
3
+ Version: 0.6.84
4
4
  Summary: Qubx - Quantitative Trading Framework
5
+ License-File: LICENSE
5
6
  Author: Dmitry Marienko
6
7
  Author-email: dmitry.marienko@xlydian.com
7
8
  Requires-Python: >=3.10,<4.0
@@ -10,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
10
11
  Classifier: Programming Language :: Python :: 3.11
11
12
  Classifier: Programming Language :: Python :: 3.12
12
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
13
15
  Requires-Dist: aiohttp (>=3.10.11,<3.11.0)
14
16
  Requires-Dist: ccxt (>=4.2.68,<5.0.0)
15
17
  Requires-Dist: croniter (>=2.0.5,<3.0.0)
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.78"
7
+ version = "0.6.84"
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"
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  import numpy as np
7
7
  import pandas as pd
8
8
  import yaml
9
+ from tqdm.auto import tqdm
9
10
 
10
11
  from qubx.core.metrics import TradingSessionResult
11
12
  from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
@@ -420,8 +421,10 @@ class BacktestsResultsManager:
420
421
  Returns:
421
422
  plotly.graph_objects.Figure: The plot of the variation.
422
423
  """
423
- import plotly.express as px
424
424
  from itertools import cycle
425
+
426
+ import plotly.express as px
427
+
425
428
  from qubx.utils.misc import string_shortener
426
429
 
427
430
  _vars = self.variations.get(variation_idx)
@@ -507,3 +510,11 @@ class BacktestsResultsManager:
507
510
  )
508
511
  )
509
512
  return figure
513
+
514
+ def export_backtests_to_markdown(self, path: str, tags: tuple[str] | None = None):
515
+ """
516
+ Export backtests to markdown format
517
+ """
518
+ for n, v in tqdm(self.results.items()):
519
+ r = TradingSessionResult.from_file(v.get("path"))
520
+ r.to_markdown(path, list(tags) if tags else None)
@@ -15,6 +15,7 @@ from qubx.core.helpers import extract_parameters_from_object, full_qualified_cla
15
15
  from qubx.core.initializer import BasicStrategyInitializer
16
16
  from qubx.core.interfaces import (
17
17
  CtrlChannel,
18
+ IDataProvider,
18
19
  IMetricEmitter,
19
20
  IStrategy,
20
21
  IStrategyContext,
@@ -224,7 +225,7 @@ class SimulationRunner:
224
225
  cc = self.channel
225
226
  t = np.datetime64(data.time, "ns")
226
227
  _account = self.account.get_account_processor(instrument.exchange)
227
- _data_provider = self._exchange_to_data_provider[instrument.exchange]
228
+ _data_provider = self._get_data_provider(instrument.exchange)
228
229
  assert isinstance(_account, SimulatedAccountProcessor)
229
230
  assert isinstance(_data_provider, SimulatedDataProvider)
230
231
 
@@ -249,7 +250,7 @@ class SimulationRunner:
249
250
  cc = self.channel
250
251
  t = np.datetime64(data.time, "ns")
251
252
  _account = self.account.get_account_processor(instrument.exchange)
252
- _data_provider = self._exchange_to_data_provider[instrument.exchange]
253
+ _data_provider = self._get_data_provider(instrument.exchange)
253
254
  assert isinstance(_account, SimulatedAccountProcessor)
254
255
  assert isinstance(_data_provider, SimulatedDataProvider)
255
256
 
@@ -267,6 +268,11 @@ class SimulationRunner:
267
268
 
268
269
  return cc.control.is_set()
269
270
 
271
+ def _get_data_provider(self, exchange: str) -> IDataProvider:
272
+ if exchange in self._exchange_to_data_provider:
273
+ return self._exchange_to_data_provider[exchange]
274
+ raise ValueError(f"Data provider for exchange {exchange} not found")
275
+
270
276
  def _run(self, start: pd.Timestamp, stop: pd.Timestamp, silent: bool = False) -> None:
271
277
  logger.info(f"{self.__class__.__name__} ::: Simulation started at {start} :::")
272
278
 
@@ -328,7 +334,7 @@ class SimulationRunner:
328
334
 
329
335
  if not _run(instrument, data_type, event, is_hist):
330
336
  return False
331
-
337
+
332
338
  return True
333
339
 
334
340
  def _handle_no_data_scenario(self, stop_time):
@@ -82,6 +82,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
82
82
  open_order_backoff: str = "1Min",
83
83
  max_position_restore_days: int = 5,
84
84
  max_retries: int = 10,
85
+ connection_timeout: int = 30,
85
86
  read_only: bool = False,
86
87
  ):
87
88
  super().__init__(
@@ -109,6 +110,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
109
110
  self._latest_instruments = set()
110
111
  self._subscription_manager = None
111
112
  self._read_only = read_only
113
+ self._connection_timeout = connection_timeout
112
114
 
113
115
  def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
114
116
  self._subscription_manager = manager
@@ -172,6 +174,14 @@ class CcxtAccountProcessor(BasicAccountProcessor):
172
174
  super().update_position_price(time, instrument, update)
173
175
 
174
176
  def get_total_capital(self, exchange: str | None = None) -> float:
177
+ # If polling is not running yet, we need to fetch balance data directly
178
+ if not self._is_running and self.exchange_manager.exchange:
179
+ try:
180
+ future = self._loop.submit(self._update_balance())
181
+ future.result(timeout=self._connection_timeout)
182
+ except Exception as e:
183
+ logger.warning(f"Failed to fetch balance data before polling started: {e}")
184
+
175
185
  # sum of balances + market value of all positions on non spot/margin
176
186
  _currency_to_value = {c: self._get_currency_value(b.total, c) for c, b in self._balances.items()}
177
187
  _positions_value = sum([p.market_value_funds for p in self._positions.values() if p.instrument.is_futures()])
@@ -294,7 +304,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
294
304
  async def _update_positions(self) -> None:
295
305
  # fetch and update positions from exchange
296
306
  ccxt_positions = await self.exchange_manager.exchange.fetch_positions()
297
- positions = ccxt_convert_positions(ccxt_positions, self.exchange_manager.exchange.name, self.exchange_manager.exchange.markets) # type: ignore
307
+ positions = ccxt_convert_positions(
308
+ ccxt_positions, self.exchange_manager.exchange.name, self.exchange_manager.exchange.markets
309
+ ) # type: ignore
298
310
  # update required instruments that we need to subscribe to
299
311
  self._required_instruments.update([p.instrument for p in positions])
300
312
  # update positions
@@ -458,7 +470,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
458
470
 
459
471
  async def _cancel_order(order: Order) -> None:
460
472
  try:
461
- await self.exchange_manager.exchange.cancel_order(order.id, symbol=instrument_to_ccxt_symbol(order.instrument))
473
+ await self.exchange_manager.exchange.cancel_order(
474
+ order.id, symbol=instrument_to_ccxt_symbol(order.instrument)
475
+ )
462
476
  logger.debug(
463
477
  f" :: [SYNC] Canceled {order.id} {order.instrument.symbol} {order.side} {order.quantity} @ {order.price} ({order.status})"
464
478
  )
@@ -476,7 +490,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
476
490
  ) -> dict[str, Order]:
477
491
  _start_ms = self._get_start_time_in_ms(days_before) if limit is None else None
478
492
  _ccxt_symbol = instrument_to_ccxt_symbol(instrument)
479
- _fetcher = self.exchange_manager.exchange.fetch_open_orders if is_open else self.exchange_manager.exchange.fetch_orders
493
+ _fetcher = (
494
+ self.exchange_manager.exchange.fetch_open_orders if is_open else self.exchange_manager.exchange.fetch_orders
495
+ )
480
496
  _raw_orders = await _fetcher(_ccxt_symbol, since=_start_ms, limit=limit)
481
497
  _orders = [ccxt_convert_order_info(instrument, o) for o in _raw_orders]
482
498
  _id_to_order = {o.id: o for o in _orders}
@@ -533,7 +549,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
533
549
  async def _watch_executions():
534
550
  exec = await self.exchange_manager.exchange.watch_orders()
535
551
  for report in exec:
536
- instrument = ccxt_find_instrument(report["symbol"], self.exchange_manager.exchange, _symbol_to_instrument)
552
+ instrument = ccxt_find_instrument(
553
+ report["symbol"], self.exchange_manager.exchange, _symbol_to_instrument
554
+ )
537
555
  order = ccxt_convert_order_info(instrument, report)
538
556
  deals = ccxt_extract_deals_from_exec(report)
539
557
  channel.send((instrument, "order", order, False))
@@ -24,12 +24,13 @@ SECONDS_PER_HOUR = 3600
24
24
 
25
25
  # Custom stall detection thresholds (in seconds)
26
26
  STALL_THRESHOLDS = {
27
- 'funding_payment': 12 * SECONDS_PER_HOUR, # 12 hours = 43,200s
28
- 'open_interest': 30 * 60, # 30 minutes = 1,800s
29
- 'orderbook': 5 * 60, # 5 minutes = 300s
30
- 'trade': 60 * 60, # 60 minutes = 3,600s
31
- 'liquidation': 7 * 24 * SECONDS_PER_HOUR, # 7 days = 604,800s
32
- 'ohlc': 15 * 60, # 15 minutes = 900s
27
+ "funding_payment": 12 * SECONDS_PER_HOUR, # 12 hours = 43,200s
28
+ "open_interest": 30 * 60, # 30 minutes = 1,800s
29
+ "orderbook": 5 * 60, # 5 minutes = 300s
30
+ "trade": 60 * 60, # 60 minutes = 3,600s
31
+ "liquidation": 7 * 24 * SECONDS_PER_HOUR, # 7 days = 604,800s
32
+ "ohlc": 5 * 60, # 5 minutes = 300s
33
+ "quote": 5 * 60, # 5 minutes = 300s
33
34
  }
34
35
  DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
35
36
 
@@ -37,10 +38,10 @@ DEFAULT_STALL_THRESHOLD_SECONDS = 2 * SECONDS_PER_HOUR # 2 hours = 7,200s
37
38
  class ExchangeManager(IDataArrivalListener):
38
39
  """
39
40
  Wrapper for CCXT Exchange that handles recreation internally with self-monitoring.
40
-
41
+
41
42
  Exposes the underlying exchange via .exchange property for explicit access.
42
43
  Self-monitors for data stalls and triggers recreation automatically.
43
-
44
+
44
45
  Key Features:
45
46
  - Explicit .exchange property for CCXT access
46
47
  - Self-contained stall detection and recreation triggering
@@ -48,7 +49,7 @@ class ExchangeManager(IDataArrivalListener):
48
49
  - Atomic exchange transitions during recreation
49
50
  - Background monitoring thread for stall detection
50
51
  """
51
-
52
+
52
53
  _exchange: cxp.Exchange # Type hint that this is always a valid exchange
53
54
 
54
55
  def __init__(
@@ -61,10 +62,10 @@ class ExchangeManager(IDataArrivalListener):
61
62
  check_interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
62
63
  ):
63
64
  """Initialize ExchangeManager with underlying CCXT exchange.
64
-
65
+
65
66
  Args:
66
67
  exchange_name: Exchange name for factory (e.g., "binance.um")
67
- factory_params: Parameters for get_ccxt_exchange()
68
+ factory_params: Parameters for get_ccxt_exchange()
68
69
  initial_exchange: Pre-created exchange instance (from factory)
69
70
  max_recreations: Maximum recreation attempts before giving up
70
71
  reset_interval_hours: Hours between recreation count resets
@@ -74,24 +75,24 @@ class ExchangeManager(IDataArrivalListener):
74
75
  self._factory_params = factory_params.copy()
75
76
  self._max_recreations = max_recreations
76
77
  self._reset_interval_hours = reset_interval_hours
77
-
78
+
78
79
  # Recreation state
79
80
  self._recreation_count = 0
80
81
  self._recreation_lock = threading.RLock()
81
82
  self._last_successful_reset = time.time()
82
-
83
+
83
84
  # Stall detection state
84
85
  self._check_interval = check_interval_seconds
85
86
  self._last_data_times: dict[str, float] = {}
86
87
  self._data_lock = threading.RLock()
87
-
88
+
88
89
  # Monitoring control
89
90
  self._monitoring_enabled = False
90
91
  self._monitor_thread = None
91
-
92
+
92
93
  # Recreation callback management
93
94
  self._recreation_callbacks: list[Callable[[], None]] = []
94
-
95
+
95
96
  # Use provided exchange or create new one
96
97
  if initial_exchange:
97
98
  self._exchange = initial_exchange
@@ -105,23 +106,23 @@ class ExchangeManager(IDataArrivalListener):
105
106
  try:
106
107
  # Import here to avoid circular import (factory → broker → exchange_manager)
107
108
  from .factory import get_ccxt_exchange
108
-
109
+
109
110
  # Create raw exchange using factory logic
110
111
  ccxt_exchange = get_ccxt_exchange(**self._factory_params)
111
-
112
+
112
113
  # Setup exception handler for the new exchange
113
114
  self._setup_ccxt_exception_handler(ccxt_exchange)
114
-
115
+
115
116
  logger.debug(f"Created new {self._exchange_name} exchange instance")
116
117
  return ccxt_exchange
117
-
118
+
118
119
  except Exception as e:
119
120
  logger.error(f"Failed to create {self._exchange_name} exchange: {e}")
120
121
  raise RuntimeError(f"Failed to create {self._exchange_name} exchange: {e}") from e
121
-
122
+
122
123
  def register_recreation_callback(self, callback: Callable[[], None]) -> None:
123
124
  """Register callback to be called after successful exchange recreation.
124
-
125
+
125
126
  Args:
126
127
  callback: Function to call after successful recreation (no parameters)
127
128
  """
@@ -131,7 +132,7 @@ class ExchangeManager(IDataArrivalListener):
131
132
  def _call_recreation_callbacks(self) -> None:
132
133
  """Call all registered recreation callbacks after successful exchange recreation."""
133
134
  logger.debug(f"Calling {len(self._recreation_callbacks)} recreation callbacks for {self._exchange_name}")
134
-
135
+
135
136
  for callback in self._recreation_callbacks:
136
137
  try:
137
138
  callback()
@@ -142,66 +143,68 @@ class ExchangeManager(IDataArrivalListener):
142
143
  def force_recreation(self) -> bool:
143
144
  """
144
145
  Force recreation due to data stalls (called by BaseHealthMonitor).
145
-
146
+
146
147
  Returns:
147
148
  True if recreation successful, False if failed/limit exceeded
148
149
  """
149
150
  with self._recreation_lock:
150
151
  # Check recreation limit
151
152
  if self._recreation_count >= self._max_recreations:
152
- logger.error(f"Cannot recreate {self._exchange_name}: recreation limit ({self._max_recreations}) exceeded")
153
+ logger.error(
154
+ f"Cannot recreate {self._exchange_name}: recreation limit ({self._max_recreations}) exceeded"
155
+ )
153
156
  return False
154
-
157
+
155
158
  logger.info(f"Stall-triggered recreation for {self._exchange_name}")
156
159
  return self._recreate_exchange()
157
-
160
+
158
161
  def _recreate_exchange(self) -> bool:
159
162
  """Recreate the underlying exchange (must be called with _recreation_lock held)."""
160
163
  self._recreation_count += 1
161
- logger.warning(f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count}/{self._max_recreations})")
162
-
164
+ logger.warning(
165
+ f"Recreating {self._exchange_name} exchange (attempt {self._recreation_count}/{self._max_recreations})"
166
+ )
167
+
163
168
  # Create new exchange
164
169
  try:
165
170
  new_exchange = self._create_exchange()
166
171
  except Exception as e:
167
172
  logger.error(f"Failed to recreate {self._exchange_name} exchange: {e}")
168
173
  return False
169
-
174
+
170
175
  # Atomically replace the exchange
171
176
  old_exchange = self._exchange
172
177
  self._exchange = new_exchange
173
-
178
+
174
179
  # Clean up old exchange
175
180
  try:
176
- if hasattr(old_exchange, 'close') and hasattr(old_exchange, 'asyncio_loop'):
177
- old_exchange.asyncio_loop.call_soon_threadsafe(
178
- lambda: asyncio.create_task(old_exchange.close())
179
- )
181
+ if hasattr(old_exchange, "close") and hasattr(old_exchange, "asyncio_loop"):
182
+ old_exchange.asyncio_loop.call_soon_threadsafe(lambda: asyncio.create_task(old_exchange.close()))
180
183
  except Exception as e:
181
184
  logger.warning(f"Error closing old {self._exchange_name} exchange: {e}")
182
-
185
+
183
186
  logger.info(f"Successfully recreated {self._exchange_name} exchange")
184
-
187
+
185
188
  # Call recreation callbacks after successful recreation
186
189
  self._call_recreation_callbacks()
187
-
190
+
188
191
  return True
189
-
192
+
190
193
  def reset_recreation_count_if_needed(self) -> None:
191
194
  """Reset recreation count periodically (called by monitoring loop)."""
192
195
  reset_interval_seconds = self._reset_interval_hours * SECONDS_PER_HOUR
193
-
196
+
194
197
  current_time = time.time()
195
198
  time_since_reset = current_time - self._last_successful_reset
196
-
199
+
197
200
  if time_since_reset >= reset_interval_seconds and self._recreation_count > 0:
198
201
  logger.info(f"Resetting recreation count for {self._exchange_name} (was {self._recreation_count})")
199
202
  self._recreation_count = 0
200
203
  self._last_successful_reset = current_time
201
-
204
+
202
205
  def on_data_arrival(self, event_type: str, event_time: dt_64) -> None:
203
206
  """Record data arrival for stall detection.
204
-
207
+
205
208
  Args:
206
209
  event_type: Type of data event (e.g., "ohlcv", "trade", "orderbook")
207
210
  event_time: Timestamp of the data event (unused for stall detection)
@@ -209,10 +212,10 @@ class ExchangeManager(IDataArrivalListener):
209
212
  current_timestamp = time.time()
210
213
  with self._data_lock:
211
214
  self._last_data_times[event_type] = current_timestamp
212
-
215
+
213
216
  def _extract_ohlc_timeframe(self, event_type: str) -> Optional[str]:
214
217
  """Extract timeframe from OHLC event type like 'ohlc(1m)' -> '1m'."""
215
- if event_type.startswith('ohlc(') and event_type.endswith(')'):
218
+ if event_type.startswith("ohlc(") and event_type.endswith(")"):
216
219
  return event_type[5:-1] # Simple slice: ohlc(1m) -> 1m
217
220
  return None
218
221
 
@@ -222,30 +225,30 @@ class ExchangeManager(IDataArrivalListener):
222
225
 
223
226
  def _get_stall_threshold(self, event_type: str) -> float:
224
227
  """Get stall threshold for specific event type.
225
-
228
+
226
229
  Extracts base data type from parameterized types like 'ohlc(1m)' -> 'ohlc'.
227
230
  """
228
231
  # Extract base data type (everything before first '(' if present)
229
- base_event_type = event_type.split('(')[0]
232
+ base_event_type = event_type.split("(")[0]
230
233
  return float(STALL_THRESHOLDS.get(base_event_type, DEFAULT_STALL_THRESHOLD_SECONDS))
231
-
234
+
232
235
  def start_monitoring(self) -> None:
233
236
  """Start background stall detection monitoring."""
234
237
  if self._monitoring_enabled:
235
238
  return
236
-
239
+
237
240
  self._monitoring_enabled = True
238
241
  self._monitor_thread = threading.Thread(target=self._stall_monitor_loop, daemon=True)
239
242
  self._monitor_thread.start()
240
243
  logger.debug(f"ExchangeManager: Started stall monitoring for {self._exchange_name}")
241
-
244
+
242
245
  def stop_monitoring(self) -> None:
243
246
  """Stop background stall detection monitoring."""
244
247
  self._monitoring_enabled = False
245
248
  if self._monitor_thread:
246
249
  self._monitor_thread = None
247
250
  logger.debug(f"ExchangeManager: Stopped stall monitoring for {self._exchange_name}")
248
-
251
+
249
252
  def _stall_monitor_loop(self) -> None:
250
253
  """Background thread that checks for data stalls and triggers self-recreation."""
251
254
  while self._monitoring_enabled:
@@ -256,26 +259,26 @@ class ExchangeManager(IDataArrivalListener):
256
259
  except Exception as e:
257
260
  logger.error(f"Error in ExchangeManager stall detection: {e}")
258
261
  time.sleep(self._check_interval)
259
-
262
+
260
263
  def _check_and_handle_stalls(self) -> None:
261
264
  """Check for stalls using custom thresholds per data type."""
262
265
  current_time = time.time()
263
266
  stalled_types = []
264
-
267
+
265
268
  with self._data_lock:
266
269
  for event_type, last_data_time in self._last_data_times.items():
267
270
  time_since_data = current_time - last_data_time
268
271
  threshold = self._get_stall_threshold(event_type)
269
-
272
+
270
273
  if time_since_data > threshold:
271
274
  stalled_types.append((event_type, time_since_data))
272
-
275
+
273
276
  if not stalled_types:
274
277
  return # No stalls detected
275
-
278
+
276
279
  stall_info = ", ".join([f"{event_type}({int(time_since)}s)" for event_type, time_since in stalled_types])
277
280
  logger.error(f"Data stalls detected in {self._exchange_name}: {stall_info}")
278
-
281
+
279
282
  try:
280
283
  logger.info(f"Self-triggering recreation for {self._exchange_name} due to stalls...")
281
284
  if self.force_recreation():
@@ -288,14 +291,14 @@ class ExchangeManager(IDataArrivalListener):
288
291
  logger.error(f"Stall-triggered recreation failed for {self._exchange_name}")
289
292
  except Exception as e:
290
293
  logger.error(f"Error during stall-triggered recreation: {e}")
291
-
294
+
292
295
  def _setup_ccxt_exception_handler(self, exchange: cxp.Exchange) -> None:
293
296
  """
294
297
  Set up global exception handler for the CCXT async loop to handle unretrieved futures.
295
298
 
296
299
  This prevents 'Future exception was never retrieved' warnings from CCXT's internal
297
300
  per-symbol futures that complete with UnsubscribeError during resubscription.
298
-
301
+
299
302
  Applied to every newly created exchange (initial and recreated).
300
303
  """
301
304
  asyncio_loop = exchange.asyncio_loop
@@ -324,15 +327,15 @@ class ExchangeManager(IDataArrivalListener):
324
327
  # Set the custom exception handler on the CCXT loop
325
328
  asyncio_loop.set_exception_handler(handle_ccxt_exception)
326
329
 
327
- # === Exchange Property Access ===
330
+ # === Exchange Property Access ===
328
331
  # Explicit property to access underlying CCXT exchange
329
-
332
+
330
333
  @property
331
334
  def exchange(self) -> cxp.Exchange:
332
335
  """Access to the underlying CCXT exchange instance.
333
-
336
+
334
337
  Use this property to call CCXT methods: exchange_manager.exchange.fetch_ticker(symbol)
335
-
338
+
336
339
  Returns:
337
340
  The current CCXT exchange instance (may change after recreation)
338
341
  """
@@ -344,7 +344,8 @@ class CompositeAccountProcessor(IAccountProcessor):
344
344
  raise ValueError("At least one account processor must be provided")
345
345
 
346
346
  def get_account_processor(self, exchange: str) -> IAccountProcessor:
347
- return self._account_processors[exchange]
347
+ exch = self._get_exchange(exchange)
348
+ return self._account_processors[exch]
348
349
 
349
350
  def _get_exchange(self, exchange: str | None = None, instrument: Instrument | None = None) -> str:
350
351
  """
@@ -63,6 +63,10 @@ class FundingPayment:
63
63
  if self.funding_interval_hours <= 0:
64
64
  raise ValueError(f"Invalid funding interval: {self.funding_interval_hours} (must be positive)")
65
65
 
66
+ @property
67
+ def funding_rate_apr(self) -> float:
68
+ return self.funding_rate * 365 * 24 / self.funding_interval_hours * 100
69
+
66
70
 
67
71
  @dataclass
68
72
  class OpenInterest: