Qubx 0.6.4__tar.gz → 0.6.7__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 (146) hide show
  1. {qubx-0.6.4 → qubx-0.6.7}/PKG-INFO +4 -2
  2. {qubx-0.6.4 → qubx-0.6.7}/pyproject.toml +10 -2
  3. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/account.py +2 -1
  4. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/ome.py +56 -41
  5. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/runner.py +22 -5
  6. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/simulated_data.py +2 -156
  7. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/utils.py +15 -3
  8. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/cli/commands.py +10 -35
  9. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/cli/deploy.py +2 -2
  10. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/cli/misc.py +54 -20
  11. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/cli/release.py +219 -105
  12. qubx-0.6.7/src/qubx/connectors/ccxt/__init__.py +3 -0
  13. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/connectors/ccxt/broker.py +10 -2
  14. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/connectors/ccxt/data.py +1 -1
  15. qubx-0.6.7/src/qubx/connectors/ccxt/reader.py +237 -0
  16. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/connectors/ccxt/utils.py +48 -37
  17. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/account.py +2 -1
  18. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/basics.py +4 -2
  19. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/context.py +109 -24
  20. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/exceptions.py +4 -0
  21. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/helpers.py +66 -14
  22. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/initializer.py +34 -10
  23. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/interfaces.py +212 -25
  24. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/lookups.py +26 -32
  25. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/metrics.py +5 -9
  26. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/mixins/market.py +2 -4
  27. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/mixins/processing.py +125 -17
  28. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/mixins/trading.py +12 -3
  29. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/series.pxd +2 -0
  30. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/series.pyi +1 -0
  31. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/series.pyx +136 -2
  32. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/data/__init__.py +2 -0
  33. qubx-0.6.7/src/qubx/data/composite.py +491 -0
  34. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/data/helpers.py +18 -27
  35. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/data/hft.py +51 -3
  36. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/data/readers.py +32 -17
  37. qubx-0.6.7/src/qubx/data/registry.py +124 -0
  38. qubx-0.6.7/src/qubx/data/tardis.py +879 -0
  39. qubx-0.6.7/src/qubx/emitters/__init__.py +17 -0
  40. qubx-0.6.7/src/qubx/emitters/base.py +206 -0
  41. qubx-0.6.7/src/qubx/emitters/composite.py +78 -0
  42. qubx-0.6.7/src/qubx/emitters/prometheus.py +222 -0
  43. qubx-0.6.7/src/qubx/emitters/questdb.py +126 -0
  44. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/exporters/redis_streams.py +61 -21
  45. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/exporters/slack.py +37 -23
  46. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/gathering/simplest.py +1 -1
  47. qubx-0.6.7/src/qubx/notifications/__init__.py +11 -0
  48. qubx-0.6.7/src/qubx/notifications/composite.py +71 -0
  49. qubx-0.6.7/src/qubx/notifications/slack.py +174 -0
  50. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/pandaz/ta.py +3 -1
  51. qubx-0.6.7/src/qubx/resources/_build.py +237 -0
  52. qubx-0.6.7/src/qubx/restarts/state_resolvers.py +118 -0
  53. qubx-0.6.7/src/qubx/restarts/time_finders.py +64 -0
  54. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/restorers/interfaces.py +2 -2
  55. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/restorers/signal.py +32 -20
  56. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/restorers/state.py +9 -7
  57. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/trackers/riskctrl.py +34 -11
  58. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/misc.py +43 -0
  59. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/orderbook.py +43 -1
  60. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/runner/_jupyter_runner.pyt +130 -23
  61. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/runner/configs.py +37 -5
  62. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/runner/runner.py +407 -67
  63. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/time.py +14 -0
  64. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/version.py +1 -1
  65. qubx-0.6.4/src/qubx/data/composite.py +0 -149
  66. qubx-0.6.4/src/qubx/data/tardis.py +0 -100
  67. qubx-0.6.4/src/qubx/restarts/state_resolvers.py +0 -66
  68. qubx-0.6.4/src/qubx/restarts/time_finders.py +0 -34
  69. qubx-0.6.4/src/qubx/utils/runner/__init__.py +0 -0
  70. {qubx-0.6.4 → qubx-0.6.7}/README.md +0 -0
  71. {qubx-0.6.4 → qubx-0.6.7}/build.py +0 -0
  72. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/__init__.py +0 -0
  73. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/_nb_magic.py +0 -0
  74. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/__init__.py +0 -0
  75. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/broker.py +0 -0
  76. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/data.py +0 -0
  77. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/management.py +0 -0
  78. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/optimization.py +0 -0
  79. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/backtester/simulator.py +0 -0
  80. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/cli/__init__.py +0 -0
  81. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/connectors/ccxt/account.py +0 -0
  82. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/connectors/ccxt/customizations.py +0 -0
  83. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  84. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/connectors/ccxt/factory.py +0 -0
  85. {qubx-0.6.4/src/qubx/connectors/ccxt → qubx-0.6.7/src/qubx/core}/__init__.py +0 -0
  86. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/loggers.py +0 -0
  87. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/mixins/__init__.py +0 -0
  88. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/mixins/subscription.py +0 -0
  89. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/mixins/universe.py +0 -0
  90. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/utils.pyi +0 -0
  91. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/core/utils.pyx +0 -0
  92. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/exporters/__init__.py +0 -0
  93. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/exporters/composite.py +0 -0
  94. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/exporters/formatters/__init__.py +0 -0
  95. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/exporters/formatters/base.py +0 -0
  96. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/exporters/formatters/incremental.py +0 -0
  97. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/exporters/formatters/slack.py +0 -0
  98. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/features/__init__.py +0 -0
  99. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/features/core.py +0 -0
  100. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/features/orderbook.py +0 -0
  101. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/features/price.py +0 -0
  102. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/features/trades.py +0 -0
  103. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/features/utils.py +0 -0
  104. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/math/__init__.py +0 -0
  105. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/math/stats.py +0 -0
  106. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/pandaz/__init__.py +0 -0
  107. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/pandaz/utils.py +0 -0
  108. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
  109. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/resources/instruments/symbols-binance.json +0 -0
  110. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
  111. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
  112. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
  113. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
  114. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
  115. {qubx-0.6.4/src/qubx/core → qubx-0.6.7/src/qubx/restarts}/__init__.py +0 -0
  116. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/restorers/__init__.py +0 -0
  117. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/restorers/balance.py +0 -0
  118. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/restorers/factory.py +0 -0
  119. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/restorers/position.py +0 -0
  120. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/restorers/utils.py +0 -0
  121. {qubx-0.6.4/src/qubx/restarts → qubx-0.6.7/src/qubx/ta}/__init__.py +0 -0
  122. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/ta/indicators.pxd +0 -0
  123. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/ta/indicators.pyi +0 -0
  124. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/ta/indicators.pyx +0 -0
  125. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/trackers/__init__.py +0 -0
  126. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/trackers/advanced.py +0 -0
  127. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/trackers/composite.py +0 -0
  128. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/trackers/rebalancers.py +0 -0
  129. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/trackers/sizers.py +0 -0
  130. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/__init__.py +0 -0
  131. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/_pyxreloader.py +0 -0
  132. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/charting/lookinglass.py +0 -0
  133. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  134. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/marketdata/binance.py +0 -0
  135. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/marketdata/ccxt.py +0 -0
  136. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/marketdata/dukas.py +0 -0
  137. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/ntp.py +0 -0
  138. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/numbers_utils.py +0 -0
  139. {qubx-0.6.4/src/qubx/ta → qubx-0.6.7/src/qubx/utils/plotting}/__init__.py +0 -0
  140. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/plotting/dashboard.py +0 -0
  141. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/plotting/data.py +0 -0
  142. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/plotting/interfaces.py +0 -0
  143. {qubx-0.6.4/src/qubx/utils/plotting → qubx-0.6.7/src/qubx/utils/plotting/renderers}/__init__.py +0 -0
  144. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  145. {qubx-0.6.4/src/qubx/utils/plotting/renderers → qubx-0.6.7/src/qubx/utils/runner}/__init__.py +0 -0
  146. {qubx-0.6.4 → qubx-0.6.7}/src/qubx/utils/runner/accounts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.4
3
+ Version: 0.6.7
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -10,13 +10,13 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: aiohttp (>=3.11.14,<4.0.0)
13
14
  Requires-Dist: ccxt (>=4.2.68,<5.0.0)
14
15
  Requires-Dist: croniter (>=2.0.5,<3.0.0)
15
16
  Requires-Dist: cython (==3.0.8)
16
17
  Requires-Dist: dash (>=2.18.2,<3.0.0)
17
18
  Requires-Dist: dash-bootstrap-components (>=1.6.0,<2.0.0)
18
19
  Requires-Dist: gitpython (>=3.1.44,<4.0.0)
19
- Requires-Dist: hftbacktest (>=2.2.0,<3.0.0)
20
20
  Requires-Dist: importlib-metadata
21
21
  Requires-Dist: ipywidgets (>=8.1.5,<9.0.0)
22
22
  Requires-Dist: jupyter (>=1.1.1,<2.0.0)
@@ -27,6 +27,7 @@ Requires-Dist: msgspec (>=0.18.6,<0.19.0)
27
27
  Requires-Dist: ntplib (>=0.4.0,<0.5.0)
28
28
  Requires-Dist: numba (>=0.59.1,<0.60.0)
29
29
  Requires-Dist: numpy (>=1.26.3,<2.0.0)
30
+ Requires-Dist: orjson (>=3.10.15,<4.0.0)
30
31
  Requires-Dist: pandas (>=2.2.2,<3.0.0)
31
32
  Requires-Dist: plotly (>=5.22.0,<6.0.0)
32
33
  Requires-Dist: psycopg (>=3.1.18,<4.0.0)
@@ -38,6 +39,7 @@ Requires-Dist: pymongo (>=4.6.1,<5.0.0)
38
39
  Requires-Dist: python-binance (>=1.0.19,<2.0.0)
39
40
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
40
41
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
42
+ Requires-Dist: questdb (>=2.0.3,<3.0.0)
41
43
  Requires-Dist: redis (>=5.2.1,<6.0.0)
42
44
  Requires-Dist: scikit-learn (>=1.4.2,<2.0.0)
43
45
  Requires-Dist: scipy (>=1.12.0,<2.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.4"
7
+ version = "0.6.7"
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"
@@ -69,7 +69,9 @@ gitpython = "^3.1.44"
69
69
  jupyter = "^1.1.1"
70
70
  jupyter-console = "^6.6.3"
71
71
  ipywidgets = "^8.1.5"
72
- hftbacktest = "^2.2.0"
72
+ questdb = "^2.0.3"
73
+ orjson = "^3.10.15"
74
+ aiohttp = "^3.11.14"
73
75
 
74
76
  [tool.ruff.lint]
75
77
  extend-select = [ "I",]
@@ -113,6 +115,12 @@ pytest-mock = "^3.12.0"
113
115
  pytest-lazy-fixture = "^0.6.3"
114
116
  pytest-cov = "^4.1.0"
115
117
 
118
+ [tool.poetry.group.k8.dependencies]
119
+ prometheus-client = "^0.21.1"
120
+
121
+ [tool.poetry.group.hft.dependencies]
122
+ hftbacktest = "^2.2.0"
123
+
116
124
  [tool.poetry.group.test.dependencies.pytest]
117
125
  extras = [ "lazyfixture",]
118
126
  version = "^8.2.0"
@@ -154,7 +154,8 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
154
154
  return
155
155
  for r in ome.process_market_data(data):
156
156
  if r.exec is not None:
157
- self.order_to_instrument.pop(r.order.id)
157
+ if r.order.id in self.order_to_instrument:
158
+ self.order_to_instrument.pop(r.order.id)
158
159
  # - process methods will be called from stg context
159
160
  self._channel.send((instrument, "order", r.order, False))
160
161
  self._channel.send((instrument, "deals", [r.exec], False))
@@ -7,6 +7,8 @@ from sortedcontainers import SortedDict
7
7
  from qubx import logger
8
8
  from qubx.core.basics import (
9
9
  OPTION_FILL_AT_SIGNAL_PRICE,
10
+ OPTION_SIGNAL_PRICE,
11
+ OPTION_SKIP_PRICE_CROSS_CONTROL,
10
12
  Deal,
11
13
  Instrument,
12
14
  ITimeProvider,
@@ -194,54 +196,67 @@ class OrdersManagementEngine:
194
196
  if order.status in ["CLOSED", "CANCELED"]:
195
197
  raise InvalidOrder(f"Order {order.id} is already closed or canceled.")
196
198
 
197
- buy_side = order.side == "BUY"
198
- c_ask = self.bbo.ask
199
- c_bid = self.bbo.bid
199
+ _buy_side = order.side == "BUY"
200
+ _c_ask = self.bbo.ask # type: ignore
201
+ _c_bid = self.bbo.bid # type: ignore
200
202
 
201
203
  # - check if order can be "executed" immediately
202
- exec_price = None
204
+ _exec_price = None
203
205
  _need_update_book = False
204
206
 
205
- if order.type == "MARKET":
206
- if exec_price is None:
207
- exec_price = c_ask if buy_side else c_bid
208
-
209
- # - special case - fill at signal price for market order
210
- # - only for simulation
211
- # - only if this is valid price: market crossed this desired price on last update
212
- if order.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False) and order.price and self.__prev_bbo:
213
- _desired_fill_price = order.price
214
-
215
- if (buy_side and self.__prev_bbo.ask < _desired_fill_price <= c_ask) or (
216
- not buy_side and self.__prev_bbo.bid > _desired_fill_price >= c_bid
217
- ):
218
- exec_price = _desired_fill_price
219
- else:
220
- raise SimulationError(
221
- f"Special execution price at {_desired_fill_price} for market order {order.id} cannot be filled because market didn't cross this price on last update !"
222
- )
223
-
224
- elif order.type == "LIMIT":
225
- _need_update_book = True
226
- if (buy_side and order.price >= c_ask) or (not buy_side and order.price <= c_bid):
227
- exec_price = c_ask if buy_side else c_bid
228
-
229
- elif order.type == "STOP_MARKET":
230
- # - it processes stop orders separately without adding to orderbook (as on real exchanges)
231
- order.status = "OPEN"
232
- self.stop_orders[order.id] = order
233
-
234
- elif order.type == "STOP_LIMIT":
235
- # TODO: (OME) check trigger conditions in options etc
236
- raise NotImplementedError("'STOP_LIMIT' order is not supported in Qubx simulator yet !")
207
+ match order.type:
208
+ case "MARKET":
209
+ if _exec_price is None:
210
+ _exec_price = _c_ask if _buy_side else _c_bid
211
+
212
+ # - special case only for simulation: exact fill at signal price for market orders
213
+ _fill_at_signal_price = order.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False)
214
+ _signal_price = order.options.get(OPTION_SIGNAL_PRICE, None)
215
+
216
+ # - some cases require to skip price cross control
217
+ _skip_price_cross_control = order.options.get(OPTION_SKIP_PRICE_CROSS_CONTROL, False)
218
+
219
+ # - it's passed only if signal price is valid: market crossed this desired price on last update
220
+ if _fill_at_signal_price and _signal_price and self.__prev_bbo:
221
+ _desired_fill_price = _signal_price
222
+ _prev_mp = self.__prev_bbo.mid_price()
223
+ _c_mid_price = self.bbo.mid_price() # type: ignore
224
+
225
+ if (
226
+ _skip_price_cross_control
227
+ or (_prev_mp < _desired_fill_price <= _c_mid_price)
228
+ or (_prev_mp > _desired_fill_price >= _c_mid_price)
229
+ ):
230
+ _exec_price = _desired_fill_price
231
+ else:
232
+ raise SimulationError(
233
+ f"Special execution price at {_desired_fill_price} for market order {order.id} cannot be filled because market didn't cross this price on last update !"
234
+ )
235
+
236
+ case "LIMIT":
237
+ _need_update_book = True
238
+ if (_buy_side and order.price >= _c_ask) or (not _buy_side and order.price <= _c_bid):
239
+ _exec_price = _c_ask if _buy_side else _c_bid
240
+
241
+ case "STOP_MARKET":
242
+ # - it processes stop orders separately without adding to orderbook (as on real exchanges)
243
+ order.status = "OPEN"
244
+ self.stop_orders[order.id] = order
245
+
246
+ case "STOP_LIMIT":
247
+ # TODO: (OME) check trigger conditions in options etc
248
+ raise NotImplementedError("'STOP_LIMIT' order is not supported in Qubx simulator yet !")
249
+
250
+ case _:
251
+ raise SimulationError(f"Invalid order type: {order.type} for {self.instrument.symbol}")
237
252
 
238
253
  # - if order must be "executed" immediately
239
- if exec_price is not None:
240
- return self._execute_order(timestamp, exec_price, order, True)
254
+ if _exec_price is not None:
255
+ return self._execute_order(timestamp, _exec_price, order, True)
241
256
 
242
257
  # - processing limit orders
243
258
  if _need_update_book:
244
- if buy_side:
259
+ if _buy_side:
245
260
  self.bids.setdefault(order.price, list()).append(order.id)
246
261
  else:
247
262
  self.asks.setdefault(order.price, list()).append(order.id)
@@ -288,8 +303,8 @@ class OrdersManagementEngine:
288
303
  if (_ot == "LIMIT" or _ot.startswith("STOP")) and (price is None or price <= 0):
289
304
  raise InvalidOrder("Invalid order price. Price must be positively defined for LIMIT or STOP orders.")
290
305
 
291
- if time_in_force.upper() not in ["GTC", "IOC"]:
292
- raise InvalidOrder("Invalid time in force. Only GTC or IOC is supported for now.")
306
+ if time_in_force.upper() not in ["GTC", "IOC", "GTX"]:
307
+ raise InvalidOrder("Invalid time in force. Only GTC, IOC, GTX are supported for now.")
293
308
 
294
309
  if _ot.startswith("STOP"):
295
310
  assert price is not None
@@ -8,7 +8,8 @@ from qubx.core.basics import SW, DataType
8
8
  from qubx.core.context import StrategyContext
9
9
  from qubx.core.exceptions import SimulationConfigError, SimulationError
10
10
  from qubx.core.helpers import extract_parameters_from_object, full_qualified_class_name
11
- from qubx.core.interfaces import IStrategy, IStrategyContext
11
+ from qubx.core.initializer import BasicStrategyInitializer
12
+ from qubx.core.interfaces import IMetricEmitter, IStrategy, IStrategyContext, StrategyState
12
13
  from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
13
14
  from qubx.core.lookups import lookup
14
15
  from qubx.pandaz.utils import _frame_to_str
@@ -57,6 +58,9 @@ class SimulationRunner:
57
58
  stop: pd.Timestamp | str,
58
59
  account_id: str = "SimulatedAccount",
59
60
  portfolio_log_freq: str = "5Min",
61
+ emitter: IMetricEmitter | None = None,
62
+ strategy_state: StrategyState | None = None,
63
+ initializer: BasicStrategyInitializer | None = None,
60
64
  ):
61
65
  """
62
66
  Initialize the BacktestContextRunner with a strategy context.
@@ -68,6 +72,7 @@ class SimulationRunner:
68
72
  stop (pd.Timestamp): The end time of the simulation.
69
73
  account_id (str): The account id to use.
70
74
  portfolio_log_freq (str): The portfolio log frequency to use.
75
+ emitter (IMetricEmitter): The emitter to use.
71
76
  """
72
77
  self.setup = setup
73
78
  self.data_config = data_config
@@ -75,6 +80,9 @@ class SimulationRunner:
75
80
  self.stop = pd.Timestamp(stop)
76
81
  self.account_id = account_id
77
82
  self.portfolio_log_freq = portfolio_log_freq
83
+ self.emitter = emitter
84
+ self.strategy_state = strategy_state if strategy_state is not None else StrategyState()
85
+ self.initializer = initializer
78
86
  self.ctx = self._create_backtest_context()
79
87
 
80
88
  # - get strategy parameters BEFORE simulation start
@@ -85,7 +93,7 @@ class SimulationRunner:
85
93
  self.strategy_params = extract_parameters_from_object(self.setup.generator)
86
94
  self.strategy_class = full_qualified_class_name(self.setup.generator)
87
95
 
88
- def run(self, silent: bool = False):
96
+ def run(self, silent: bool = False, catch_keyboard_interrupt: bool = True, close_data_readers: bool = False):
89
97
  """
90
98
  Run the backtest from start to stop.
91
99
 
@@ -94,7 +102,7 @@ class SimulationRunner:
94
102
  stop (pd.Timestamp | str): The end time of the simulation.
95
103
  silent (bool, optional): Whether to suppress progress output. Defaults to False.
96
104
  """
97
- logger.debug(f"[<y>BacktestContextRunner</y>] :: Running simulation from {self.start} to {self.stop}")
105
+ logger.debug(f"[<y>SimulationRunner</y>] :: Running simulation from {self.start} to {self.stop}")
98
106
 
99
107
  # Start the context
100
108
  self.ctx.start()
@@ -103,7 +111,7 @@ class SimulationRunner:
103
111
  for s in self.ctx.get_subscriptions():
104
112
  if not self.ctx.get_warmup(s) and (_d_wt := self.data_config.default_warmups.get(s)):
105
113
  logger.debug(
106
- f"[<y>BacktestContextRunner</y>] :: Strategy didn't set warmup period for <c>{s}</c> so default <c>{_d_wt}</c> will be used"
114
+ f"[<y>SimulationRunner</y>] :: Strategy didn't set warmup period for <c>{s}</c> so default <c>{_d_wt}</c> will be used"
107
115
  )
108
116
  self.ctx.set_warmup({s: _d_wt})
109
117
 
@@ -129,13 +137,19 @@ class SimulationRunner:
129
137
  stop = self._stop or self.stop
130
138
 
131
139
  try:
132
- # Run the data provider
133
140
  self.data_provider.run(self.start, stop, silent=silent)
134
141
  except KeyboardInterrupt:
135
142
  logger.error("Simulated trading interrupted by user!")
143
+ if not catch_keyboard_interrupt:
144
+ raise
136
145
  finally:
137
146
  # Stop the context
138
147
  self.ctx.stop()
148
+ if close_data_readers:
149
+ assert isinstance(self.data_provider, SimulatedDataProvider)
150
+ for reader in self.data_provider._readers.values():
151
+ if hasattr(reader, "close"):
152
+ reader.close() # type: ignore
139
153
 
140
154
  def print_latency_report(self) -> None:
141
155
  _l_r = SW.latency_report()
@@ -229,6 +243,9 @@ class SimulationRunner:
229
243
  instruments=self.setup.instruments,
230
244
  logging=StrategyLogging(logs_writer, portfolio_log_freq=self.portfolio_log_freq),
231
245
  aux_data_provider=_aux_data,
246
+ emitter=self.emitter,
247
+ strategy_state=self.strategy_state,
248
+ initializer=self.initializer,
232
249
  )
233
250
 
234
251
  # - setup base subscription from spec
@@ -1,12 +1,11 @@
1
- import math
2
- from collections import defaultdict, deque
3
- from typing import Any, Iterator, TypeAlias
1
+ from typing import Any, Iterator
4
2
 
5
3
  import pandas as pd
6
4
 
7
5
  from qubx import logger
8
6
  from qubx.core.basics import DataType, Instrument, Timestamped
9
7
  from qubx.core.exceptions import SimulationError
8
+ from qubx.data.composite import IteratedDataStreamsSlicer
10
9
  from qubx.data.readers import (
11
10
  AsDict,
12
11
  AsOrderBook,
@@ -19,159 +18,6 @@ from qubx.data.readers import (
19
18
  RestoreTradesFromOHLC,
20
19
  )
21
20
 
22
- SlicerOutData: TypeAlias = tuple[str, int, Timestamped] | tuple
23
-
24
-
25
- class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
26
- """
27
- This class manages seamless iteration over multiple time-series data streams,
28
- ensuring that events are processed in the correct chronological order regardless of their source.
29
- It supports adding / removing new data streams to the slicer on the fly (during the itration).
30
- """
31
-
32
- _iterators: dict[str, Iterator[list[Timestamped]]]
33
- _buffers: dict[str, list[Timestamped]]
34
- _keys: deque[str]
35
- _iterating: bool
36
-
37
- def __init__(self):
38
- self._buffers = defaultdict(list)
39
- self._iterators = {}
40
- self._keys = deque()
41
- self._iterating = False
42
-
43
- def put(self, data: dict[str, Iterator[list[Timestamped]]]):
44
- _rebuild = False
45
- for k, vi in data.items():
46
- if k not in self._keys:
47
- self._iterators[k] = vi
48
- self._buffers[k] = self._load_next_chunk_to_buffer(k) # do initial chunk fetching
49
- self._keys.append(k)
50
- _rebuild = True
51
-
52
- # - rebuild strategy
53
- if _rebuild and self._iterating:
54
- self._build_initial_iteration_seq()
55
-
56
- def __add__(self, data: dict[str, Iterator]) -> "IteratedDataStreamsSlicer":
57
- self.put(data)
58
- return self
59
-
60
- def remove(self, keys: list[str] | str):
61
- """
62
- Remove data iterator and associated keys from the queue.
63
- If the key is not found, it does nothing.
64
- """
65
- _keys = keys if isinstance(keys, list) else [keys]
66
- _rebuild = False
67
- for i in _keys:
68
- if i in self._buffers:
69
- self._buffers.pop(i)
70
- self._iterators.pop(i)
71
- self._keys.remove(i)
72
- _rebuild = True
73
-
74
- # - rebuild strategy
75
- if _rebuild and self._iterating:
76
- self._build_initial_iteration_seq()
77
-
78
- def __iter__(self) -> Iterator:
79
- self._build_initial_iteration_seq()
80
- self._iterating = True
81
- return self
82
-
83
- def _build_initial_iteration_seq(self):
84
- _init_seq = {k: self._buffers[k][-1].time for k in self._keys}
85
- _init_seq = dict(sorted(_init_seq.items(), key=lambda item: item[1]))
86
- self._keys = deque(_init_seq.keys())
87
-
88
- def _load_next_chunk_to_buffer(self, index: str) -> list[Timestamped]:
89
- return list(reversed(next(self._iterators[index])))
90
-
91
- def _remove_iterator(self, key: str):
92
- self._buffers.pop(key)
93
- self._iterators.pop(key)
94
- self._keys.remove(key)
95
-
96
- def _pop_top(self, k: str) -> Timestamped:
97
- """
98
- Removes and returns the most recent timestamped data element from the buffer associated with the given key.
99
- If the buffer is empty after popping, it attempts to load the next chunk of data into the buffer.
100
- If no more data is available, the iterator associated with the key is removed.
101
-
102
- Parameters:
103
- k (str): The key identifying the data stream buffer to pop from.
104
-
105
- Returns:
106
- Timestamped: The most recent timestamped data element from the buffer.
107
- """
108
- v = (data := self._buffers[k]).pop()
109
- if not data:
110
- try:
111
- data.extend(self._load_next_chunk_to_buffer(k)) # - get next chunk of data
112
- except StopIteration:
113
- self._remove_iterator(k) # - remove iterable data
114
- return v
115
-
116
- def fetch_before_time(self, key: str, time_ns: int) -> list[Timestamped]:
117
- """
118
- Fetches and returns all timestamped data elements from the buffer associated with the given key
119
- that have a timestamp earlier than the specified time.
120
-
121
- Parameters:
122
- - key (str): The key identifying the data stream buffer to fetch from.
123
- - time_ns (int): The timestamp in nanoseconds. All returned elements will have a timestamp less than this value.
124
-
125
- Returns:
126
- - list[Timestamped]: A list of timestamped data elements that occur before the specified time.
127
- """
128
- values = []
129
- data = self._buffers[key]
130
- if not data:
131
- try:
132
- data.extend(self._load_next_chunk_to_buffer(key)) # - get next chunk of data
133
- except StopIteration:
134
- self._remove_iterator(key)
135
-
136
- # pull most past elements
137
- v = data[-1]
138
- while v.time < time_ns:
139
- values.append(data.pop())
140
- if not data:
141
- try:
142
- data.extend(self._load_next_chunk_to_buffer(key)) # - get next chunk of data
143
- except StopIteration:
144
- self._remove_iterator(key)
145
- break
146
- v = data[-1]
147
-
148
- return values
149
-
150
- def __next__(self) -> SlicerOutData:
151
- """
152
- Advances the iterator to the next available timestamped data element across all data streams.
153
-
154
- Returns:
155
- - SlicerOutData: A tuple containing the key of the data stream, the timestamp of the data element, and the data element itself.
156
-
157
- Raises:
158
- - StopIteration: If there are no more data elements to iterate over.
159
- """
160
- if not self._keys:
161
- self._iterating = False
162
- raise StopIteration
163
-
164
- _min_t = math.inf
165
- _min_k = self._keys[0]
166
- for i in self._keys:
167
- _x = self._buffers[i][-1]
168
- if _x.time < _min_t:
169
- _min_t = _x.time
170
- _min_k = i
171
-
172
- _v = self._pop_top(_min_k)
173
- return (_min_k, _v.time, _v)
174
-
175
21
 
176
22
  class DataFetcher:
177
23
  _fetcher_id: str
@@ -23,6 +23,7 @@ from qubx.core.interfaces import IStrategy, IStrategyContext, PositionsTracker
23
23
  from qubx.core.lookups import lookup
24
24
  from qubx.core.series import OHLCV, Bar, Quote, Trade
25
25
  from qubx.core.utils import time_delta_to_str
26
+ from qubx.data import TardisMachineReader
26
27
  from qubx.data.helpers import InMemoryCachedReader, TimeGuardedWrapper
27
28
  from qubx.data.hft import HftDataReader
28
29
  from qubx.data.readers import AsDict, DataReader, InMemoryDataFrameReader
@@ -86,9 +87,9 @@ class SimulationSetup:
86
87
  exchange: str
87
88
  capital: float
88
89
  base_currency: str
89
- commissions: str | None
90
- signal_timeframe: str
91
- accurate_stop_orders_execution: bool
90
+ commissions: str | None = None
91
+ signal_timeframe: str = "1Min"
92
+ accurate_stop_orders_execution: bool = False
92
93
 
93
94
  def __str__(self) -> str:
94
95
  return f"{self.name} {self.setup_type} capital {self.capital} {self.base_currency} for [{','.join(map(lambda x: x.symbol, self.instruments))}] @ {self.exchange}[{self.commissions}]"
@@ -729,6 +730,12 @@ def recognize_simulation_data_config(
729
730
 
730
731
  _available_symbols = list(set.intersection(*_sets_of_symbols.values()))
731
732
 
733
+ case TardisMachineReader():
734
+ _supported_types = [DataType.ORDERBOOK, DataType.TRADE]
735
+ _available_symbols = decls.get_symbols(exchange, _supported_types[0])
736
+ for _type in _supported_types:
737
+ _requests[_type] = (_type, decls)
738
+
732
739
  case DataReader():
733
740
  _supported_data_type = sniffer._sniff_reader(f"{exchange}:{instruments[0].symbol}", decls, None)
734
741
  _available_symbols = decls.get_symbols(exchange, DataType.from_str(_supported_data_type)[0])
@@ -766,6 +773,11 @@ def recognize_simulation_data_config(
766
773
  _available_symbols = _provider.get_symbols(exchange, _supported_data_type)
767
774
  _requests[_requested_type] = (_supported_data_type, _provider)
768
775
 
776
+ case TardisMachineReader():
777
+ _supported_data_type = _requested_type
778
+ _available_symbols = _provider.get_symbols(exchange, _supported_data_type)
779
+ _requests[_requested_type] = (_supported_data_type, _provider)
780
+
769
781
  case DataReader():
770
782
  _supported_data_type = sniffer._sniff_reader(
771
783
  f"{exchange}:{instruments[0].symbol}", _provider, _requested_type
@@ -1,8 +1,8 @@
1
1
  import os
2
- import sys
3
2
  from pathlib import Path
4
3
 
5
4
  import click
5
+ from dotenv import load_dotenv
6
6
 
7
7
  from qubx import QubxLogConfig, logger
8
8
 
@@ -32,13 +32,18 @@ def main(debug: bool, debug_port: int, log_level: str):
32
32
  """
33
33
  Qubx CLI.
34
34
  """
35
+ os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
35
36
  log_level = log_level.upper() if not debug else "DEBUG"
36
37
 
38
+ env_file = Path.cwd().joinpath(".env")
39
+ if env_file.exists():
40
+ logger.info(f"Loading environment variables from {env_file}")
41
+ load_dotenv(env_file)
42
+ log_level = os.getenv("QUBX_LOG_LEVEL", log_level)
43
+
37
44
  QubxLogConfig.set_log_level(log_level)
38
45
 
39
46
  if debug:
40
- os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
41
-
42
47
  import debugpy
43
48
 
44
49
  logger.info(f"Waiting for debugger to attach (port {debug_port})")
@@ -146,7 +151,7 @@ def ls(directory: str):
146
151
  "-o",
147
152
  type=click.STRING,
148
153
  help="Output directory to put zip file.",
149
- default="releases",
154
+ default=".releases",
150
155
  show_default=True,
151
156
  )
152
157
  @click.option(
@@ -173,27 +178,6 @@ def ls(directory: str):
173
178
  help="Commit changes and create tag in repo (default: False)",
174
179
  show_default=True,
175
180
  )
176
- @click.option(
177
- "--default-exchange",
178
- type=click.STRING,
179
- help="Default exchange to use in the generated config.",
180
- default="BINANCE.UM",
181
- show_default=True,
182
- )
183
- @click.option(
184
- "--default-connector",
185
- type=click.STRING,
186
- help="Default connector to use in the generated config.",
187
- default="ccxt",
188
- show_default=True,
189
- )
190
- @click.option(
191
- "--default-instruments",
192
- type=click.STRING,
193
- help="Default instruments to use in the generated config (comma-separated).",
194
- default="BTCUSDT",
195
- show_default=True,
196
- )
197
181
  def release(
198
182
  directory: str,
199
183
  strategy: str,
@@ -201,15 +185,12 @@ def release(
201
185
  message: str | None,
202
186
  commit: bool,
203
187
  output_dir: str,
204
- default_exchange: str,
205
- default_connector: str,
206
- default_instruments: str,
207
188
  ) -> None:
208
189
  """
209
190
  Releases the strategy to a zip file.
210
191
 
211
192
  The strategy can be specified in two ways:
212
- 1. As a strategy name (class name) - strategies are scanned in the given directory
193
+ 1. As a strategy name (class name) - strategies are scanned in the given directory (NOT SUPPORTED ANYMORE !)
213
194
  2. As a path to a config YAML file containing the strategy configuration in StrategyConfig format
214
195
 
215
196
  If a strategy name is provided, a default configuration will be generated with:
@@ -228,9 +209,6 @@ def release(
228
209
  """
229
210
  from .release import release_strategy
230
211
 
231
- # Parse default instruments
232
- instruments = [instr.strip() for instr in default_instruments.split(",")]
233
-
234
212
  release_strategy(
235
213
  directory=directory,
236
214
  strategy_name=strategy,
@@ -238,9 +216,6 @@ def release(
238
216
  message=message,
239
217
  commit=commit,
240
218
  output_dir=output_dir,
241
- default_exchange=default_exchange,
242
- default_connector=default_connector,
243
- default_instruments=instruments,
244
219
  )
245
220
 
246
221
 
@@ -161,10 +161,10 @@ def setup_poetry_environment(output_dir: str) -> bool:
161
161
  if var in env:
162
162
  del env[var]
163
163
 
164
- subprocess.run(install_cmd, cwd=output_dir, check=True, capture_output=True, text=True, env=env)
164
+ subprocess.run(install_cmd, cwd=output_dir, check=True, capture_output=False, text=True, env=env)
165
165
  else:
166
166
  # Normal case - not in a Poetry shell
167
- subprocess.run(install_cmd, cwd=output_dir, check=True, capture_output=True, text=True)
167
+ subprocess.run(install_cmd, cwd=output_dir, check=True, capture_output=False, text=True)
168
168
 
169
169
  # Verify that the virtual environment was created
170
170
  venv_path = os.path.join(output_dir, ".venv")