Qubx 0.6.59__tar.gz → 0.6.61__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 (172) hide show
  1. {qubx-0.6.59 → qubx-0.6.61}/PKG-INFO +1 -1
  2. {qubx-0.6.59 → qubx-0.6.61}/pyproject.toml +1 -1
  3. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/simulator.py +7 -6
  4. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/utils.py +1 -1
  5. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/basics.py +131 -60
  6. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/context.py +17 -0
  7. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/helpers.py +6 -2
  8. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/interfaces.py +48 -3
  9. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/loggers.py +64 -36
  10. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/metrics.py +20 -9
  11. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/mixins/processing.py +190 -92
  12. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/mixins/universe.py +12 -5
  13. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/series.pxd +1 -0
  14. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/series.pyi +2 -0
  15. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/series.pyx +13 -0
  16. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/emitters/__init__.py +9 -1
  17. qubx-0.6.61/src/qubx/emitters/indicator.py +213 -0
  18. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/gathering/simplest.py +1 -1
  19. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/loggers/csv.py +22 -7
  20. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/loggers/inmemory.py +18 -6
  21. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/loggers/mongo.py +2 -1
  22. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restarts/state_resolvers.py +62 -25
  23. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restarts/time_finders.py +47 -4
  24. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restorers/interfaces.py +8 -2
  25. qubx-0.6.61/src/qubx/restorers/signal.py +376 -0
  26. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restorers/state.py +25 -9
  27. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/trackers/advanced.py +4 -5
  28. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/trackers/composite.py +4 -4
  29. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/trackers/riskctrl.py +166 -39
  30. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/trackers/sizers.py +8 -8
  31. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/runner/_jupyter_runner.pyt +1 -1
  32. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/runner/runner.py +3 -2
  33. qubx-0.6.59/src/qubx/restorers/signal.py +0 -293
  34. {qubx-0.6.59 → qubx-0.6.61}/LICENSE +0 -0
  35. {qubx-0.6.59 → qubx-0.6.61}/README.md +0 -0
  36. {qubx-0.6.59 → qubx-0.6.61}/build.py +0 -0
  37. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/__init__.py +0 -0
  38. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/_nb_magic.py +0 -0
  39. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/__init__.py +0 -0
  40. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/account.py +0 -0
  41. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/broker.py +0 -0
  42. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/data.py +0 -0
  43. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/management.py +0 -0
  44. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/ome.py +0 -0
  45. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/optimization.py +0 -0
  46. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/runner.py +0 -0
  47. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/simulated_data.py +0 -0
  48. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/backtester/simulated_exchange.py +0 -0
  49. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/cli/__init__.py +0 -0
  50. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/cli/commands.py +0 -0
  51. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/cli/deploy.py +0 -0
  52. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/cli/misc.py +0 -0
  53. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/cli/release.py +0 -0
  54. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/cli/tui.py +0 -0
  55. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/__init__.py +0 -0
  56. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/account.py +0 -0
  57. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/broker.py +0 -0
  58. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/data.py +0 -0
  59. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  60. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  61. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  62. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  63. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  64. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  65. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  66. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/factory.py +0 -0
  67. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/reader.py +0 -0
  68. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/ccxt/utils.py +0 -0
  69. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/tardis/data.py +0 -0
  70. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/connectors/tardis/utils.py +0 -0
  71. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/__init__.py +0 -0
  72. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/account.py +0 -0
  73. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/deque.py +0 -0
  74. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/errors.py +0 -0
  75. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/exceptions.py +0 -0
  76. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/initializer.py +0 -0
  77. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/lookups.py +0 -0
  78. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/mixins/__init__.py +0 -0
  79. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/mixins/market.py +0 -0
  80. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/mixins/subscription.py +0 -0
  81. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/mixins/trading.py +0 -0
  82. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/utils.pyi +0 -0
  83. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/core/utils.pyx +0 -0
  84. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/data/__init__.py +0 -0
  85. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/data/composite.py +0 -0
  86. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/data/helpers.py +0 -0
  87. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/data/hft.py +0 -0
  88. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/data/readers.py +0 -0
  89. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/data/registry.py +0 -0
  90. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/data/tardis.py +0 -0
  91. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/emitters/base.py +0 -0
  92. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/emitters/composite.py +0 -0
  93. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/emitters/csv.py +0 -0
  94. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/emitters/prometheus.py +0 -0
  95. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/emitters/questdb.py +0 -0
  96. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/exporters/__init__.py +0 -0
  97. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/exporters/composite.py +0 -0
  98. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/exporters/formatters/__init__.py +0 -0
  99. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/exporters/formatters/base.py +0 -0
  100. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/exporters/formatters/incremental.py +0 -0
  101. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/exporters/formatters/slack.py +0 -0
  102. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/exporters/redis_streams.py +0 -0
  103. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/exporters/slack.py +0 -0
  104. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/features/__init__.py +0 -0
  105. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/features/core.py +0 -0
  106. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/features/orderbook.py +0 -0
  107. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/features/price.py +0 -0
  108. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/features/trades.py +0 -0
  109. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/features/utils.py +0 -0
  110. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/health/__init__.py +0 -0
  111. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/health/base.py +0 -0
  112. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/loggers/__init__.py +0 -0
  113. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/loggers/factory.py +0 -0
  114. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/math/__init__.py +0 -0
  115. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/math/stats.py +0 -0
  116. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/notifications/__init__.py +0 -0
  117. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/notifications/composite.py +0 -0
  118. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/notifications/slack.py +0 -0
  119. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/notifications/throttler.py +0 -0
  120. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/pandaz/__init__.py +0 -0
  121. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/pandaz/ta.py +0 -0
  122. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/pandaz/utils.py +0 -0
  123. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/_build.py +0 -0
  124. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/crypto-fees.ini +0 -0
  125. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  126. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  127. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  128. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  129. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  130. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  131. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -0
  132. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -0
  133. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  134. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  135. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  136. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restarts/__init__.py +0 -0
  137. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restorers/__init__.py +0 -0
  138. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restorers/balance.py +0 -0
  139. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restorers/factory.py +0 -0
  140. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restorers/position.py +0 -0
  141. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/restorers/utils.py +0 -0
  142. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/ta/__init__.py +0 -0
  143. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/ta/indicators.pxd +0 -0
  144. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/ta/indicators.pyi +0 -0
  145. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/ta/indicators.pyx +0 -0
  146. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/trackers/__init__.py +0 -0
  147. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/trackers/rebalancers.py +0 -0
  148. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/__init__.py +0 -0
  149. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/_pyxreloader.py +0 -0
  150. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/charting/lookinglass.py +0 -0
  151. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  152. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/collections.py +0 -0
  153. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/marketdata/binance.py +0 -0
  154. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/marketdata/ccxt.py +0 -0
  155. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/marketdata/dukas.py +0 -0
  156. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/misc.py +0 -0
  157. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/ntp.py +0 -0
  158. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/numbers_utils.py +0 -0
  159. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/orderbook.py +0 -0
  160. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/plotting/__init__.py +0 -0
  161. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/plotting/dashboard.py +0 -0
  162. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/plotting/data.py +0 -0
  163. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/plotting/interfaces.py +0 -0
  164. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  165. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  166. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/questdb.py +0 -0
  167. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/runner/__init__.py +0 -0
  168. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/runner/accounts.py +0 -0
  169. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/runner/configs.py +0 -0
  170. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/runner/factory.py +0 -0
  171. {qubx-0.6.59 → qubx-0.6.61}/src/qubx/utils/time.py +0 -0
  172. {qubx-0.6.59 → qubx-0.6.61}/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.59
3
+ Version: 0.6.61
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.59"
7
+ version = "0.6.61"
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"
@@ -265,12 +265,13 @@ def _run_setup(
265
265
  stop,
266
266
  setup.exchanges,
267
267
  setup.instruments,
268
- setup.capital,
269
- setup.base_currency,
270
- commissions_for_result,
271
- runner.logs_writer.get_portfolio(as_plain_dataframe=True),
272
- runner.logs_writer.get_executions(),
273
- runner.logs_writer.get_signals(),
268
+ capital=setup.capital,
269
+ base_currency=setup.base_currency,
270
+ commissions=commissions_for_result,
271
+ portfolio_log=runner.logs_writer.get_portfolio(as_plain_dataframe=True),
272
+ executions_log=runner.logs_writer.get_executions(),
273
+ signals_log=runner.logs_writer.get_signals(),
274
+ targets_log=runner.logs_writer.get_targets(),
274
275
  strategy_class=runner.strategy_class,
275
276
  parameters=runner.strategy_params,
276
277
  is_simulation=True,
@@ -209,7 +209,7 @@ class SignalsProxy(IStrategy):
209
209
  signal = event.data.get("order")
210
210
  # - TODO: also need to think about how to pass stop/take here
211
211
  if signal is not None and event.instrument:
212
- return [event.instrument.signal(signal)]
212
+ return [event.instrument.signal(ctx, signal)]
213
213
  return None
214
214
 
215
215
 
@@ -56,22 +56,73 @@ class TimestampedDict:
56
56
  data: dict[str, Any]
57
57
 
58
58
 
59
+ class ITimeProvider:
60
+ """
61
+ Generic interface for providing current time
62
+ """
63
+
64
+ def time(self) -> dt_64:
65
+ """
66
+ Returns current time
67
+ """
68
+ ...
69
+
70
+
59
71
  # Alias for timestamped data types used in Qubx
60
72
  Timestamped: TypeAlias = Quote | Trade | Bar | OrderBook | TimestampedDict | FundingRate | Liquidation
61
73
 
62
74
 
75
+ @dataclass
76
+ class TargetPosition:
77
+ """
78
+ Class for presenting target position calculated from signal
79
+ """
80
+
81
+ time: dt_64 | str # time when position was created
82
+ instrument: "Instrument"
83
+ target_position_size: float # actual position size after processing in sizer
84
+ entry_price: float | None = None
85
+ stop_price: float | None = None
86
+ take_price: float | None = None
87
+ options: dict[str, Any] = field(default_factory=dict)
88
+
89
+ @property
90
+ def price(self) -> float | None:
91
+ return self.entry_price
92
+
93
+ @property
94
+ def stop(self) -> float | None:
95
+ return self.stop_price
96
+
97
+ @property
98
+ def take(self) -> float | None:
99
+ return self.take_price
100
+
101
+ def __str__(self) -> str:
102
+ _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
103
+ _p = f" @ {self.entry_price}" if self.entry_price is not None else ""
104
+ _s = f" stop: {self.stop_price}" if self.stop_price is not None else ""
105
+ _t = f" take: {self.take_price}" if self.take_price is not None else ""
106
+ return f"[{_d}] TARGET {self.target_position_size:+f} {self.instrument.base}{_p}{_s}{_t} for {self.instrument}"
107
+
108
+
63
109
  @dataclass
64
110
  class Signal:
65
111
  """
66
112
  Class for presenting signals generated by strategy
67
113
 
68
114
  Attributes:
69
- reference_price: float - exact price when signal was generated
115
+ reference_price: float - aux market price when signal was generated
116
+ is_service: bool - when we need this signal only for informative purposes (post-factum risk management etc)
70
117
 
71
118
  Options:
72
- - allow_override: bool - if True, and there is another signal for the same instrument, then override current.
119
+ - allow_override: bool - if True, and there is another signal for the same instrument, then override current.
120
+ - group: str - group name for signal
121
+ - comment: str - comment for signal
122
+ - options: dict[str, Any] - additional options for signal
73
123
  """
74
124
 
125
+ time: dt_64 | str # time when signal was generated
75
126
  instrument: "Instrument"
76
127
  signal: float
77
128
  price: float | None = None
@@ -81,20 +132,37 @@ class Signal:
81
132
  group: str = ""
82
133
  comment: str = ""
83
134
  options: dict[str, Any] = field(default_factory=dict)
135
+ is_service: bool = False # when we need this signal only for informative purposes (post-factum risk management etc)
136
+
137
+ def target_for_amount(self, amount: float, **kwargs) -> TargetPosition:
138
+ assert not self.is_service, "Service signals can't be converted to target positions !"
139
+ return self.instrument.target(
140
+ self.time,
141
+ self.instrument.round_size_down(amount),
142
+ entry_price=self.price,
143
+ stop_price=self.stop,
144
+ take_price=self.take,
145
+ options=self.options,
146
+ **kwargs,
147
+ )
84
148
 
85
149
  def __str__(self) -> str:
150
+ _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
86
151
  _p = f" @ {self.price}" if self.price is not None else ""
87
152
  _s = f" stop: {self.stop}" if self.stop is not None else ""
88
153
  _t = f" take: {self.take}" if self.take is not None else ""
89
154
  _r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
90
155
  _c = f" ({self.comment})" if self.comment else ""
91
- return f"{self.group}{_r} {self.signal:+f} {self.instrument}{_p}{_s}{_t}{_c}"
156
+ _i = "SERVICE ::" if self.is_service else ""
157
+
158
+ return f"[{_d}] {_i}{self.group}{_r} {self.signal:+.2f} {self.instrument}{_p}{_s}{_t}{_c}"
92
159
 
93
160
  def copy(self) -> "Signal":
94
161
  """
95
162
  Return a copy of the original signal
96
163
  """
97
164
  return Signal(
165
+ self.time,
98
166
  self.instrument,
99
167
  self.signal,
100
168
  self.price,
@@ -104,60 +172,27 @@ class Signal:
104
172
  self.group,
105
173
  self.comment,
106
174
  dict(self.options),
175
+ self.is_service,
107
176
  )
108
177
 
109
178
 
110
179
  @dataclass
111
- class TargetPosition:
180
+ class InitializingSignal(Signal):
112
181
  """
113
- Class for presenting target position calculated from signal
182
+ Special signal type for post-warmup initialization
114
183
  """
115
184
 
116
- time: dt_64 # time when position was set
117
- signal: Signal # original signal
118
- target_position_size: float # actual position size after processing in sizer
119
- _is_service: bool = False
120
-
121
- @staticmethod
122
- def create(ctx: "ITimeProvider", signal: Signal, target_size: float) -> "TargetPosition":
123
- return TargetPosition(ctx.time(), signal, signal.instrument.round_size_down(target_size))
124
-
125
- @staticmethod
126
- def zero(ctx: "ITimeProvider", signal: Signal) -> "TargetPosition":
127
- return TargetPosition(ctx.time(), signal, 0.0)
128
-
129
- @staticmethod
130
- def service(ctx: "ITimeProvider", signal: Signal, size: float | None = None) -> "TargetPosition":
131
- """
132
- Generate just service position target (for logging purposes)
133
- """
134
- return TargetPosition(ctx.time(), signal, size if size else signal.signal, _is_service=True)
135
-
136
- @property
137
- def instrument(self) -> "Instrument":
138
- return self.signal.instrument
139
-
140
- @property
141
- def price(self) -> float | None:
142
- return self.signal.price
143
-
144
- @property
145
- def stop(self) -> float | None:
146
- return self.signal.stop
147
-
148
- @property
149
- def take(self) -> float | None:
150
- return self.signal.take
151
-
152
- @property
153
- def is_service(self) -> bool:
154
- """
155
- Some target may be used just for informative purposes (post-factum risk management etc)
156
- """
157
- return self._is_service
185
+ use_limit_order: bool = False # if True, then use limit order for post-warmup initialization
158
186
 
159
187
  def __str__(self) -> str:
160
- return f"{'::: INFORMATIVE ::: ' if self.is_service else ''}Target {self.target_position_size:+f} for {self.signal}"
188
+ _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
189
+ _p = f" @ {self.price}" if self.price is not None else ""
190
+ _s = f" stop: {self.stop}" if self.stop is not None else ""
191
+ _t = f" take: {self.take}" if self.take is not None else ""
192
+ _r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
193
+ _c = f" ({self.comment})" if self.comment else ""
194
+
195
+ return f"[{_d}] POST-WARMUP-INIT ::{self.group}{_r} {self.signal:+.2f} {self.instrument}{_p}{_s}{_t}{_c}"
161
196
 
162
197
 
163
198
  class AssetType(StrEnum):
@@ -261,8 +296,26 @@ class Instrument:
261
296
  """
262
297
  return prec_ceil(price, self.price_precision)
263
298
 
299
+ def service_signal(
300
+ self,
301
+ time: dt_64 | str | ITimeProvider,
302
+ signal: float,
303
+ price: float | None = None,
304
+ stop: float | None = None,
305
+ take: float | None = None,
306
+ group: str = "",
307
+ comment: str = "",
308
+ options: dict[str, Any] | None = None,
309
+ **kwargs,
310
+ ) -> Signal:
311
+ """
312
+ Create service signal for the instrument
313
+ """
314
+ return self.signal(time, signal, price, stop, take, group, comment, options, is_service=True, **kwargs)
315
+
264
316
  def signal(
265
317
  self,
318
+ time: dt_64 | str | ITimeProvider,
266
319
  signal: float,
267
320
  price: float | None = None,
268
321
  stop: float | None = None,
@@ -270,9 +323,14 @@ class Instrument:
270
323
  group: str = "",
271
324
  comment: str = "",
272
325
  options: dict[str, Any] | None = None,
326
+ is_service: bool = False,
273
327
  **kwargs,
274
328
  ) -> Signal:
329
+ """
330
+ Create signal for the instrument
331
+ """
275
332
  return Signal(
333
+ time=time.time() if isinstance(time, ITimeProvider) else time,
276
334
  instrument=self,
277
335
  signal=signal,
278
336
  price=price,
@@ -281,6 +339,30 @@ class Instrument:
281
339
  group=group,
282
340
  comment=comment,
283
341
  options=(options or {}) | kwargs,
342
+ is_service=is_service,
343
+ )
344
+
345
+ def target(
346
+ self,
347
+ time: dt_64 | str | ITimeProvider,
348
+ amount: float,
349
+ entry_price: float | None = None,
350
+ stop_price: float | None = None,
351
+ take_price: float | None = None,
352
+ options: dict[str, Any] | None = None,
353
+ **kwargs,
354
+ ) -> TargetPosition:
355
+ """
356
+ Create target position for the instrument
357
+ """
358
+ return TargetPosition(
359
+ time=time.time() if isinstance(time, ITimeProvider) else time,
360
+ instrument=self,
361
+ target_position_size=self.round_size_down(amount),
362
+ entry_price=entry_price,
363
+ stop_price=stop_price,
364
+ take_price=take_price,
365
+ options=(options or {}) | kwargs,
284
366
  )
285
367
 
286
368
  def __hash__(self) -> int:
@@ -736,18 +818,6 @@ class CtrlChannel:
736
818
  raise QueueTimeout(f"Timeout waiting for data on {self.name} channel")
737
819
 
738
820
 
739
- class ITimeProvider:
740
- """
741
- Generic interface for providing current time
742
- """
743
-
744
- def time(self) -> dt_64:
745
- """
746
- Returns current time
747
- """
748
- ...
749
-
750
-
751
821
  class DataType(StrEnum):
752
822
  """
753
823
  Data type constants. Used for specifying the type of data and can be used for subscription to.
@@ -884,6 +954,7 @@ class RestoredState:
884
954
 
885
955
  time: np.datetime64
886
956
  balances: dict[str, AssetBalance]
957
+ instrument_to_signal_positions: dict[Instrument, list[Signal]]
887
958
  instrument_to_target_positions: dict[Instrument, list[TargetPosition]]
888
959
  positions: dict[Instrument, Position]
889
960
 
@@ -15,6 +15,8 @@ from qubx.core.basics import (
15
15
  Order,
16
16
  OrderRequest,
17
17
  Position,
18
+ Signal,
19
+ TargetPosition,
18
20
  Timestamped,
19
21
  dt_64,
20
22
  )
@@ -87,6 +89,7 @@ class StrategyContext(IStrategyContext):
87
89
 
88
90
  _warmup_positions: dict[Instrument, Position] | None = None
89
91
  _warmup_orders: dict[Instrument, list[Order]] | None = None
92
+ _warmup_active_targets: dict[Instrument, list[TargetPosition]] | None = None
90
93
 
91
94
  def __init__(
92
95
  self,
@@ -420,6 +423,8 @@ class StrategyContext(IStrategyContext):
420
423
 
421
424
  # ITradingManager delegation
422
425
  def trade(self, instrument: Instrument, amount: float, price: float | None = None, time_in_force="gtc", **options):
426
+ # TODO: we need to generate target position and apply it in the processing manager
427
+ # - one of the options is to have multiple entry levels in TargetPosition class
423
428
  return self._trading_manager.trade(instrument, amount, price, time_in_force, **options)
424
429
 
425
430
  def trade_async(
@@ -522,16 +527,28 @@ class StrategyContext(IStrategyContext):
522
527
  def is_fitted(self) -> bool:
523
528
  return self._processing_manager.is_fitted()
524
529
 
530
+ def get_active_targets(self) -> dict[Instrument, list[TargetPosition]]:
531
+ return self._processing_manager.get_active_targets()
532
+
533
+ def emit_signal(self, signal: Signal) -> None:
534
+ return self._processing_manager.emit_signal(signal)
535
+
525
536
  # IWarmupStateSaver delegation
526
537
  def set_warmup_positions(self, positions: dict[Instrument, Position]) -> None:
527
538
  self._warmup_positions = positions
528
539
 
540
+ def set_warmup_active_targets(self, active_targets: dict[Instrument, list[TargetPosition]]) -> None:
541
+ self._warmup_active_targets = active_targets
542
+
529
543
  def set_warmup_orders(self, orders: dict[Instrument, list[Order]]) -> None:
530
544
  self._warmup_orders = orders
531
545
 
532
546
  def get_warmup_positions(self) -> dict[Instrument, Position]:
533
547
  return self._warmup_positions if self._warmup_positions is not None else {}
534
548
 
549
+ def get_warmup_active_targets(self) -> dict[Instrument, list[TargetPosition]]:
550
+ return self._warmup_active_targets if self._warmup_active_targets is not None else {}
551
+
535
552
  def get_warmup_orders(self) -> dict[Instrument, list[Order]]:
536
553
  return self._warmup_orders if self._warmup_orders is not None else {}
537
554
 
@@ -276,6 +276,10 @@ def _parse_schedule_spec(schedule: str) -> dict[str, str]:
276
276
  return {k: v for k, v in m.groupdict().items() if v} if m else {}
277
277
 
278
278
 
279
+ def _to_dt_64(time: float) -> np.datetime64:
280
+ return np.datetime64(int(time * 1000000000), "ns")
281
+
282
+
279
283
  def process_schedule_spec(spec_str: str | None) -> dict[str, Any]:
280
284
  AS_INT = lambda d, k: int(d.get(k, 0)) # noqa: E731
281
285
  S = lambda s: [x for x in re.split(r"[, ]", s) if x] # noqa: E731
@@ -409,11 +413,11 @@ class BasicScheduler:
409
413
  prev_time = iter.get_prev()
410
414
  next_time = iter.get_next(start_time=start_time)
411
415
  if next_time:
412
- self._scdlr.enterabs(next_time, 1, self._trigger, (event, prev_time, next_time))
416
+ self._scdlr.enterabs(next_time, 1, self._trigger, (event, _to_dt_64(prev_time), _to_dt_64(next_time)))
413
417
 
414
418
  # - update next nearest time
415
419
  self._next_times[event] = next_time
416
- self._next_nearest_time = np.datetime64(int(min(self._next_times.values()) * 1000000000), "ns")
420
+ self._next_nearest_time = _to_dt_64(min(self._next_times.values()))
417
421
  # logger.debug(f" >>> ({event}) task is scheduled at {self._next_nearest_time}")
418
422
 
419
423
  return True
@@ -508,7 +508,9 @@ class IMarketManager(ITimeProvider):
508
508
  """
509
509
  ...
510
510
 
511
- def ohlc_pd(self, instrument: Instrument, timeframe: str | None = None, length: int | None = None, consolidated: bool = True) -> pd.DataFrame:
511
+ def ohlc_pd(
512
+ self, instrument: Instrument, timeframe: str | None = None, length: int | None = None, consolidated: bool = True
513
+ ) -> pd.DataFrame:
512
514
  """Get OHLCV data for an instrument as pandas DataFrame.
513
515
 
514
516
  Args:
@@ -1035,6 +1037,26 @@ class IProcessingManager:
1035
1037
  """
1036
1038
  ...
1037
1039
 
1040
+ def get_active_targets(self) -> dict[Instrument, TargetPosition]:
1041
+ """
1042
+ Get active target positions for each instrument in the universe.
1043
+ Target position (TP) is considered active if
1044
+ 1. signal (S) is sent, converted to a TP, and position is open
1045
+ 2. S is sent, converted to a TP, and limit order is sent for opening
1046
+
1047
+ So when position is closed TP (because of opposite signal or stop loss/take profit) becomes inactive.
1048
+
1049
+ Returns:
1050
+ dict[Instrument, TargetPosition]: Dictionary mapping instruments to their active targets.
1051
+ """
1052
+ ...
1053
+
1054
+ def emit_signal(self, signal: Signal) -> None:
1055
+ """
1056
+ Emit a signal for processing
1057
+ """
1058
+ ...
1059
+
1038
1060
 
1039
1061
  class IWarmupStateSaver:
1040
1062
  """
@@ -1057,6 +1079,14 @@ class IWarmupStateSaver:
1057
1079
  """Get warmup orders."""
1058
1080
  ...
1059
1081
 
1082
+ def set_warmup_active_targets(self, active_targets: dict[Instrument, TargetPosition]) -> None:
1083
+ """Set warmup active targets."""
1084
+ ...
1085
+
1086
+ def get_warmup_active_targets(self) -> dict[Instrument, TargetPosition]:
1087
+ """Get warmup active targets."""
1088
+ ...
1089
+
1060
1090
 
1061
1091
  @dataclass
1062
1092
  class StrategyState:
@@ -1118,6 +1148,11 @@ class IStrategyContext(
1118
1148
  """Check if the strategy context is running in simulation mode."""
1119
1149
  return False
1120
1150
 
1151
+ @property
1152
+ def is_live_or_warmup(self) -> bool:
1153
+ """Check if the strategy context is running in live or warmup mode."""
1154
+ return not self.is_simulation or self.is_warmup_in_progress
1155
+
1121
1156
  @property
1122
1157
  def is_paper_trading(self) -> bool:
1123
1158
  """Check if the strategy context is running in simulated trading mode."""
@@ -1145,8 +1180,6 @@ class IPositionGathering:
1145
1180
  res = {}
1146
1181
  if targets:
1147
1182
  for t in targets:
1148
- if t.is_service: # we skip processing service positions
1149
- continue
1150
1183
  try:
1151
1184
  res[t.instrument] = self.alter_position_size(ctx, t)
1152
1185
  except Exception as ex:
@@ -1237,6 +1270,16 @@ class PositionsTracker:
1237
1270
  """
1238
1271
  ...
1239
1272
 
1273
+ def restore_position_from_target(self, ctx: IStrategyContext, target: TargetPosition):
1274
+ """
1275
+ Restore active position and tracking from the target.
1276
+
1277
+ Args:
1278
+ - ctx: Strategy context object.
1279
+ - target: Target position to restore from.
1280
+ """
1281
+ ...
1282
+
1240
1283
 
1241
1284
  @dataclass
1242
1285
  class HealthMetrics:
@@ -1515,6 +1558,7 @@ class StateResolverProtocol(Protocol):
1515
1558
  ctx: "IStrategyContext",
1516
1559
  sim_positions: dict[Instrument, Position],
1517
1560
  sim_orders: dict[Instrument, list[Order]],
1561
+ sim_active_targets: dict[Instrument, TargetPosition],
1518
1562
  ) -> None:
1519
1563
  """
1520
1564
  Resolve position mismatches between warmup simulation and live trading.
@@ -1523,6 +1567,7 @@ class StateResolverProtocol(Protocol):
1523
1567
  ctx (IStrategyContext): The strategy context
1524
1568
  sim_positions (dict[Instrument, Position]): Positions from the simulation
1525
1569
  sim_orders (dict[Instrument, list[Order]]): Orders from the simulation
1570
+ sim_active_targets (dict[Instrument, TargetPosition]): Active targets from the simulation
1526
1571
  """
1527
1572
  ...
1528
1573