Qubx 0.6.21__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.24__cp312-cp312-manylinux_2_39_x86_64.whl

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 (44) hide show
  1. qubx/backtester/account.py +16 -94
  2. qubx/backtester/broker.py +19 -38
  3. qubx/backtester/data.py +8 -138
  4. qubx/backtester/ome.py +13 -9
  5. qubx/backtester/runner.py +256 -40
  6. qubx/backtester/simulated_exchange.py +233 -0
  7. qubx/backtester/simulator.py +13 -22
  8. qubx/backtester/utils.py +27 -39
  9. qubx/connectors/ccxt/account.py +4 -4
  10. qubx/connectors/ccxt/data.py +93 -18
  11. qubx/connectors/ccxt/exchanges/__init__.py +5 -1
  12. qubx/connectors/ccxt/exchanges/binance/exchange.py +1 -0
  13. qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +43 -0
  14. qubx/connectors/ccxt/exchanges/kraken/kraken.py +14 -0
  15. qubx/connectors/ccxt/utils.py +20 -6
  16. qubx/connectors/tardis/data.py +733 -0
  17. qubx/connectors/tardis/utils.py +249 -0
  18. qubx/core/account.py +206 -20
  19. qubx/core/basics.py +0 -9
  20. qubx/core/context.py +56 -53
  21. qubx/core/interfaces.py +67 -65
  22. qubx/core/lookups.py +129 -18
  23. qubx/core/metrics.py +14 -11
  24. qubx/core/mixins/market.py +24 -9
  25. qubx/core/mixins/subscription.py +58 -28
  26. qubx/core/mixins/trading.py +35 -31
  27. qubx/core/mixins/universe.py +0 -20
  28. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  29. qubx/core/series.pyx +1 -1
  30. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  31. qubx/data/helpers.py +1 -1
  32. qubx/data/tardis.py +0 -1
  33. qubx/restorers/state.py +2 -0
  34. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  35. qubx/ta/indicators.pxd +1 -1
  36. qubx/ta/indicators.pyx +5 -8
  37. qubx/utils/runner/accounts.py +0 -1
  38. qubx/utils/runner/configs.py +8 -0
  39. qubx/utils/runner/runner.py +43 -18
  40. {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/METADATA +1 -1
  41. {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/RECORD +44 -39
  42. {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/WHEEL +1 -1
  43. {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/LICENSE +0 -0
  44. {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/entry_points.txt +0 -0
@@ -1,54 +1,40 @@
1
- from qubx import logger
2
- from qubx.backtester.ome import OrdersManagementEngine
1
+ from qubx.backtester.simulated_exchange import ISimulatedExchange
3
2
  from qubx.core.account import BasicAccountProcessor
4
3
  from qubx.core.basics import (
5
- ZERO_COSTS,
6
4
  CtrlChannel,
7
5
  Instrument,
8
6
  Order,
9
7
  Position,
10
8
  Timestamped,
11
- TransactionCostsCalculator,
12
9
  dt_64,
13
10
  )
14
- from qubx.core.interfaces import ITimeProvider
15
- from qubx.core.series import Bar, OrderBook, Quote, Trade, TradeArray
11
+ from qubx.core.series import OrderBook, Quote, Trade, TradeArray
16
12
  from qubx.restorers import RestoredState
17
13
 
18
14
 
19
15
  class SimulatedAccountProcessor(BasicAccountProcessor):
20
- ome: dict[Instrument, OrdersManagementEngine]
21
- order_to_instrument: dict[str, Instrument]
22
-
23
16
  _channel: CtrlChannel
24
- _fill_stop_order_at_price: bool
25
- _half_tick_size: dict[Instrument, float]
17
+ _exchange: ISimulatedExchange
26
18
 
27
19
  def __init__(
28
20
  self,
29
21
  account_id: str,
22
+ exchange: ISimulatedExchange,
30
23
  channel: CtrlChannel,
31
24
  base_currency: str,
32
25
  initial_capital: float,
33
- time_provider: ITimeProvider,
34
- tcc: TransactionCostsCalculator = ZERO_COSTS,
35
- accurate_stop_orders_execution: bool = False,
36
26
  restored_state: RestoredState | None = None,
37
27
  ) -> None:
38
28
  super().__init__(
39
29
  account_id=account_id,
40
- time_provider=time_provider,
30
+ time_provider=exchange.get_time_provider(),
41
31
  base_currency=base_currency,
42
- tcc=tcc,
32
+ tcc=exchange.get_transaction_costs_calculator(),
43
33
  initial_capital=initial_capital,
44
34
  )
45
- self.ome = {}
46
- self.order_to_instrument = {}
35
+
36
+ self._exchange = exchange
47
37
  self._channel = channel
48
- self._half_tick_size = {}
49
- self._fill_stop_order_at_price = accurate_stop_orders_execution
50
- if self._fill_stop_order_at_price:
51
- logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
52
38
 
53
39
  if restored_state is not None:
54
40
  self._balances.update(restored_state.balances)
@@ -57,42 +43,25 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
57
43
  _pos.reset_by_position(position)
58
44
 
59
45
  def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
60
- if instrument is not None:
61
- ome = self.ome.get(instrument)
62
- if ome is None:
63
- raise ValueError(f"ExchangeService:get_orders :: No OME configured for '{instrument}'!")
64
-
65
- return {o.id: o for o in ome.get_open_orders()}
66
-
67
- return {o.id: o for ome in self.ome.values() for o in ome.get_open_orders()}
46
+ return self._exchange.get_open_orders(instrument)
68
47
 
69
48
  def get_position(self, instrument: Instrument) -> Position:
70
49
  if instrument in self.positions:
71
50
  return self.positions[instrument]
72
51
 
73
- # - initiolize OME for this instrument
74
- self.ome[instrument] = OrdersManagementEngine(
75
- instrument=instrument,
76
- time_provider=self.time_provider,
77
- tcc=self._tcc, # type: ignore
78
- fill_stop_order_at_price=self._fill_stop_order_at_price,
79
- )
80
-
81
- # - initiolize empty position
52
+ # - initialize empty position
82
53
  position = Position(instrument) # type: ignore
83
- self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
84
54
  self.attach_positions(position)
85
55
  return self.positions[instrument]
86
56
 
87
57
  def update_position_price(self, time: dt_64, instrument: Instrument, update: float | Timestamped) -> None:
58
+ self.get_position(instrument)
59
+
88
60
  super().update_position_price(time, instrument, update)
89
61
 
90
- # - first we need to update OME with new quote.
91
- # - if update is not a quote we need 'emulate' it.
92
- # - actually if SimulatedExchangeService is used in backtesting mode it will recieve only quotes
93
- # - case when we need that - SimulatedExchangeService is used for paper trading and data provider configured to listen to OHLC or TAS.
94
- # - probably we need to subscribe to quotes in real data provider in any case and then this emulation won't be needed.
95
- quote = update if isinstance(update, Quote) else self.emulate_quote_from_data(instrument, time, update)
62
+ quote = (
63
+ update if isinstance(update, Quote) else self._exchange.emulate_quote_from_data(instrument, time, update)
64
+ )
96
65
  if quote is None:
97
66
  return
98
67
 
@@ -106,56 +75,9 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
106
75
 
107
76
  super().process_market_data(time, instrument, update)
108
77
 
109
- def process_order(self, order: Order, update_locked_value: bool = True) -> None:
110
- _new = order.status == "NEW"
111
- _open = order.status == "OPEN"
112
- _cancel = order.status == "CANCELED"
113
- _closed = order.status == "CLOSED"
114
- if _new or _open:
115
- self.order_to_instrument[order.id] = order.instrument
116
- if (_cancel or _closed) and order.id in self.order_to_instrument:
117
- self.order_to_instrument.pop(order.id)
118
- return super().process_order(order, update_locked_value)
119
-
120
- def emulate_quote_from_data(
121
- self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
122
- ) -> Quote | None:
123
- if instrument not in self._half_tick_size:
124
- _ = self.get_position(instrument)
125
-
126
- if isinstance(data, Quote):
127
- return data
128
-
129
- elif isinstance(data, Trade):
130
- _ts2 = self._half_tick_size[instrument]
131
- if data.side == 1: # type: ignore
132
- return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
133
- else:
134
- return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
135
-
136
- elif isinstance(data, Bar):
137
- _ts2 = self._half_tick_size[instrument]
138
- return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
139
-
140
- elif isinstance(data, OrderBook):
141
- return data.to_quote()
142
-
143
- elif isinstance(data, float):
144
- _ts2 = self._half_tick_size[instrument]
145
- return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
146
-
147
- else:
148
- return None
149
-
150
78
  def _process_new_data(self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray) -> None:
151
- ome = self.ome.get(instrument)
152
- if ome is None:
153
- logger.warning(f"ExchangeService:update :: No OME configured for '{instrument}' yet !")
154
- return
155
- for r in ome.process_market_data(data):
79
+ for r in self._exchange.process_market_data(instrument, data):
156
80
  if r.exec is not None:
157
- if r.order.id in self.order_to_instrument:
158
- self.order_to_instrument.pop(r.order.id)
159
81
  # - process methods will be called from stg context
160
82
  self._channel.send((instrument, "order", r.order, False))
161
83
  self._channel.send((instrument, "deals", [r.exec], False))
qubx/backtester/broker.py CHANGED
@@ -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
qubx/backtester/data.py CHANGED
@@ -1,9 +1,6 @@
1
1
  from collections import defaultdict
2
- from typing import Any, Dict, Optional
3
2
 
4
- import numpy as np
5
3
  import pandas as pd
6
- from tqdm.auto import tqdm
7
4
 
8
5
  from qubx import logger
9
6
  from qubx.backtester.simulated_data import IterableSimulationData
@@ -13,7 +10,6 @@ from qubx.core.basics import (
13
10
  Instrument,
14
11
  TimestampedDict,
15
12
  )
16
- from qubx.core.exceptions import SimulationError
17
13
  from qubx.core.helpers import BasicScheduler
18
14
  from qubx.core.interfaces import IDataProvider
19
15
  from qubx.core.series import Bar, Quote, time_as_nsec
@@ -30,11 +26,8 @@ class SimulatedDataProvider(IDataProvider):
30
26
 
31
27
  _scheduler: BasicScheduler
32
28
  _account: SimulatedAccountProcessor
33
- _last_quotes: Dict[Instrument, Optional[Quote]]
29
+ _last_quotes: dict[Instrument, Quote | None]
34
30
  _readers: dict[str, DataReader]
35
- _scheduler: BasicScheduler
36
- _pregenerated_signals: dict[Instrument, pd.Series | pd.DataFrame]
37
- _to_process: dict[Instrument, list]
38
31
  _data_source: IterableSimulationData
39
32
  _open_close_time_indent_ns: int
40
33
 
@@ -46,6 +39,7 @@ class SimulatedDataProvider(IDataProvider):
46
39
  time_provider: SimulatedTimeProvider,
47
40
  account: SimulatedAccountProcessor,
48
41
  readers: dict[str, DataReader],
42
+ data_source: IterableSimulationData,
49
43
  open_close_time_indent_secs=1,
50
44
  ):
51
45
  self.channel = channel
@@ -55,79 +49,14 @@ class SimulatedDataProvider(IDataProvider):
55
49
  self._account = account
56
50
  self._readers = readers
57
51
 
58
- # - create exchange's instance
59
- self._last_quotes = defaultdict(lambda: None)
60
-
61
- # - pregenerated signals storage
62
- self._pregenerated_signals = dict()
63
- self._to_process = {}
64
-
65
52
  # - simulation data source
66
- self._data_source = IterableSimulationData(
67
- self._readers, open_close_time_indent_secs=open_close_time_indent_secs
68
- )
53
+ self._data_source = data_source
69
54
  self._open_close_time_indent_ns = open_close_time_indent_secs * 1_000_000_000 # convert seconds to nanoseconds
70
55
 
71
- logger.info(f"{self.__class__.__name__}.{exchange_id} is initialized")
72
-
73
- def run(
74
- self,
75
- start: str | pd.Timestamp,
76
- end: str | pd.Timestamp,
77
- silent: bool = False,
78
- ) -> None:
79
- logger.info(f"{self.__class__.__name__} ::: Simulation started at {start} :::")
80
-
81
- if self._pregenerated_signals:
82
- self._prepare_generated_signals(start, end)
83
- _run = self._process_generated_signals
84
- else:
85
- _run = self._process_strategy
86
-
87
- start, end = pd.Timestamp(start), pd.Timestamp(end)
88
- total_duration = end - start
89
- update_delta = total_duration / 100
90
- prev_dt = pd.Timestamp(start)
91
-
92
- # - date iteration
93
- qiter = self._data_source.create_iterable(start, end)
94
- if silent:
95
- for instrument, data_type, event, is_hist in qiter:
96
- if not _run(instrument, data_type, event, is_hist):
97
- break
98
- else:
99
- _p = 0
100
- with tqdm(total=100, desc="Simulating", unit="%", leave=False) as pbar:
101
- for instrument, data_type, event, is_hist in qiter:
102
- if not _run(instrument, data_type, event, is_hist):
103
- break
104
- dt = pd.Timestamp(event.time)
105
- # update only if date has changed
106
- if dt - prev_dt > update_delta:
107
- _p += 1
108
- pbar.n = _p
109
- pbar.refresh()
110
- prev_dt = dt
111
- pbar.n = 100
112
- pbar.refresh()
113
-
114
- logger.info(f"{self.__class__.__name__} ::: Simulation finished at {end} :::")
115
-
116
- def set_generated_signals(self, signals: pd.Series | pd.DataFrame):
117
- logger.debug(
118
- f"[<y>{self.__class__.__name__}</y>] :: Using pre-generated signals:\n {str(signals.count()).strip('ndtype: int64')}"
119
- )
120
- # - sanity check
121
- signals.index = pd.DatetimeIndex(signals.index)
122
-
123
- if isinstance(signals, pd.Series):
124
- self._pregenerated_signals[str(signals.name)] = signals # type: ignore
56
+ # - create exchange's instance
57
+ self._last_quotes = defaultdict(lambda: None)
125
58
 
126
- elif isinstance(signals, pd.DataFrame):
127
- for col in signals.columns:
128
- self._pregenerated_signals[col] = signals[col] # type: ignore
129
- else:
130
- raise ValueError("Invalid signals or strategy configuration")
59
+ logger.info(f"{self.__class__.__name__}.{exchange_id} is initialized")
131
60
 
132
61
  @property
133
62
  def is_simulation(self) -> bool:
@@ -143,7 +72,7 @@ class SimulatedDataProvider(IDataProvider):
143
72
  if h_data:
144
73
  # _s_type = DataType.from_str(subscription_type)[0]
145
74
  last_update = h_data[-1]
146
- if last_quote := self._account.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
75
+ if last_quote := self._account._exchange.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
147
76
  # - send historical data to the channel
148
77
  self.channel.send((i, subscription_type, h_data, True))
149
78
 
@@ -151,7 +80,7 @@ class SimulatedDataProvider(IDataProvider):
151
80
  self._last_quotes[i] = last_quote
152
81
 
153
82
  # - also need to pass this quote to OME !
154
- self._account._process_new_data(i, last_quote)
83
+ self._account.process_market_data(last_quote.time, i, last_quote) # type: ignore
155
84
 
156
85
  logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
157
86
 
@@ -201,26 +130,6 @@ class SimulatedDataProvider(IDataProvider):
201
130
  def close(self):
202
131
  pass
203
132
 
204
- def _prepare_generated_signals(self, start: str | pd.Timestamp, end: str | pd.Timestamp):
205
- for s, v in self._pregenerated_signals.items():
206
- _s_inst = None
207
-
208
- for i in self.get_subscribed_instruments():
209
- # - we can process series with variable id's if we can find some similar instrument
210
- if s == i.symbol or s == str(i) or s == f"{i.exchange}:{i.symbol}" or str(s) == str(i):
211
- _start, _end = pd.Timestamp(start), pd.Timestamp(end)
212
- _start_idx, _end_idx = v.index.get_indexer([_start, _end], method="ffill")
213
- sel = v.iloc[max(_start_idx, 0) : _end_idx + 1]
214
-
215
- # TODO: check if data has exec_price - it means we have deals
216
- self._to_process[i] = list(zip(sel.index, sel.values))
217
- _s_inst = i
218
- break
219
-
220
- if _s_inst is None:
221
- logger.error(f"Can't find instrument for pregenerated signals with id '{s}'")
222
- raise SimulationError(f"Can't find instrument for pregenerated signals with id '{s}'")
223
-
224
133
  def _convert_records_to_bars(
225
134
  self, records: list[TimestampedDict], cut_time_ns: int, timeframe_ns: int
226
135
  ) -> list[Bar]:
@@ -253,44 +162,5 @@ class SimulatedDataProvider(IDataProvider):
253
162
 
254
163
  return bars
255
164
 
256
- def _process_generated_signals(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
257
- cc = self.channel
258
- t = np.datetime64(data.time, "ns")
259
-
260
- if not is_hist:
261
- # - signals for this instrument
262
- sigs = self._to_process[instrument]
263
-
264
- while sigs and t >= (_signal_time := sigs[0][0].as_unit("ns").asm8):
265
- self.time_provider.set_time(_signal_time)
266
- cc.send((instrument, "event", {"order": sigs[0][1]}, False))
267
- sigs.pop(0)
268
-
269
- if q := self._account.emulate_quote_from_data(instrument, t, data):
270
- self._last_quotes[instrument] = q
271
-
272
- self.time_provider.set_time(t)
273
- cc.send((instrument, data_type, data, is_hist))
274
-
275
- return cc.control.is_set()
276
-
277
- def _process_strategy(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
278
- cc = self.channel
279
- t = np.datetime64(data.time, "ns")
280
-
281
- if not is_hist:
282
- if t >= (_next_exp_time := self._scheduler.next_expected_event_time()):
283
- # - we use exact event's time
284
- self.time_provider.set_time(_next_exp_time)
285
- self._scheduler.check_and_run_tasks()
286
-
287
- if q := self._account.emulate_quote_from_data(instrument, t, data):
288
- self._last_quotes[instrument] = q
289
-
290
- self.time_provider.set_time(t)
291
- cc.send((instrument, data_type, data, is_hist))
292
-
293
- return cc.control.is_set()
294
-
295
165
  def exchange(self) -> str:
296
166
  return self._exchange_id.upper()
qubx/backtester/ome.py CHANGED
@@ -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