Qubx 0.6.67__tar.gz → 0.6.68__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 (207) hide show
  1. {qubx-0.6.67 → qubx-0.6.68}/PKG-INFO +50 -4
  2. {qubx-0.6.67 → qubx-0.6.68}/README.md +48 -3
  3. {qubx-0.6.67 → qubx-0.6.68}/pyproject.toml +2 -1
  4. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/data.py +19 -2
  5. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/cli/commands.py +125 -0
  6. qubx-0.6.68/src/qubx/connectors/ccxt/connection_manager.py +310 -0
  7. qubx-0.6.68/src/qubx/connectors/ccxt/data.py +241 -0
  8. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +68 -16
  9. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/__init__.py +29 -0
  10. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/base.py +93 -0
  11. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/factory.py +123 -0
  12. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/funding_rate.py +93 -0
  13. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/liquidation.py +91 -0
  14. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/ohlc.py +202 -0
  15. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/open_interest.py +208 -0
  16. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/orderbook.py +186 -0
  17. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/quote.py +98 -0
  18. qubx-0.6.68/src/qubx/connectors/ccxt/handlers/trade.py +94 -0
  19. qubx-0.6.68/src/qubx/connectors/ccxt/subscription_config.py +40 -0
  20. qubx-0.6.68/src/qubx/connectors/ccxt/subscription_manager.py +331 -0
  21. qubx-0.6.68/src/qubx/connectors/ccxt/subscription_orchestrator.py +215 -0
  22. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/utils.py +88 -1
  23. qubx-0.6.68/src/qubx/connectors/ccxt/warmup_service.py +113 -0
  24. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/tardis/data.py +6 -6
  25. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/basics.py +15 -0
  26. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/helpers.py +43 -24
  27. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/initializer.py +5 -9
  28. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/metrics.py +252 -22
  29. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/series.pxd +22 -5
  30. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/series.pyi +33 -3
  31. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/series.pyx +116 -59
  32. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/data/readers.py +68 -33
  33. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/pandaz/ta.py +97 -22
  34. qubx-0.6.68/src/qubx/templates/__init__.py +5 -0
  35. qubx-0.6.68/src/qubx/templates/base.py +166 -0
  36. qubx-0.6.68/src/qubx/templates/project/accounts.toml.j2 +22 -0
  37. qubx-0.6.68/src/qubx/templates/project/config.yml.j2 +33 -0
  38. qubx-0.6.68/src/qubx/templates/project/jlive.sh.j2 +43 -0
  39. qubx-0.6.68/src/qubx/templates/project/jpaper.sh.j2 +6 -0
  40. qubx-0.6.68/src/qubx/templates/project/pyproject.toml.j2 +18 -0
  41. qubx-0.6.68/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +5 -0
  42. qubx-0.6.68/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +170 -0
  43. qubx-0.6.68/src/qubx/templates/project/template.yml +20 -0
  44. qubx-0.6.68/src/qubx/templates/simple/__init__.py.j2 +5 -0
  45. qubx-0.6.68/src/qubx/templates/simple/accounts.toml.j2 +22 -0
  46. qubx-0.6.68/src/qubx/templates/simple/config.yml.j2 +30 -0
  47. qubx-0.6.68/src/qubx/templates/simple/jlive.sh.j2 +43 -0
  48. qubx-0.6.68/src/qubx/templates/simple/jpaper.sh.j2 +6 -0
  49. qubx-0.6.68/src/qubx/templates/simple/strategy.py.j2 +95 -0
  50. qubx-0.6.68/src/qubx/templates/simple/template.yml +20 -0
  51. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/trackers/sizers.py +9 -2
  52. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/charting/lookinglass.py +93 -15
  53. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/misc.py +9 -2
  54. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/runner/_jupyter_runner.pyt +4 -0
  55. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/runner/configs.py +1 -0
  56. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/runner/runner.py +1 -0
  57. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/time.py +13 -13
  58. qubx-0.6.67/src/qubx/connectors/ccxt/data.py +0 -831
  59. {qubx-0.6.67 → qubx-0.6.68}/LICENSE +0 -0
  60. {qubx-0.6.67 → qubx-0.6.68}/build.py +0 -0
  61. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/__init__.py +0 -0
  62. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/_nb_magic.py +0 -0
  63. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/__init__.py +0 -0
  64. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/account.py +0 -0
  65. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/broker.py +0 -0
  66. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/management.py +0 -0
  67. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/ome.py +0 -0
  68. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/optimization.py +0 -0
  69. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/runner.py +0 -0
  70. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/sentinels.py +0 -0
  71. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/simulated_data.py +0 -0
  72. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/simulated_exchange.py +0 -0
  73. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/simulator.py +0 -0
  74. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/backtester/utils.py +0 -0
  75. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/cli/__init__.py +0 -0
  76. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/cli/deploy.py +0 -0
  77. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/cli/misc.py +0 -0
  78. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/cli/release.py +0 -0
  79. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/cli/tui.py +0 -0
  80. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/__init__.py +0 -0
  81. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/account.py +0 -0
  82. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/broker.py +0 -0
  83. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  84. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  85. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  86. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  87. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  88. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  89. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/factory.py +0 -0
  90. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/ccxt/reader.py +0 -0
  91. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/connectors/tardis/utils.py +0 -0
  92. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/__init__.py +0 -0
  93. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/account.py +0 -0
  94. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/context.py +0 -0
  95. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/deque.py +0 -0
  96. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/errors.py +0 -0
  97. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/exceptions.py +0 -0
  98. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/interfaces.py +0 -0
  99. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/loggers.py +0 -0
  100. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/lookups.py +0 -0
  101. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/mixins/__init__.py +0 -0
  102. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/mixins/market.py +0 -0
  103. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/mixins/processing.py +0 -0
  104. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/mixins/subscription.py +0 -0
  105. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/mixins/trading.py +0 -0
  106. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/mixins/universe.py +0 -0
  107. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/stale_data_detector.py +0 -0
  108. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/utils.pyi +0 -0
  109. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/core/utils.pyx +0 -0
  110. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/data/__init__.py +0 -0
  111. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/data/composite.py +0 -0
  112. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/data/helpers.py +0 -0
  113. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/data/hft.py +0 -0
  114. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/data/registry.py +0 -0
  115. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/data/tardis.py +0 -0
  116. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/emitters/__init__.py +0 -0
  117. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/emitters/base.py +0 -0
  118. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/emitters/composite.py +0 -0
  119. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/emitters/csv.py +0 -0
  120. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/emitters/indicator.py +0 -0
  121. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/emitters/inmemory.py +0 -0
  122. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/emitters/prometheus.py +0 -0
  123. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/emitters/questdb.py +0 -0
  124. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/exporters/__init__.py +0 -0
  125. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/exporters/composite.py +0 -0
  126. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/exporters/formatters/__init__.py +0 -0
  127. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/exporters/formatters/base.py +0 -0
  128. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/exporters/formatters/incremental.py +0 -0
  129. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/exporters/formatters/slack.py +0 -0
  130. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/exporters/redis_streams.py +0 -0
  131. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/exporters/slack.py +0 -0
  132. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/features/__init__.py +0 -0
  133. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/features/core.py +0 -0
  134. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/features/orderbook.py +0 -0
  135. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/features/price.py +0 -0
  136. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/features/trades.py +0 -0
  137. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/features/utils.py +0 -0
  138. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/gathering/simplest.py +0 -0
  139. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/health/__init__.py +0 -0
  140. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/health/base.py +0 -0
  141. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/loggers/__init__.py +0 -0
  142. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/loggers/csv.py +0 -0
  143. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/loggers/factory.py +0 -0
  144. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/loggers/inmemory.py +0 -0
  145. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/loggers/mongo.py +0 -0
  146. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/math/__init__.py +0 -0
  147. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/math/stats.py +0 -0
  148. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/notifications/__init__.py +0 -0
  149. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/notifications/composite.py +0 -0
  150. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/notifications/slack.py +0 -0
  151. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/notifications/throttler.py +0 -0
  152. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/pandaz/__init__.py +0 -0
  153. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/pandaz/utils.py +0 -0
  154. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/_build.py +0 -0
  155. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/crypto-fees.ini +0 -0
  156. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  157. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  158. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  159. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  160. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  161. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  162. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -0
  163. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -0
  164. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  165. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  166. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  167. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restarts/__init__.py +0 -0
  168. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restarts/state_resolvers.py +0 -0
  169. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restarts/time_finders.py +0 -0
  170. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restorers/__init__.py +0 -0
  171. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restorers/balance.py +0 -0
  172. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restorers/factory.py +0 -0
  173. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restorers/interfaces.py +0 -0
  174. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restorers/position.py +0 -0
  175. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restorers/signal.py +0 -0
  176. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restorers/state.py +0 -0
  177. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/restorers/utils.py +0 -0
  178. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/ta/__init__.py +0 -0
  179. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/ta/indicators.pxd +0 -0
  180. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/ta/indicators.pyi +0 -0
  181. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/ta/indicators.pyx +0 -0
  182. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/trackers/__init__.py +0 -0
  183. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/trackers/advanced.py +0 -0
  184. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/trackers/composite.py +0 -0
  185. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/trackers/rebalancers.py +0 -0
  186. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/trackers/riskctrl.py +0 -0
  187. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/__init__.py +0 -0
  188. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/_pyxreloader.py +0 -0
  189. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  190. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/collections.py +0 -0
  191. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/marketdata/binance.py +0 -0
  192. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/marketdata/ccxt.py +0 -0
  193. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/marketdata/dukas.py +0 -0
  194. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/ntp.py +0 -0
  195. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/numbers_utils.py +0 -0
  196. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/orderbook.py +0 -0
  197. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/plotting/__init__.py +0 -0
  198. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/plotting/dashboard.py +0 -0
  199. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/plotting/data.py +0 -0
  200. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/plotting/interfaces.py +0 -0
  201. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  202. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  203. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/questdb.py +0 -0
  204. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/runner/__init__.py +0 -0
  205. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/runner/accounts.py +0 -0
  206. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/runner/factory.py +0 -0
  207. {qubx-0.6.67 → qubx-0.6.68}/src/qubx/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.67
3
+ Version: 0.6.68
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -19,6 +19,7 @@ Requires-Dist: dash-bootstrap-components (>=1.6.0,<2.0.0)
19
19
  Requires-Dist: gitpython (>=3.1.44,<4.0.0)
20
20
  Requires-Dist: importlib-metadata
21
21
  Requires-Dist: ipywidgets (>=8.1.5,<9.0.0)
22
+ Requires-Dist: jinja2 (>=3.1.0,<4.0.0)
22
23
  Requires-Dist: jupyter (>=1.1.1,<2.0.0)
23
24
  Requires-Dist: jupyter-console (>=6.6.3,<7.0.0)
24
25
  Requires-Dist: loguru (>=0.7.2,<0.8.0)
@@ -69,6 +70,49 @@ Description-Content-Type: text/markdown
69
70
 
70
71
  Qubx is a next-generation quantitative trading framework designed for efficient backtesting and live trading. Built with Python, it offers a robust environment for developing, testing, and deploying trading strategies.
71
72
 
73
+ ## Quick Start
74
+
75
+ ### 1. Install Dependencies
76
+ ```bash
77
+ poetry install
78
+ ```
79
+
80
+ ### 2. Create a Strategy
81
+ ```bash
82
+ # Create a simple strategy template (default)
83
+ poetry run qubx init
84
+
85
+ # Or specify a name and symbols
86
+ poetry run qubx init --name my_strategy --symbols BTCUSDT,ETHUSDT
87
+ ```
88
+
89
+ ### 3. Run Your Strategy
90
+ ```bash
91
+ cd my_strategy
92
+
93
+ # Run in paper trading mode
94
+ poetry run qubx run config.yml --paper
95
+
96
+ # Or run in Jupyter mode for interactive development
97
+ ./jpaper.sh
98
+ ```
99
+
100
+ ### Available Templates
101
+ ```bash
102
+ # List available strategy templates
103
+ poetry run qubx init --list-templates
104
+
105
+ # Create strategy with full project structure and MACD example
106
+ poetry run qubx init --template project --name my_project
107
+ ```
108
+
109
+ ### Strategy Development Workflow
110
+ 1. **Initialize**: `poetry run qubx init` - Create strategy from template
111
+ 2. **Develop**: Edit `strategy.py` to implement your trading logic
112
+ 3. **Test**: `poetry run qubx run config.yml --paper` - Run in paper mode
113
+ 4. **Debug**: `./jpaper.sh` - Use Jupyter for interactive development
114
+ 5. **Deploy**: Configure for live trading when ready
115
+
72
116
  ## Features
73
117
 
74
118
  - 🚀 High-performance backtesting engine
@@ -119,11 +163,13 @@ qubx --help # Show all available commands
119
163
 
120
164
  Available commands:
121
165
 
122
- - `qubx deploy` - Deploy a strategy from a zip file
123
- - `qubx ls` - List all strategies in a directory
124
- - `qubx release` - Package a strategy into a zip file
166
+ - `qubx init` - Create a new strategy from template
125
167
  - `qubx run` - Start a strategy with given configuration
126
168
  - `qubx simulate` - Run strategy simulation
169
+ - `qubx ls` - List all strategies in a directory
170
+ - `qubx release` - Package a strategy into a zip file
171
+ - `qubx deploy` - Deploy a strategy from a zip file
172
+ - `qubx browse` - Browse backtest results using interactive TUI
127
173
 
128
174
  ## Development
129
175
 
@@ -11,6 +11,49 @@
11
11
 
12
12
  Qubx is a next-generation quantitative trading framework designed for efficient backtesting and live trading. Built with Python, it offers a robust environment for developing, testing, and deploying trading strategies.
13
13
 
14
+ ## Quick Start
15
+
16
+ ### 1. Install Dependencies
17
+ ```bash
18
+ poetry install
19
+ ```
20
+
21
+ ### 2. Create a Strategy
22
+ ```bash
23
+ # Create a simple strategy template (default)
24
+ poetry run qubx init
25
+
26
+ # Or specify a name and symbols
27
+ poetry run qubx init --name my_strategy --symbols BTCUSDT,ETHUSDT
28
+ ```
29
+
30
+ ### 3. Run Your Strategy
31
+ ```bash
32
+ cd my_strategy
33
+
34
+ # Run in paper trading mode
35
+ poetry run qubx run config.yml --paper
36
+
37
+ # Or run in Jupyter mode for interactive development
38
+ ./jpaper.sh
39
+ ```
40
+
41
+ ### Available Templates
42
+ ```bash
43
+ # List available strategy templates
44
+ poetry run qubx init --list-templates
45
+
46
+ # Create strategy with full project structure and MACD example
47
+ poetry run qubx init --template project --name my_project
48
+ ```
49
+
50
+ ### Strategy Development Workflow
51
+ 1. **Initialize**: `poetry run qubx init` - Create strategy from template
52
+ 2. **Develop**: Edit `strategy.py` to implement your trading logic
53
+ 3. **Test**: `poetry run qubx run config.yml --paper` - Run in paper mode
54
+ 4. **Debug**: `./jpaper.sh` - Use Jupyter for interactive development
55
+ 5. **Deploy**: Configure for live trading when ready
56
+
14
57
  ## Features
15
58
 
16
59
  - 🚀 High-performance backtesting engine
@@ -61,11 +104,13 @@ qubx --help # Show all available commands
61
104
 
62
105
  Available commands:
63
106
 
64
- - `qubx deploy` - Deploy a strategy from a zip file
65
- - `qubx ls` - List all strategies in a directory
66
- - `qubx release` - Package a strategy into a zip file
107
+ - `qubx init` - Create a new strategy from template
67
108
  - `qubx run` - Start a strategy with given configuration
68
109
  - `qubx simulate` - Run strategy simulation
110
+ - `qubx ls` - List all strategies in a directory
111
+ - `qubx release` - Package a strategy into a zip file
112
+ - `qubx deploy` - Deploy a strategy from a zip file
113
+ - `qubx browse` - Browse backtest results using interactive TUI
69
114
 
70
115
  ## Development
71
116
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.67"
7
+ version = "0.6.68"
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"
@@ -76,6 +76,7 @@ websockets = "15.0.1"
76
76
  qubx-bitfinex-api = "^3.0.7"
77
77
  textual = "^0.88.0"
78
78
  rich = "^13.9.4"
79
+ jinja2 = "^3.1.0"
79
80
 
80
81
  [tool.ruff.lint]
81
82
  extend-select = [ "I",]
@@ -1,4 +1,5 @@
1
1
  from collections import defaultdict
2
+ from typing import Any, TypeVar
2
3
 
3
4
  import pandas as pd
4
5
 
@@ -19,6 +20,17 @@ from qubx.utils.time import infer_series_frequency
19
20
  from .account import SimulatedAccountProcessor
20
21
  from .utils import SimulatedTimeProvider
21
22
 
23
+ T = TypeVar("T")
24
+
25
+
26
+ def _get_first_existing(data: dict, keys: list, default: T = None) -> T:
27
+ data_get = data.get # Cache method lookup
28
+ sentinel = object()
29
+ for key in keys:
30
+ if (value := data_get(key, sentinel)) is not sentinel:
31
+ return value
32
+ return default
33
+
22
34
 
23
35
  class SimulatedDataProvider(IDataProvider):
24
36
  time_provider: SimulatedTimeProvider
@@ -164,8 +176,13 @@ class SimulatedDataProvider(IDataProvider):
164
176
  r.data["high"],
165
177
  r.data["low"],
166
178
  r.data["close"],
167
- r.data.get("volume", 0),
168
- r.data.get("bought_volume", 0),
179
+ volume=r.data.get("volume", 0),
180
+ bought_volume=_get_first_existing(r.data, ["taker_buy_volume", "bought_volume"], 0),
181
+ volume_quote=_get_first_existing(r.data, ["quote_volume", "volume_quote"], 0),
182
+ bought_volume_quote=_get_first_existing(
183
+ r.data, ["taker_buy_quote_volume", "bought_volume_quote"], 0
184
+ ),
185
+ trade_count=_get_first_existing(r.data, ["count", "trade_count"], 0),
169
186
  )
170
187
  )
171
188
 
@@ -275,5 +275,130 @@ def browse(results_path: str):
275
275
  run_backtest_browser(results_path)
276
276
 
277
277
 
278
+ @main.command()
279
+ @click.option(
280
+ "--template",
281
+ "-t",
282
+ type=str,
283
+ default="simple",
284
+ help="Built-in template to use (default: simple)",
285
+ show_default=True,
286
+ )
287
+ @click.option(
288
+ "--template-path",
289
+ type=click.Path(exists=True, resolve_path=True),
290
+ help="Path to custom template directory",
291
+ )
292
+ @click.option(
293
+ "--name",
294
+ "-n",
295
+ type=str,
296
+ default="my_strategy",
297
+ help="Name of the strategy to create",
298
+ show_default=True,
299
+ )
300
+ @click.option(
301
+ "--exchange",
302
+ "-e",
303
+ type=str,
304
+ default="BINANCE.UM",
305
+ help="Exchange to configure for the strategy",
306
+ show_default=True,
307
+ )
308
+ @click.option(
309
+ "--symbols",
310
+ "-s",
311
+ type=str,
312
+ default="BTCUSDT",
313
+ help="Comma-separated list of symbols to trade",
314
+ show_default=True,
315
+ )
316
+ @click.option(
317
+ "--timeframe",
318
+ type=str,
319
+ default="1h",
320
+ help="Timeframe for market data",
321
+ show_default=True,
322
+ )
323
+ @click.option(
324
+ "--output-dir",
325
+ "-o",
326
+ type=click.Path(resolve_path=True),
327
+ default=".",
328
+ help="Directory to create the strategy in",
329
+ show_default=True,
330
+ )
331
+ @click.option(
332
+ "--list-templates",
333
+ is_flag=True,
334
+ help="List all available built-in templates",
335
+ )
336
+ def init(
337
+ template: str,
338
+ template_path: str | None,
339
+ name: str,
340
+ exchange: str,
341
+ symbols: str,
342
+ timeframe: str,
343
+ output_dir: str,
344
+ list_templates: bool,
345
+ ):
346
+ """
347
+ Create a new strategy from a template.
348
+
349
+ This command generates a complete strategy project structure with:
350
+ - Strategy class implementing IStrategy interface
351
+ - Configuration file for qubx run command
352
+ - Package structure for proper imports
353
+
354
+ The generated strategy can be run immediately with:
355
+ poetry run qubx run --config config.yml --paper
356
+ """
357
+ from qubx.templates import TemplateManager, TemplateError
358
+
359
+ try:
360
+ manager = TemplateManager()
361
+
362
+ if list_templates:
363
+ templates = manager.list_templates()
364
+ if not templates:
365
+ click.echo("No templates available.")
366
+ return
367
+
368
+ click.echo("Available templates:")
369
+ for template_name, metadata in templates.items():
370
+ description = metadata.get("description", "No description")
371
+ click.echo(f" {template_name:<15} - {description}")
372
+ return
373
+
374
+ # Generate strategy
375
+ strategy_path = manager.generate_strategy(
376
+ template_name=template if not template_path else None,
377
+ template_path=template_path,
378
+ output_dir=output_dir,
379
+ name=name,
380
+ exchange=exchange,
381
+ symbols=symbols,
382
+ timeframe=timeframe,
383
+ )
384
+
385
+ click.echo(f"✅ Strategy '{name}' created successfully!")
386
+ click.echo(f"📁 Location: {strategy_path}")
387
+ click.echo()
388
+ click.echo("To run your strategy:")
389
+ click.echo(f" cd {strategy_path}")
390
+ click.echo(" poetry run qubx run config.yml --paper")
391
+ click.echo()
392
+ click.echo("To run in Jupyter mode:")
393
+ click.echo(" ./jpaper.sh")
394
+
395
+ except TemplateError as e:
396
+ click.echo(f"❌ Template error: {e}", err=True)
397
+ raise click.Abort()
398
+ except Exception as e:
399
+ click.echo(f"❌ Unexpected error: {e}", err=True)
400
+ raise click.Abort()
401
+
402
+
278
403
  if __name__ == "__main__":
279
404
  main()
@@ -0,0 +1,310 @@
1
+ """
2
+ Connection management for CCXT data provider.
3
+
4
+ This module handles WebSocket connections, retry logic, and stream lifecycle management,
5
+ separating connection concerns from subscription state and data handling.
6
+ """
7
+
8
+ import asyncio
9
+ import concurrent.futures
10
+ from asyncio.exceptions import CancelledError
11
+ from collections import defaultdict
12
+ from typing import Awaitable, Callable, Dict
13
+
14
+ from ccxt import ExchangeClosedByUser, ExchangeError, ExchangeNotAvailable, NetworkError
15
+ from ccxt.pro import Exchange
16
+ from qubx import logger
17
+ from qubx.core.basics import CtrlChannel
18
+
19
+ from .exceptions import CcxtSymbolNotRecognized
20
+ from .subscription_manager import SubscriptionManager
21
+
22
+
23
+ class ConnectionManager:
24
+ """
25
+ Manages WebSocket connections and stream lifecycle for CCXT data provider.
26
+
27
+ Responsibilities:
28
+ - Handle WebSocket connection establishment and management
29
+ - Implement retry logic and error handling
30
+ - Manage stream lifecycle (start, stop, cleanup)
31
+ - Coordinate with SubscriptionManager for state updates
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ exchange_id: str,
37
+ max_ws_retries: int = 10,
38
+ subscription_manager: SubscriptionManager | None = None
39
+ ):
40
+ self._exchange_id = exchange_id
41
+ self.max_ws_retries = max_ws_retries
42
+ self._subscription_manager = subscription_manager
43
+
44
+ # Stream state management
45
+ self._is_stream_enabled: Dict[str, bool] = defaultdict(lambda: False)
46
+ self._stream_to_unsubscriber: Dict[str, Callable[[], Awaitable[None]]] = {}
47
+
48
+ # Connection tracking
49
+ self._stream_to_coro: Dict[str, concurrent.futures.Future] = {}
50
+
51
+ def set_subscription_manager(self, subscription_manager: SubscriptionManager) -> None:
52
+ """Set the subscription manager for state coordination."""
53
+ self._subscription_manager = subscription_manager
54
+
55
+ async def listen_to_stream(
56
+ self,
57
+ subscriber: Callable[[], Awaitable[None]],
58
+ exchange: Exchange,
59
+ channel: CtrlChannel,
60
+ stream_name: str,
61
+ unsubscriber: Callable[[], Awaitable[None]] | None = None,
62
+ ) -> None:
63
+ """
64
+ Listen to a WebSocket stream with error handling and retry logic.
65
+
66
+ Args:
67
+ subscriber: Async function that handles the stream data
68
+ exchange: CCXT exchange instance
69
+ channel: Control channel for data flow
70
+ stream_name: Unique name for this stream
71
+ unsubscriber: Optional cleanup function for graceful unsubscription
72
+ """
73
+ logger.info(f"<yellow>{self._exchange_id}</yellow> Listening to {stream_name}")
74
+
75
+ # Register unsubscriber for cleanup
76
+ if unsubscriber is not None:
77
+ self._stream_to_unsubscriber[stream_name] = unsubscriber
78
+
79
+ # Enable the stream
80
+ self._is_stream_enabled[stream_name] = True
81
+ n_retry = 0
82
+ connection_established = False
83
+
84
+ while channel.control.is_set() and self._is_stream_enabled[stream_name]:
85
+ try:
86
+ await subscriber()
87
+ n_retry = 0 # Reset retry counter on success
88
+
89
+ # Mark subscription as active on first successful data reception
90
+ if not connection_established and self._subscription_manager:
91
+ subscription_type = self._subscription_manager.find_subscription_type_by_name(stream_name)
92
+ if subscription_type:
93
+ self._subscription_manager.mark_subscription_active(subscription_type)
94
+ connection_established = True
95
+
96
+ # Check if stream was disabled during subscriber execution
97
+ if not self._is_stream_enabled[stream_name]:
98
+ break
99
+
100
+ except CcxtSymbolNotRecognized:
101
+ # Skip unrecognized symbols but continue listening
102
+ continue
103
+ except CancelledError:
104
+ # Graceful cancellation
105
+ break
106
+ except ExchangeClosedByUser:
107
+ # Connection closed by us, stop gracefully
108
+ logger.info(f"<yellow>{self._exchange_id}</yellow> {stream_name} listening has been stopped")
109
+ break
110
+ except (NetworkError, ExchangeError, ExchangeNotAvailable) as e:
111
+ # Network/exchange errors - retry after short delay
112
+ logger.error(f"<yellow>{self._exchange_id}</yellow> {e.__class__.__name__} :: Error in {stream_name} : {e}")
113
+ await asyncio.sleep(1)
114
+ continue
115
+ except Exception as e:
116
+ # Unexpected errors
117
+ if not channel.control.is_set() or not self._is_stream_enabled[stream_name]:
118
+ # Channel closed or stream disabled, exit gracefully
119
+ break
120
+
121
+ logger.error(f"<yellow>{self._exchange_id}</yellow> Exception in {stream_name}: {e}")
122
+ logger.exception(e)
123
+
124
+ n_retry += 1
125
+ if n_retry >= self.max_ws_retries:
126
+ logger.error(
127
+ f"<yellow>{self._exchange_id}</yellow> Max retries reached for {stream_name}. Closing connection."
128
+ )
129
+ # Clean up exchange reference to force reconnection
130
+ del exchange
131
+ break
132
+
133
+ # Exponential backoff with cap at 60 seconds
134
+ await asyncio.sleep(min(2**n_retry, 60))
135
+
136
+ # Stream ended, cleanup
137
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Stream {stream_name} ended")
138
+
139
+ async def stop_stream(
140
+ self,
141
+ stream_name: str,
142
+ future: concurrent.futures.Future | None = None,
143
+ is_resubscription: bool = False
144
+ ) -> None:
145
+ """
146
+ Stop a stream gracefully with proper cleanup.
147
+
148
+ Args:
149
+ stream_name: Name of the stream to stop
150
+ future: Optional future representing the stream task
151
+ is_resubscription: True if this is stopping an old stream during resubscription
152
+ """
153
+ try:
154
+ context = "old stream" if is_resubscription else "stream"
155
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Stopping {context} {stream_name}")
156
+
157
+ # Disable the stream to signal it should stop
158
+ self._is_stream_enabled[stream_name] = False
159
+
160
+ # Wait for the stream to stop naturally
161
+ if future:
162
+ total_sleep_time = 0.0
163
+ while future.running() and total_sleep_time < 20.0:
164
+ await asyncio.sleep(1.0)
165
+ total_sleep_time += 1.0
166
+
167
+ if future.running():
168
+ logger.warning(
169
+ f"<yellow>{self._exchange_id}</yellow> {context.title()} {stream_name} is still running. Cancelling it."
170
+ )
171
+ future.cancel()
172
+ else:
173
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> {context.title()} {stream_name} has been stopped")
174
+
175
+ # Run unsubscriber if available
176
+ if stream_name in self._stream_to_unsubscriber:
177
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Unsubscribing from {stream_name}")
178
+ await self._stream_to_unsubscriber[stream_name]()
179
+ del self._stream_to_unsubscriber[stream_name]
180
+
181
+ # Clean up stream state
182
+ if is_resubscription:
183
+ # For resubscription, only clean up if the stream is actually disabled
184
+ # (avoid interfering with new streams using the same name)
185
+ if stream_name in self._is_stream_enabled and not self._is_stream_enabled[stream_name]:
186
+ del self._is_stream_enabled[stream_name]
187
+ else:
188
+ # For regular stops, always clean up completely
189
+ self._is_stream_enabled.pop(stream_name, None)
190
+ self._stream_to_coro.pop(stream_name, None)
191
+
192
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> {context.title()} {stream_name} stopped")
193
+
194
+ except Exception as e:
195
+ logger.error(f"<yellow>{self._exchange_id}</yellow> Error stopping {stream_name}")
196
+ logger.exception(e)
197
+
198
+ def register_stream_future(
199
+ self,
200
+ stream_name: str,
201
+ future: concurrent.futures.Future
202
+ ) -> None:
203
+ """
204
+ Register a future for a stream for tracking and cleanup.
205
+
206
+ Args:
207
+ stream_name: Name of the stream
208
+ future: Future representing the stream task
209
+ """
210
+ self._stream_to_coro[stream_name] = future
211
+
212
+ def is_stream_enabled(self, stream_name: str) -> bool:
213
+ """
214
+ Check if a stream is enabled.
215
+
216
+ Args:
217
+ stream_name: Name of the stream to check
218
+
219
+ Returns:
220
+ True if stream is enabled, False otherwise
221
+ """
222
+ return self._is_stream_enabled.get(stream_name, False)
223
+
224
+ def get_stream_future(self, stream_name: str) -> concurrent.futures.Future | None:
225
+ """
226
+ Get the future for a stream.
227
+
228
+ Args:
229
+ stream_name: Name of the stream
230
+
231
+ Returns:
232
+ Future if exists, None otherwise
233
+ """
234
+ return self._stream_to_coro.get(stream_name)
235
+
236
+ def disable_stream(self, stream_name: str) -> None:
237
+ """
238
+ Disable a stream (signal it to stop).
239
+
240
+ Args:
241
+ stream_name: Name of the stream to disable
242
+ """
243
+ self._is_stream_enabled[stream_name] = False
244
+
245
+ def enable_stream(self, stream_name: str) -> None:
246
+ """
247
+ Enable a stream.
248
+
249
+ Args:
250
+ stream_name: Name of the stream to enable
251
+ """
252
+ self._is_stream_enabled[stream_name] = True
253
+
254
+ def set_stream_unsubscriber(
255
+ self,
256
+ stream_name: str,
257
+ unsubscriber: Callable[[], Awaitable[None]]
258
+ ) -> None:
259
+ """
260
+ Set unsubscriber function for a stream.
261
+
262
+ Args:
263
+ stream_name: Name of the stream
264
+ unsubscriber: Async function to call for unsubscription
265
+ """
266
+ self._stream_to_unsubscriber[stream_name] = unsubscriber
267
+
268
+ def get_stream_unsubscriber(self, stream_name: str) -> Callable[[], Awaitable[None]] | None:
269
+ """
270
+ Get unsubscriber function for a stream.
271
+
272
+ Args:
273
+ stream_name: Name of the stream
274
+
275
+ Returns:
276
+ Unsubscriber function if exists, None otherwise
277
+ """
278
+ return self._stream_to_unsubscriber.get(stream_name)
279
+
280
+ def set_stream_coro(
281
+ self,
282
+ stream_name: str,
283
+ coro: concurrent.futures.Future
284
+ ) -> None:
285
+ """
286
+ Set coroutine/future for a stream.
287
+
288
+ Args:
289
+ stream_name: Name of the stream
290
+ coro: Future representing the stream task
291
+ """
292
+ self._stream_to_coro[stream_name] = coro
293
+
294
+ def get_stream_coro(self, stream_name: str) -> concurrent.futures.Future | None:
295
+ """
296
+ Get coroutine/future for a stream.
297
+
298
+ Args:
299
+ stream_name: Name of the stream
300
+
301
+ Returns:
302
+ Future if exists, None otherwise
303
+ """
304
+ return self._stream_to_coro.get(stream_name)
305
+
306
+ def cleanup_all_streams(self) -> None:
307
+ """Clean up all stream state (for shutdown)."""
308
+ self._is_stream_enabled.clear()
309
+ self._stream_to_unsubscriber.clear()
310
+ self._stream_to_coro.clear()