Qubx 0.6.21__tar.gz → 0.6.23__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 (153) hide show
  1. {qubx-0.6.21 → qubx-0.6.23}/PKG-INFO +1 -1
  2. {qubx-0.6.21 → qubx-0.6.23}/pyproject.toml +1 -1
  3. qubx-0.6.23/src/qubx/backtester/account.py +83 -0
  4. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/broker.py +19 -38
  5. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/data.py +6 -7
  6. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/ome.py +13 -9
  7. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/runner.py +15 -4
  8. qubx-0.6.23/src/qubx/backtester/simulated_exchange.py +233 -0
  9. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/context.py +1 -0
  10. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/interfaces.py +29 -29
  11. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/ta/indicators.pxd +1 -1
  12. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/ta/indicators.pyx +5 -8
  13. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/runner/runner.py +7 -3
  14. qubx-0.6.21/src/qubx/backtester/account.py +0 -161
  15. {qubx-0.6.21 → qubx-0.6.23}/LICENSE +0 -0
  16. {qubx-0.6.21 → qubx-0.6.23}/README.md +0 -0
  17. {qubx-0.6.21 → qubx-0.6.23}/build.py +0 -0
  18. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/__init__.py +0 -0
  19. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/_nb_magic.py +0 -0
  20. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/__init__.py +0 -0
  21. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/management.py +0 -0
  22. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/optimization.py +0 -0
  23. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/simulated_data.py +0 -0
  24. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/simulator.py +0 -0
  25. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/backtester/utils.py +0 -0
  26. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/cli/__init__.py +0 -0
  27. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/cli/commands.py +0 -0
  28. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/cli/deploy.py +0 -0
  29. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/cli/misc.py +0 -0
  30. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/cli/release.py +0 -0
  31. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/__init__.py +0 -0
  32. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/account.py +0 -0
  33. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/broker.py +0 -0
  34. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/data.py +0 -0
  35. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  36. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  37. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  38. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  39. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/factory.py +0 -0
  40. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/reader.py +0 -0
  41. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/connectors/ccxt/utils.py +0 -0
  42. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/__init__.py +0 -0
  43. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/account.py +0 -0
  44. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/basics.py +0 -0
  45. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/deque.py +0 -0
  46. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/errors.py +0 -0
  47. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/exceptions.py +0 -0
  48. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/helpers.py +0 -0
  49. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/initializer.py +0 -0
  50. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/loggers.py +0 -0
  51. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/lookups.py +0 -0
  52. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/metrics.py +0 -0
  53. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/mixins/__init__.py +0 -0
  54. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/mixins/market.py +0 -0
  55. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/mixins/processing.py +0 -0
  56. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/mixins/subscription.py +0 -0
  57. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/mixins/trading.py +0 -0
  58. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/mixins/universe.py +0 -0
  59. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/series.pxd +0 -0
  60. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/series.pyi +0 -0
  61. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/series.pyx +0 -0
  62. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/utils.pyi +0 -0
  63. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/core/utils.pyx +0 -0
  64. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/data/__init__.py +0 -0
  65. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/data/composite.py +0 -0
  66. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/data/helpers.py +0 -0
  67. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/data/hft.py +0 -0
  68. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/data/readers.py +0 -0
  69. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/data/registry.py +0 -0
  70. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/data/tardis.py +0 -0
  71. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/emitters/__init__.py +0 -0
  72. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/emitters/base.py +0 -0
  73. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/emitters/composite.py +0 -0
  74. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/emitters/csv.py +0 -0
  75. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/emitters/prometheus.py +0 -0
  76. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/emitters/questdb.py +0 -0
  77. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/exporters/__init__.py +0 -0
  78. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/exporters/composite.py +0 -0
  79. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/exporters/formatters/__init__.py +0 -0
  80. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/exporters/formatters/base.py +0 -0
  81. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/exporters/formatters/incremental.py +0 -0
  82. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/exporters/formatters/slack.py +0 -0
  83. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/exporters/redis_streams.py +0 -0
  84. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/exporters/slack.py +0 -0
  85. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/features/__init__.py +0 -0
  86. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/features/core.py +0 -0
  87. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/features/orderbook.py +0 -0
  88. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/features/price.py +0 -0
  89. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/features/trades.py +0 -0
  90. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/features/utils.py +0 -0
  91. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/gathering/simplest.py +0 -0
  92. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/health/__init__.py +0 -0
  93. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/health/base.py +0 -0
  94. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/math/__init__.py +0 -0
  95. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/math/stats.py +0 -0
  96. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/notifications/__init__.py +0 -0
  97. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/notifications/composite.py +0 -0
  98. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/notifications/slack.py +0 -0
  99. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/pandaz/__init__.py +0 -0
  100. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/pandaz/ta.py +0 -0
  101. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/pandaz/utils.py +0 -0
  102. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/resources/_build.py +0 -0
  103. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
  104. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/resources/instruments/symbols-binance.json +0 -0
  105. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
  106. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
  107. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
  108. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
  109. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
  110. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restarts/__init__.py +0 -0
  111. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restarts/state_resolvers.py +0 -0
  112. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restarts/time_finders.py +0 -0
  113. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restorers/__init__.py +0 -0
  114. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restorers/balance.py +0 -0
  115. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restorers/factory.py +0 -0
  116. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restorers/interfaces.py +0 -0
  117. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restorers/position.py +0 -0
  118. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restorers/signal.py +0 -0
  119. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restorers/state.py +0 -0
  120. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/restorers/utils.py +0 -0
  121. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/ta/__init__.py +0 -0
  122. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/ta/indicators.pyi +0 -0
  123. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/trackers/__init__.py +0 -0
  124. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/trackers/advanced.py +0 -0
  125. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/trackers/composite.py +0 -0
  126. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/trackers/rebalancers.py +0 -0
  127. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/trackers/riskctrl.py +0 -0
  128. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/trackers/sizers.py +0 -0
  129. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/__init__.py +0 -0
  130. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/_pyxreloader.py +0 -0
  131. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/charting/lookinglass.py +0 -0
  132. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  133. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/collections.py +0 -0
  134. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/marketdata/binance.py +0 -0
  135. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/marketdata/ccxt.py +0 -0
  136. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/marketdata/dukas.py +0 -0
  137. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/misc.py +0 -0
  138. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/ntp.py +0 -0
  139. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/numbers_utils.py +0 -0
  140. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/orderbook.py +0 -0
  141. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/plotting/__init__.py +0 -0
  142. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/plotting/dashboard.py +0 -0
  143. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/plotting/data.py +0 -0
  144. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/plotting/interfaces.py +0 -0
  145. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  146. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  147. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/runner/__init__.py +0 -0
  148. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  149. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/runner/accounts.py +0 -0
  150. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/runner/configs.py +0 -0
  151. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/runner/factory.py +0 -0
  152. {qubx-0.6.21 → qubx-0.6.23}/src/qubx/utils/time.py +0 -0
  153. {qubx-0.6.21 → qubx-0.6.23}/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.21
3
+ Version: 0.6.23
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.21"
7
+ version = "0.6.23"
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"
@@ -0,0 +1,83 @@
1
+ from qubx.backtester.simulated_exchange import ISimulatedExchange
2
+ from qubx.core.account import BasicAccountProcessor
3
+ from qubx.core.basics import (
4
+ CtrlChannel,
5
+ Instrument,
6
+ Order,
7
+ Position,
8
+ Timestamped,
9
+ dt_64,
10
+ )
11
+ from qubx.core.series import OrderBook, Quote, Trade, TradeArray
12
+ from qubx.restorers import RestoredState
13
+
14
+
15
+ class SimulatedAccountProcessor(BasicAccountProcessor):
16
+ _channel: CtrlChannel
17
+ _exchange: ISimulatedExchange
18
+
19
+ def __init__(
20
+ self,
21
+ account_id: str,
22
+ exchange: ISimulatedExchange,
23
+ channel: CtrlChannel,
24
+ base_currency: str,
25
+ initial_capital: float,
26
+ restored_state: RestoredState | None = None,
27
+ ) -> None:
28
+ super().__init__(
29
+ account_id=account_id,
30
+ time_provider=exchange.get_time_provider(),
31
+ base_currency=base_currency,
32
+ tcc=exchange.get_transaction_costs_calculator(),
33
+ initial_capital=initial_capital,
34
+ )
35
+
36
+ self._exchange = exchange
37
+ self._channel = channel
38
+
39
+ if restored_state is not None:
40
+ self._balances.update(restored_state.balances)
41
+ for instrument, position in restored_state.positions.items():
42
+ _pos = self.get_position(instrument)
43
+ _pos.reset_by_position(position)
44
+
45
+ def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
46
+ return self._exchange.get_open_orders(instrument)
47
+
48
+ def get_position(self, instrument: Instrument) -> Position:
49
+ if instrument in self.positions:
50
+ return self.positions[instrument]
51
+
52
+ # - initialize empty position
53
+ position = Position(instrument) # type: ignore
54
+ self.attach_positions(position)
55
+ return self.positions[instrument]
56
+
57
+ def update_position_price(self, time: dt_64, instrument: Instrument, update: float | Timestamped) -> None:
58
+ self.get_position(instrument)
59
+
60
+ super().update_position_price(time, instrument, update)
61
+
62
+ quote = (
63
+ update if isinstance(update, Quote) else self._exchange.emulate_quote_from_data(instrument, time, update)
64
+ )
65
+ if quote is None:
66
+ return
67
+
68
+ # - process new data
69
+ self._process_new_data(instrument, quote)
70
+
71
+ def process_market_data(self, time: dt_64, instrument: Instrument, update: Timestamped) -> None:
72
+ if isinstance(update, (TradeArray, Quote, Trade, OrderBook)):
73
+ # - process new data
74
+ self._process_new_data(instrument, update)
75
+
76
+ super().process_market_data(time, instrument, update)
77
+
78
+ def _process_new_data(self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray) -> None:
79
+ for r in self._exchange.process_market_data(instrument, data):
80
+ if r.exec is not None:
81
+ # - process methods will be called from stg context
82
+ self._channel.send((instrument, "order", r.order, False))
83
+ self._channel.send((instrument, "deals", [r.exec], False))
@@ -1,4 +1,5 @@
1
- from qubx.backtester.ome import OmeReport
1
+ from qubx.backtester.ome import SimulatedExecutionReport
2
+ from qubx.backtester.simulated_exchange import ISimulatedExchange
2
3
  from qubx.core.basics import (
3
4
  CtrlChannel,
4
5
  Instrument,
@@ -13,16 +14,17 @@ class SimulatedBroker(IBroker):
13
14
  channel: CtrlChannel
14
15
 
15
16
  _account: SimulatedAccountProcessor
17
+ _exchange: ISimulatedExchange
16
18
 
17
19
  def __init__(
18
20
  self,
19
21
  channel: CtrlChannel,
20
22
  account: SimulatedAccountProcessor,
21
- exchange_id: str = "simulated",
23
+ simulated_exchange: ISimulatedExchange,
22
24
  ) -> None:
23
25
  self.channel = channel
24
26
  self._account = account
25
- self._exchange_id = exchange_id
27
+ self._exchange = simulated_exchange
26
28
 
27
29
  @property
28
30
  def is_simulated_trading(self) -> bool:
@@ -39,22 +41,12 @@ class SimulatedBroker(IBroker):
39
41
  time_in_force: str = "gtc",
40
42
  **options,
41
43
  ) -> Order:
42
- ome = self._account.ome.get(instrument)
43
- if ome is None:
44
- raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument.symbol}'!")
45
-
46
- # - try to place order in OME
47
- report = ome.place_order(
48
- order_side.upper(), # type: ignore
49
- order_type.upper(), # type: ignore
50
- amount,
51
- price,
52
- client_id,
53
- time_in_force,
54
- **options,
44
+ # - place order at exchange and send exec report to data channel
45
+ self._send_execution_report(
46
+ report := self._exchange.place_order(
47
+ instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
48
+ )
55
49
  )
56
-
57
- self._send_exec_report(instrument, report)
58
50
  return report.order
59
51
 
60
52
  def send_order_async(
@@ -71,22 +63,8 @@ class SimulatedBroker(IBroker):
71
63
  self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
72
64
 
73
65
  def cancel_order(self, order_id: str) -> Order | None:
74
- instrument = self._account.order_to_instrument.get(order_id)
75
- if instrument is None:
76
- raise ValueError(f"ExchangeService:cancel_order :: can't find order with id = '{order_id}'!")
77
-
78
- ome = self._account.ome.get(instrument)
79
- if ome is None:
80
- raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument}'!")
81
-
82
- # - cancel order in OME and remove from the map to free memory
83
- order_update = ome.cancel_order(order_id)
84
- if order_update is None:
85
- return None
86
-
87
- self._send_exec_report(instrument, order_update)
88
-
89
- return order_update.order
66
+ self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
67
+ return order_update.order if order_update is not None else None
90
68
 
91
69
  def cancel_orders(self, instrument: Instrument) -> None:
92
70
  raise NotImplementedError("Not implemented yet")
@@ -94,10 +72,13 @@ class SimulatedBroker(IBroker):
94
72
  def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
95
73
  raise NotImplementedError("Not implemented yet")
96
74
 
97
- def _send_exec_report(self, instrument: Instrument, report: OmeReport):
98
- self.channel.send((instrument, "order", report.order, False))
75
+ def _send_execution_report(self, report: SimulatedExecutionReport | None):
76
+ if report is None:
77
+ return
78
+
79
+ self.channel.send((report.instrument, "order", report.order, False))
99
80
  if report.exec is not None:
100
- self.channel.send((instrument, "deals", [report.exec], False))
81
+ self.channel.send((report.instrument, "deals", [report.exec], False))
101
82
 
102
83
  def exchange(self) -> str:
103
- return self._exchange_id.upper()
84
+ return self._exchange.exchange_id
@@ -1,5 +1,5 @@
1
1
  from collections import defaultdict
2
- from typing import Any, Dict, Optional
2
+ from typing import Any
3
3
 
4
4
  import numpy as np
5
5
  import pandas as pd
@@ -30,9 +30,8 @@ class SimulatedDataProvider(IDataProvider):
30
30
 
31
31
  _scheduler: BasicScheduler
32
32
  _account: SimulatedAccountProcessor
33
- _last_quotes: Dict[Instrument, Optional[Quote]]
33
+ _last_quotes: dict[Instrument, Quote | None]
34
34
  _readers: dict[str, DataReader]
35
- _scheduler: BasicScheduler
36
35
  _pregenerated_signals: dict[Instrument, pd.Series | pd.DataFrame]
37
36
  _to_process: dict[Instrument, list]
38
37
  _data_source: IterableSimulationData
@@ -143,7 +142,7 @@ class SimulatedDataProvider(IDataProvider):
143
142
  if h_data:
144
143
  # _s_type = DataType.from_str(subscription_type)[0]
145
144
  last_update = h_data[-1]
146
- if last_quote := self._account.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
145
+ if last_quote := self._account._exchange.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
147
146
  # - send historical data to the channel
148
147
  self.channel.send((i, subscription_type, h_data, True))
149
148
 
@@ -151,7 +150,7 @@ class SimulatedDataProvider(IDataProvider):
151
150
  self._last_quotes[i] = last_quote
152
151
 
153
152
  # - also need to pass this quote to OME !
154
- self._account._process_new_data(i, last_quote)
153
+ self._account.process_market_data(last_quote.time, i, last_quote) # type: ignore
155
154
 
156
155
  logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
157
156
 
@@ -266,7 +265,7 @@ class SimulatedDataProvider(IDataProvider):
266
265
  cc.send((instrument, "event", {"order": sigs[0][1]}, False))
267
266
  sigs.pop(0)
268
267
 
269
- if q := self._account.emulate_quote_from_data(instrument, t, data):
268
+ if q := self._account._exchange.emulate_quote_from_data(instrument, t, data):
270
269
  self._last_quotes[instrument] = q
271
270
 
272
271
  self.time_provider.set_time(t)
@@ -284,7 +283,7 @@ class SimulatedDataProvider(IDataProvider):
284
283
  self.time_provider.set_time(_next_exp_time)
285
284
  self._scheduler.check_and_run_tasks()
286
285
 
287
- if q := self._account.emulate_quote_from_data(instrument, t, data):
286
+ if q := self._account._exchange.emulate_quote_from_data(instrument, t, data):
288
287
  self._last_quotes[instrument] = q
289
288
 
290
289
  self.time_provider.set_time(t)
@@ -27,7 +27,8 @@ from qubx.core.series import OrderBook, Quote, Trade, TradeArray
27
27
 
28
28
 
29
29
  @dataclass
30
- class OmeReport:
30
+ class SimulatedExecutionReport:
31
+ instrument: Instrument
31
32
  timestamp: dt_64
32
33
  order: Order
33
34
  exec: Deal | None
@@ -91,7 +92,7 @@ class OrdersManagementEngine:
91
92
  def get_open_orders(self) -> list[Order]:
92
93
  return list(self.active_orders.values()) + list(self.stop_orders.values())
93
94
 
94
- def process_market_data(self, mdata: Quote | OrderBook | Trade | TradeArray) -> list[OmeReport]:
95
+ def process_market_data(self, mdata: Quote | OrderBook | Trade | TradeArray) -> list[SimulatedExecutionReport]:
95
96
  """
96
97
  Processes the new market data (quote, trade or trades array) and simulates the execution of pending orders.
97
98
  """
@@ -173,7 +174,7 @@ class OrdersManagementEngine:
173
174
  client_id: str | None = None,
174
175
  time_in_force: str = "gtc",
175
176
  **options,
176
- ) -> OmeReport:
177
+ ) -> SimulatedExecutionReport:
177
178
  if self.bbo is None:
178
179
  raise ExchangeError(f"Simulator is not ready for order management - no quote for {self.instrument.symbol}")
179
180
 
@@ -200,7 +201,7 @@ class OrdersManagementEngine:
200
201
  def _dbg(self, message, **kwargs) -> None:
201
202
  logger.debug(f" [<y>OME</y>(<g>{self.instrument}</g>)] :: {message}", **kwargs)
202
203
 
203
- def _process_order(self, timestamp: dt_64, order: Order) -> OmeReport:
204
+ def _process_order(self, timestamp: dt_64, order: Order) -> SimulatedExecutionReport:
204
205
  if order.status in ["CLOSED", "CANCELED"]:
205
206
  raise InvalidOrder(f"Order {order.id} is already closed or canceled.")
206
207
 
@@ -273,12 +274,15 @@ class OrdersManagementEngine:
273
274
  self.active_orders[order.id] = order
274
275
 
275
276
  self._dbg(f"registered {order.id} {order.type} {order.side} {order.quantity} {order.price}")
276
- return OmeReport(timestamp, order, None)
277
+ return SimulatedExecutionReport(self.instrument, timestamp, order, None)
277
278
 
278
- def _execute_order(self, timestamp: dt_64, exec_price: float, order: Order, taker: bool) -> OmeReport:
279
+ def _execute_order(
280
+ self, timestamp: dt_64, exec_price: float, order: Order, taker: bool
281
+ ) -> SimulatedExecutionReport:
279
282
  order.status = "CLOSED"
280
283
  self._dbg(f"<red>{order.id}</red> {order.type} {order.side} {order.quantity} executed at {exec_price}")
281
- return OmeReport(
284
+ return SimulatedExecutionReport(
285
+ self.instrument,
282
286
  timestamp,
283
287
  order,
284
288
  Deal(
@@ -322,7 +326,7 @@ class OrdersManagementEngine:
322
326
  f"Stop price would trigger immediately: STOP_MARKET {order_side} {amount} of {self.instrument.symbol} at {price} | market: {c_ask} / {c_bid}"
323
327
  )
324
328
 
325
- def cancel_order(self, order_id: str) -> OmeReport | None:
329
+ def cancel_order(self, order_id: str) -> SimulatedExecutionReport | None:
326
330
  # - check limit orders
327
331
  if order_id in self.active_orders:
328
332
  order = self.active_orders.pop(order_id)
@@ -346,7 +350,7 @@ class OrdersManagementEngine:
346
350
 
347
351
  order.status = "CANCELED"
348
352
  self._dbg(f"{order.id} {order.type} {order.side} {order.quantity} canceled")
349
- return OmeReport(self.time_service.time(), order, None)
353
+ return SimulatedExecutionReport(self.instrument, self.time_service.time(), order, None)
350
354
 
351
355
  def __str__(self) -> str:
352
356
  _a, _b = True, True
@@ -17,6 +17,7 @@ from qubx.pandaz.utils import _frame_to_str
17
17
  from .account import SimulatedAccountProcessor
18
18
  from .broker import SimulatedBroker
19
19
  from .data import SimulatedDataProvider
20
+ from .simulated_exchange import get_simulated_exchange
20
21
  from .utils import (
21
22
  SetupTypes,
22
23
  SignalsProxy,
@@ -176,17 +177,25 @@ class SimulationRunner:
176
177
  f"[<y>simulator</y>] :: Preparing simulated trading on <g>{self.setup.exchange.upper()}</g> for {self.setup.capital} {self.setup.base_currency}..."
177
178
  )
178
179
 
180
+ # - create simulated exchange:
181
+ # - we can use different emulations of real exchanges features in future here: for Binance, Bybit, InteractiveBrokers, etc.
182
+ # - for now we use simple basic simulated exchange implementation
183
+ simulated_exchange = get_simulated_exchange(
184
+ self.setup.exchange, simulated_clock, tcc, self.setup.accurate_stop_orders_execution
185
+ )
186
+
179
187
  account = SimulatedAccountProcessor(
180
188
  account_id=self.account_id,
189
+ exchange=simulated_exchange,
181
190
  channel=channel,
182
191
  base_currency=self.setup.base_currency,
183
192
  initial_capital=self.setup.capital,
184
- time_provider=simulated_clock,
185
- tcc=tcc,
186
- accurate_stop_orders_execution=self.setup.accurate_stop_orders_execution,
187
193
  )
188
194
  scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
189
- broker = SimulatedBroker(channel, account, self.setup.exchange)
195
+
196
+ # - broker is order's interface to the exchange
197
+ broker = SimulatedBroker(channel, account, simulated_exchange)
198
+
190
199
  data_provider = SimulatedDataProvider(
191
200
  exchange_id=self.setup.exchange,
192
201
  channel=channel,
@@ -196,8 +205,10 @@ class SimulationRunner:
196
205
  readers=self.data_config.data_providers,
197
206
  open_close_time_indent_secs=self.data_config.adjusted_open_close_time_indent_secs,
198
207
  )
208
+
199
209
  # - get aux data provider
200
210
  _aux_data = self.data_config.get_timeguarded_aux_reader(simulated_clock)
211
+
201
212
  # - it will store simulation results into memory
202
213
  logs_writer = InMemoryLogsWriter(self.account_id, self.setup.name, "0")
203
214
 
@@ -0,0 +1,233 @@
1
+ from collections.abc import Generator
2
+
3
+ from qubx import logger
4
+ from qubx.backtester.ome import OrdersManagementEngine, SimulatedExecutionReport
5
+ from qubx.core.basics import (
6
+ ZERO_COSTS,
7
+ Instrument,
8
+ ITimeProvider,
9
+ Order,
10
+ Timestamped,
11
+ TransactionCostsCalculator,
12
+ dt_64,
13
+ )
14
+ from qubx.core.series import Bar, OrderBook, Quote, Trade, TradeArray
15
+
16
+
17
+ class ISimulatedExchange:
18
+ """
19
+ Generic interface for simulated exchange.
20
+ """
21
+
22
+ exchange_id: str
23
+ _half_tick_size: dict[Instrument, float]
24
+
25
+ def __init__(self, exchange_id: str):
26
+ self.exchange_id = exchange_id.upper()
27
+ self._half_tick_size = {}
28
+
29
+ def get_time_provider(self) -> ITimeProvider: ...
30
+
31
+ def get_transaction_costs_calculator(self) -> TransactionCostsCalculator: ...
32
+
33
+ def place_order(
34
+ self,
35
+ instrument: Instrument,
36
+ order_side: str,
37
+ order_type: str,
38
+ amount: float,
39
+ price: float | None = None,
40
+ client_id: str | None = None,
41
+ time_in_force: str = "gtc",
42
+ **options,
43
+ ) -> SimulatedExecutionReport: ...
44
+
45
+ def cancel_order(self, order_id: str) -> SimulatedExecutionReport | None: ...
46
+
47
+ def get_open_orders(self, instrument: Instrument | None = None) -> dict[str, Order]: ...
48
+
49
+ def process_market_data(
50
+ self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray
51
+ ) -> Generator[SimulatedExecutionReport]: ...
52
+
53
+ def emulate_quote_from_data(
54
+ self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
55
+ ) -> Quote | None:
56
+ """
57
+ Emulate quote from data.
58
+
59
+ TODO: we need to get rid of this method in the future
60
+ """
61
+ if instrument not in self._half_tick_size:
62
+ self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
63
+
64
+ if isinstance(data, Quote):
65
+ return data
66
+
67
+ elif isinstance(data, Trade):
68
+ _ts2 = self._half_tick_size[instrument]
69
+ if data.side == 1: # type: ignore
70
+ return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
71
+ else:
72
+ return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
73
+
74
+ elif isinstance(data, Bar):
75
+ _ts2 = self._half_tick_size[instrument]
76
+ return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
77
+
78
+ elif isinstance(data, OrderBook):
79
+ return data.to_quote()
80
+
81
+ elif isinstance(data, float):
82
+ _ts2 = self._half_tick_size[instrument]
83
+ return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
84
+
85
+ else:
86
+ return None
87
+
88
+
89
+ class BasicSimulatedExchange(ISimulatedExchange):
90
+ """
91
+ Basic implementation of generic crypto exchange.
92
+ """
93
+
94
+ _ome: dict[Instrument, OrdersManagementEngine]
95
+ _order_to_instrument: dict[str, Instrument]
96
+ _fill_stop_order_at_price: bool
97
+ _time_provider: ITimeProvider
98
+ _tcc: TransactionCostsCalculator
99
+
100
+ def __init__(
101
+ self,
102
+ exchange_id: str,
103
+ time_provider: ITimeProvider,
104
+ tcc: TransactionCostsCalculator = ZERO_COSTS,
105
+ accurate_stop_orders_execution: bool = False,
106
+ ):
107
+ super().__init__(exchange_id)
108
+ self._ome = {}
109
+ self._order_to_instrument = {}
110
+ self._half_tick_size = {}
111
+ self._fill_stop_order_at_price = accurate_stop_orders_execution
112
+ self._time_provider = time_provider
113
+ self._tcc = tcc
114
+ if self._fill_stop_order_at_price:
115
+ logger.info(
116
+ f"[<y>{self.__class__.__name__}</y>] :: emulation of stop orders executions at exact price is ON"
117
+ )
118
+
119
+ def get_time_provider(self) -> ITimeProvider:
120
+ return self._time_provider
121
+
122
+ def get_transaction_costs_calculator(self) -> TransactionCostsCalculator:
123
+ return self._tcc
124
+
125
+ def place_order(
126
+ self,
127
+ instrument: Instrument,
128
+ order_side: str,
129
+ order_type: str,
130
+ amount: float,
131
+ price: float | None = None,
132
+ client_id: str | None = None,
133
+ time_in_force: str = "gtc",
134
+ **options,
135
+ ) -> SimulatedExecutionReport:
136
+ # - try to place order in OME
137
+ return self._get_ome(instrument).place_order(
138
+ order_side.upper(), # type: ignore
139
+ order_type.upper(), # type: ignore
140
+ amount,
141
+ price,
142
+ client_id,
143
+ time_in_force,
144
+ **options,
145
+ )
146
+
147
+ def cancel_order(self, order_id: str) -> SimulatedExecutionReport | None:
148
+ # - first check in active orders
149
+ instrument = self._order_to_instrument.get(order_id)
150
+
151
+ if instrument is None:
152
+ # - if not found in active orders, check in each OME
153
+ for o in self._ome.values():
154
+ for order in o.get_open_orders():
155
+ if order.id == order_id:
156
+ return self._process_ome_response(o.cancel_order(order_id))
157
+
158
+ logger.error(
159
+ f"[<y>{self.__class__.__name__}</y>] :: cancel_order :: can't find order with id = 'ValueError{order_id}'!"
160
+ )
161
+ return None
162
+
163
+ ome = self._ome.get(instrument)
164
+ if ome is None:
165
+ raise ValueError(
166
+ f"{self.__class__.__name__}</y>] :: cancel_order :: No OME created for '{instrument}' - fatal error!"
167
+ )
168
+
169
+ # - cancel order in OME and remove from the map to free memory
170
+ return self._process_ome_response(ome.cancel_order(order_id))
171
+
172
+ def _process_ome_response(self, report: SimulatedExecutionReport | None) -> SimulatedExecutionReport | None:
173
+ if report is not None:
174
+ _order = report.order
175
+ _new = _order.status == "NEW"
176
+ _open = _order.status == "OPEN"
177
+ _cancel = _order.status == "CANCELED"
178
+ _closed = _order.status == "CLOSED"
179
+
180
+ if _new or _open:
181
+ self._order_to_instrument[_order.id] = _order.instrument
182
+
183
+ if (_cancel or _closed) and _order.id in self._order_to_instrument:
184
+ self._order_to_instrument.pop(_order.id)
185
+
186
+ return report
187
+
188
+ def get_open_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
189
+ if instrument is not None:
190
+ ome = self._get_ome(instrument)
191
+ return {o.id: o for o in ome.get_open_orders()}
192
+
193
+ return {o.id: o for ome in self._ome.values() for o in ome.get_open_orders()}
194
+
195
+ def _get_ome(self, instrument: Instrument) -> OrdersManagementEngine:
196
+ if (ome := self._ome.get(instrument)) is None:
197
+ self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
198
+ # - create order management engine for instrument
199
+ self._ome[instrument] = (
200
+ ome := OrdersManagementEngine(
201
+ instrument=instrument,
202
+ time_provider=self._time_provider,
203
+ tcc=self._tcc, # type: ignore
204
+ fill_stop_order_at_price=self._fill_stop_order_at_price,
205
+ )
206
+ )
207
+ return ome
208
+
209
+ def process_market_data(
210
+ self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray
211
+ ) -> Generator[SimulatedExecutionReport]:
212
+ ome = self._get_ome(instrument)
213
+
214
+ for r in ome.process_market_data(data):
215
+ if r.exec is not None:
216
+ if r.order.id in self._order_to_instrument:
217
+ self._order_to_instrument.pop(r.order.id)
218
+ yield r
219
+
220
+
221
+ def get_simulated_exchange(
222
+ exchange_name: str,
223
+ time_provider: ITimeProvider,
224
+ tcc: TransactionCostsCalculator,
225
+ accurate_stop_orders_execution=False,
226
+ ) -> ISimulatedExchange:
227
+ """
228
+ Factory function to create different types of simulated exchanges based on it's name etc
229
+ Now it supports only basic exchange that fits for most cases of crypto trading.
230
+ """
231
+ return BasicSimulatedExchange(
232
+ exchange_name, time_provider, tcc, accurate_stop_orders_execution=accurate_stop_orders_execution
233
+ )
@@ -550,6 +550,7 @@ class StrategyContext(IStrategyContext):
550
550
  if self._lifecycle_notifier:
551
551
  self._lifecycle_notifier.notify_error(self._strategy_name, e)
552
552
  # Don't stop the channel here, let it continue processing
553
+
553
554
  logger.info("[StrategyContext] :: Market data processing stopped")
554
555
 
555
556
  def __instantiate_strategy(self, strategy: IStrategy, config: dict[str, Any] | None) -> IStrategy: