Qubx 0.6.1__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.3__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 (69) hide show
  1. qubx/backtester/account.py +25 -10
  2. qubx/backtester/data.py +1 -1
  3. qubx/backtester/ome.py +94 -38
  4. qubx/backtester/runner.py +248 -0
  5. qubx/backtester/simulated_data.py +15 -2
  6. qubx/backtester/simulator.py +26 -166
  7. qubx/backtester/utils.py +48 -3
  8. qubx/cli/commands.py +8 -4
  9. qubx/cli/release.py +1 -1
  10. qubx/connectors/ccxt/account.py +6 -4
  11. qubx/connectors/ccxt/data.py +8 -5
  12. qubx/core/account.py +6 -2
  13. qubx/core/basics.py +27 -5
  14. qubx/core/context.py +18 -2
  15. qubx/core/helpers.py +1 -1
  16. qubx/core/initializer.py +84 -0
  17. qubx/core/interfaces.py +217 -8
  18. qubx/core/loggers.py +47 -11
  19. qubx/core/metrics.py +2 -2
  20. qubx/core/mixins/processing.py +11 -7
  21. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  22. qubx/core/series.pxd +22 -1
  23. qubx/core/series.pyi +21 -5
  24. qubx/core/series.pyx +149 -3
  25. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  26. qubx/core/utils.pyi +3 -1
  27. qubx/data/composite.py +149 -0
  28. qubx/data/hft.py +779 -0
  29. qubx/data/readers.py +171 -7
  30. qubx/exporters/__init__.py +2 -1
  31. qubx/exporters/composite.py +83 -0
  32. qubx/exporters/formatters/__init__.py +2 -1
  33. qubx/exporters/formatters/base.py +0 -1
  34. qubx/exporters/formatters/incremental.py +89 -2
  35. qubx/exporters/redis_streams.py +5 -5
  36. qubx/features/__init__.py +14 -0
  37. qubx/features/core.py +250 -0
  38. qubx/features/orderbook.py +41 -0
  39. qubx/features/price.py +20 -0
  40. qubx/features/trades.py +105 -0
  41. qubx/features/utils.py +10 -0
  42. qubx/math/stats.py +6 -4
  43. qubx/pandaz/__init__.py +8 -0
  44. qubx/restarts/__init__.py +0 -0
  45. qubx/restarts/state_resolvers.py +66 -0
  46. qubx/restarts/time_finders.py +34 -0
  47. qubx/restorers/__init__.py +36 -0
  48. qubx/restorers/balance.py +120 -0
  49. qubx/restorers/factory.py +201 -0
  50. qubx/restorers/interfaces.py +80 -0
  51. qubx/restorers/position.py +137 -0
  52. qubx/restorers/signal.py +159 -0
  53. qubx/restorers/state.py +110 -0
  54. qubx/restorers/utils.py +42 -0
  55. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  56. qubx/ta/indicators.pyi +2 -2
  57. qubx/trackers/__init__.py +30 -3
  58. qubx/trackers/{abvanced.py → advanced.py} +69 -0
  59. qubx/trackers/rebalancers.py +5 -7
  60. qubx/trackers/sizers.py +5 -7
  61. qubx/utils/marketdata/binance.py +132 -93
  62. qubx/utils/runner/_jupyter_runner.pyt +110 -18
  63. qubx/utils/runner/configs.py +20 -3
  64. qubx/utils/runner/runner.py +249 -34
  65. {qubx-0.6.1.dist-info → qubx-0.6.3.dist-info}/METADATA +2 -3
  66. qubx-0.6.3.dist-info/RECORD +133 -0
  67. {qubx-0.6.1.dist-info → qubx-0.6.3.dist-info}/WHEEL +1 -1
  68. qubx-0.6.1.dist-info/RECORD +0 -111
  69. {qubx-0.6.1.dist-info → qubx-0.6.3.dist-info}/entry_points.txt +0 -0
@@ -12,7 +12,8 @@ from qubx.core.basics import (
12
12
  dt_64,
13
13
  )
14
14
  from qubx.core.interfaces import ITimeProvider
15
- from qubx.core.series import Bar, OrderBook, Quote, Trade
15
+ from qubx.core.series import Bar, OrderBook, Quote, Trade, TradeArray
16
+ from qubx.restorers import RestoredState
16
17
 
17
18
 
18
19
  class SimulatedAccountProcessor(BasicAccountProcessor):
@@ -32,6 +33,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
32
33
  time_provider: ITimeProvider,
33
34
  tcc: TransactionCostsCalculator = ZERO_COSTS,
34
35
  accurate_stop_orders_execution: bool = False,
36
+ restored_state: RestoredState | None = None,
35
37
  ) -> None:
36
38
  super().__init__(
37
39
  account_id=account_id,
@@ -48,6 +50,12 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
48
50
  if self._fill_stop_order_at_price:
49
51
  logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
50
52
 
53
+ if restored_state is not None:
54
+ self._balances.update(restored_state.balances)
55
+ for instrument, position in restored_state.positions.items():
56
+ _pos = self.get_position(instrument)
57
+ _pos.reset_by_position(position)
58
+
51
59
  def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
52
60
  if instrument is not None:
53
61
  ome = self.ome.get(instrument)
@@ -76,20 +84,27 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
76
84
  self.attach_positions(position)
77
85
  return self.positions[instrument]
78
86
 
79
- def update_position_price(self, time: dt_64, instrument: Instrument, price: float) -> None:
80
- super().update_position_price(time, instrument, price)
87
+ def update_position_price(self, time: dt_64, instrument: Instrument, update: float | Timestamped) -> None:
88
+ super().update_position_price(time, instrument, update)
81
89
 
82
90
  # - first we need to update OME with new quote.
83
91
  # - if update is not a quote we need 'emulate' it.
84
92
  # - actually if SimulatedExchangeService is used in backtesting mode it will recieve only quotes
85
93
  # - case when we need that - SimulatedExchangeService is used for paper trading and data provider configured to listen to OHLC or TAS.
86
94
  # - probably we need to subscribe to quotes in real data provider in any case and then this emulation won't be needed.
87
- quote = price if isinstance(price, Quote) else self.emulate_quote_from_data(instrument, time, price)
95
+ quote = update if isinstance(update, Quote) else self.emulate_quote_from_data(instrument, time, update)
88
96
  if quote is None:
89
97
  return
90
98
 
91
- # - process new quote
92
- self._process_new_quote(instrument, quote)
99
+ # - process new data
100
+ self._process_new_data(instrument, quote)
101
+
102
+ def process_market_data(self, time: dt_64, instrument: Instrument, update: Timestamped) -> None:
103
+ if isinstance(update, (TradeArray, Quote, Trade, OrderBook)):
104
+ # - process new data
105
+ self._process_new_data(instrument, update)
106
+
107
+ super().process_market_data(time, instrument, update)
93
108
 
94
109
  def process_order(self, order: Order, update_locked_value: bool = True) -> None:
95
110
  _new = order.status == "NEW"
@@ -113,7 +128,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
113
128
 
114
129
  elif isinstance(data, Trade):
115
130
  _ts2 = self._half_tick_size[instrument]
116
- if data.taker: # type: ignore
131
+ if data.side == 1: # type: ignore
117
132
  return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
118
133
  else:
119
134
  return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
@@ -132,12 +147,12 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
132
147
  else:
133
148
  return None
134
149
 
135
- def _process_new_quote(self, instrument: Instrument, data: Quote) -> None:
150
+ def _process_new_data(self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray) -> None:
136
151
  ome = self.ome.get(instrument)
137
152
  if ome is None:
138
- logger.warning("ExchangeService:update :: No OME configured for '{symbol}' yet !")
153
+ logger.warning(f"ExchangeService:update :: No OME configured for '{instrument}' yet !")
139
154
  return
140
- for r in ome.update_bbo(data):
155
+ for r in ome.process_market_data(data):
141
156
  if r.exec is not None:
142
157
  self.order_to_instrument.pop(r.order.id)
143
158
  # - process methods will be called from stg context
qubx/backtester/data.py CHANGED
@@ -151,7 +151,7 @@ class SimulatedDataProvider(IDataProvider):
151
151
  self._last_quotes[i] = last_quote
152
152
 
153
153
  # - also need to pass this quote to OME !
154
- self._account._process_new_quote(i, last_quote)
154
+ self._account._process_new_data(i, last_quote)
155
155
 
156
156
  logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
157
157
 
qubx/backtester/ome.py CHANGED
@@ -19,8 +19,9 @@ from qubx.core.basics import (
19
19
  from qubx.core.exceptions import (
20
20
  ExchangeError,
21
21
  InvalidOrder,
22
+ SimulationError,
22
23
  )
23
- from qubx.core.series import Quote
24
+ from qubx.core.series import OrderBook, Quote, Trade, TradeArray
24
25
 
25
26
 
26
27
  @dataclass
@@ -31,13 +32,18 @@ class OmeReport:
31
32
 
32
33
 
33
34
  class OrdersManagementEngine:
35
+ """
36
+ Orders Management Engine (OME) is a simple implementation of a management of orders for simulation of a limit order book.
37
+ """
38
+
34
39
  instrument: Instrument
35
40
  time_service: ITimeProvider
36
41
  active_orders: dict[str, Order]
37
42
  stop_orders: dict[str, Order]
38
43
  asks: SortedDict[float, list[str]]
39
44
  bids: SortedDict[float, list[str]]
40
- bbo: Quote | None # current best bid/ask order book (simplest impl)
45
+ bbo: Quote | None # - current best bid/ask order book
46
+ __prev_bbo: Quote | None # - previous best bid/ask order book
41
47
  __order_id: int
42
48
  __trade_id: int
43
49
  _fill_stops_at_price: bool
@@ -73,46 +79,80 @@ class OrdersManagementEngine:
73
79
  return "SIM-EXEC-" + self.instrument.symbol + "-" + str(self.__trade_id)
74
80
 
75
81
  def get_quote(self) -> Quote:
76
- return self.bbo
82
+ return self.bbo # type: ignore
77
83
 
78
84
  def get_open_orders(self) -> list[Order]:
79
85
  return list(self.active_orders.values()) + list(self.stop_orders.values())
80
86
 
81
- def update_bbo(self, quote: Quote) -> list[OmeReport]:
87
+ def process_market_data(self, mdata: Quote | OrderBook | Trade | TradeArray) -> list[OmeReport]:
88
+ """
89
+ Processes the new market data (quote, trade or trades array) and simulates the execution of pending orders.
90
+ """
82
91
  timestamp = self.time_service.time()
83
- rep = []
84
-
85
- if self.bbo is not None:
86
- if quote.bid >= self.bbo.ask:
87
- for level in self.asks.irange(0, quote.bid):
88
- for order_id in self.asks[level]:
89
- order = self.active_orders.pop(order_id)
90
- rep.append(self._execute_order(timestamp, order.price, order, False))
91
- self.asks.pop(level)
92
-
93
- if quote.ask <= self.bbo.bid:
94
- for level in self.bids.irange(np.inf, quote.ask):
95
- for order_id in self.bids[level]:
96
- order = self.active_orders.pop(order_id)
97
- rep.append(self._execute_order(timestamp, order.price, order, False))
98
- self.bids.pop(level)
99
-
100
- # - processing stop orders
101
- for soid in list(self.stop_orders.keys()):
102
- so = self.stop_orders[soid]
103
- _emulate_price_exec = self._fill_stops_at_price or so.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False)
104
-
105
- if so.side == "BUY" and quote.ask >= so.price:
106
- _exec_price = quote.ask if not _emulate_price_exec else so.price
107
- self.stop_orders.pop(soid)
108
- rep.append(self._execute_order(timestamp, _exec_price, so, True))
109
- elif so.side == "SELL" and quote.bid <= so.price:
110
- _exec_price = quote.bid if not _emulate_price_exec else so.price
111
- self.stop_orders.pop(soid)
112
- rep.append(self._execute_order(timestamp, _exec_price, so, True))
113
-
114
- self.bbo = quote
115
- return rep
92
+ _exec_report = []
93
+
94
+ # - new quote
95
+ if isinstance(mdata, Quote):
96
+ _b, _a = mdata.bid, mdata.ask
97
+ _bs, _as = _b, _a
98
+
99
+ # - update BBO by new quote
100
+ self.__prev_bbo = self.bbo
101
+ self.bbo = mdata
102
+
103
+ # - bunch of trades
104
+ elif isinstance(mdata, TradeArray):
105
+ _b = mdata.max_buy_price
106
+ _a = mdata.min_sell_price
107
+ _bs, _as = _a, _b
108
+
109
+ # - single trade
110
+ elif isinstance(mdata, Trade):
111
+ _b, _a = mdata.price, mdata.price
112
+ _bs, _as = _b, _a
113
+
114
+ # - order book
115
+ elif isinstance(mdata, OrderBook):
116
+ _b, _a = mdata.top_bid, mdata.top_ask
117
+ _bs, _as = _b, _a
118
+
119
+ else:
120
+ raise SimulationError(f"Invalid market data type: {type(mdata)} for update OME({self.instrument.symbol})")
121
+
122
+ # - when new quote bid is higher than the lowest ask order execute all affected orders
123
+ if self.asks and _b >= self.asks.keys()[0]:
124
+ _asks_to_execute = list(self.asks.irange(0, _b))
125
+ for level in _asks_to_execute:
126
+ for order_id in self.asks[level]:
127
+ order = self.active_orders.pop(order_id)
128
+ _exec_report.append(self._execute_order(timestamp, order.price, order, False))
129
+ self.asks.pop(level)
130
+
131
+ # - when new quote ask is lower than the highest bid order execute all affected orders
132
+ if self.bids and _a <= self.bids.keys()[0]:
133
+ _bids_to_execute = list(self.bids.irange(np.inf, _a))
134
+ for level in _bids_to_execute:
135
+ for order_id in self.bids[level]:
136
+ order = self.active_orders.pop(order_id)
137
+ _exec_report.append(self._execute_order(timestamp, order.price, order, False))
138
+ self.bids.pop(level)
139
+
140
+ # - processing stop orders
141
+ for soid in list(self.stop_orders.keys()):
142
+ so = self.stop_orders[soid]
143
+ _emulate_price_exec = self._fill_stops_at_price or so.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False)
144
+
145
+ if so.side == "BUY" and _as >= so.price:
146
+ _exec_price = _as if not _emulate_price_exec else so.price
147
+ self.stop_orders.pop(soid)
148
+ _exec_report.append(self._execute_order(timestamp, _exec_price, so, True))
149
+
150
+ elif so.side == "SELL" and _bs <= so.price:
151
+ _exec_price = _bs if not _emulate_price_exec else so.price
152
+ self.stop_orders.pop(soid)
153
+ _exec_report.append(self._execute_order(timestamp, _exec_price, so, True))
154
+
155
+ return _exec_report
116
156
 
117
157
  def place_order(
118
158
  self,
@@ -163,7 +203,23 @@ class OrdersManagementEngine:
163
203
  _need_update_book = False
164
204
 
165
205
  if order.type == "MARKET":
166
- exec_price = c_ask if buy_side else c_bid
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
+ )
167
223
 
168
224
  elif order.type == "LIMIT":
169
225
  _need_update_book = True
@@ -0,0 +1,248 @@
1
+ from typing import Any
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from qubx import logger
7
+ from qubx.core.basics import SW, DataType
8
+ from qubx.core.context import StrategyContext
9
+ from qubx.core.exceptions import SimulationConfigError, SimulationError
10
+ from qubx.core.helpers import extract_parameters_from_object, full_qualified_class_name
11
+ from qubx.core.interfaces import IStrategy, IStrategyContext
12
+ from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
13
+ from qubx.core.lookups import lookup
14
+ from qubx.pandaz.utils import _frame_to_str
15
+
16
+ from .account import SimulatedAccountProcessor
17
+ from .broker import SimulatedBroker
18
+ from .data import SimulatedDataProvider
19
+ from .utils import (
20
+ SetupTypes,
21
+ SignalsProxy,
22
+ SimulatedCtrlChannel,
23
+ SimulatedScheduler,
24
+ SimulatedTimeProvider,
25
+ SimulationDataConfig,
26
+ SimulationSetup,
27
+ )
28
+
29
+
30
+ class SimulationRunner:
31
+ """
32
+ A wrapper around the StrategyContext that encapsulates the simulation logic.
33
+ This class is responsible for running a backtest context from a start time to an end time.
34
+ """
35
+
36
+ setup: SimulationSetup
37
+ data_config: SimulationDataConfig
38
+ start: pd.Timestamp
39
+ stop: pd.Timestamp
40
+ account_id: str
41
+ portfolio_log_freq: str
42
+ ctx: IStrategyContext
43
+ data_provider: SimulatedDataProvider
44
+ logs_writer: InMemoryLogsWriter
45
+
46
+ strategy_params: dict[str, Any]
47
+ strategy_class: str
48
+
49
+ # adjusted times
50
+ _stop: pd.Timestamp | None = None
51
+
52
+ def __init__(
53
+ self,
54
+ setup: SimulationSetup,
55
+ data_config: SimulationDataConfig,
56
+ start: pd.Timestamp | str,
57
+ stop: pd.Timestamp | str,
58
+ account_id: str = "SimulatedAccount",
59
+ portfolio_log_freq: str = "5Min",
60
+ ):
61
+ """
62
+ Initialize the BacktestContextRunner with a strategy context.
63
+
64
+ Args:
65
+ setup (SimulationSetup): The setup to run.
66
+ data_config (SimulationDataConfig): The data setup to use.
67
+ start (pd.Timestamp): The start time of the simulation.
68
+ stop (pd.Timestamp): The end time of the simulation.
69
+ account_id (str): The account id to use.
70
+ portfolio_log_freq (str): The portfolio log frequency to use.
71
+ """
72
+ self.setup = setup
73
+ self.data_config = data_config
74
+ self.start = pd.Timestamp(start)
75
+ self.stop = pd.Timestamp(stop)
76
+ self.account_id = account_id
77
+ self.portfolio_log_freq = portfolio_log_freq
78
+ self.ctx = self._create_backtest_context()
79
+
80
+ # - get strategy parameters BEFORE simulation start
81
+ # potentially strategy may change it's parameters during simulation
82
+ self.strategy_params = {}
83
+ self.strategy_class = ""
84
+ if self.setup.setup_type in [SetupTypes.STRATEGY, SetupTypes.STRATEGY_AND_TRACKER]:
85
+ self.strategy_params = extract_parameters_from_object(self.setup.generator)
86
+ self.strategy_class = full_qualified_class_name(self.setup.generator)
87
+
88
+ def run(self, silent: bool = False):
89
+ """
90
+ Run the backtest from start to stop.
91
+
92
+ Args:
93
+ start (pd.Timestamp | str): The start time of the simulation.
94
+ stop (pd.Timestamp | str): The end time of the simulation.
95
+ silent (bool, optional): Whether to suppress progress output. Defaults to False.
96
+ """
97
+ logger.debug(f"[<y>BacktestContextRunner</y>] :: Running simulation from {self.start} to {self.stop}")
98
+
99
+ # Start the context
100
+ self.ctx.start()
101
+
102
+ # Apply default warmup periods if strategy didn't set them
103
+ for s in self.ctx.get_subscriptions():
104
+ if not self.ctx.get_warmup(s) and (_d_wt := self.data_config.default_warmups.get(s)):
105
+ 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"
107
+ )
108
+ self.ctx.set_warmup({s: _d_wt})
109
+
110
+ # Subscribe to any custom data types if needed
111
+ def _is_known_type(t: str):
112
+ try:
113
+ DataType(t)
114
+ return True
115
+ except: # noqa: E722
116
+ return False
117
+
118
+ for t, r in self.data_config.data_providers.items():
119
+ if not _is_known_type(t) or t in [
120
+ DataType.TRADE,
121
+ DataType.OHLC_TRADES,
122
+ DataType.OHLC_QUOTES,
123
+ DataType.QUOTE,
124
+ DataType.ORDERBOOK,
125
+ ]:
126
+ logger.debug(f"[<y>BacktestContextRunner</y>] :: Subscribing to: {t}")
127
+ self.ctx.subscribe(t, self.ctx.instruments)
128
+
129
+ stop = self._stop or self.stop
130
+
131
+ try:
132
+ # Run the data provider
133
+ self.data_provider.run(self.start, stop, silent=silent)
134
+ except KeyboardInterrupt:
135
+ logger.error("Simulated trading interrupted by user!")
136
+ finally:
137
+ # Stop the context
138
+ self.ctx.stop()
139
+
140
+ def print_latency_report(self) -> None:
141
+ _l_r = SW.latency_report()
142
+ if _l_r is not None:
143
+ logger.info(
144
+ "<BLUE> Time spent in simulation report </BLUE>\n<r>"
145
+ + _frame_to_str(
146
+ _l_r.sort_values("latency", ascending=False).reset_index(drop=True), "simulation", -1, -1, False
147
+ )
148
+ + "</r>"
149
+ )
150
+
151
+ def _create_backtest_context(self) -> IStrategyContext:
152
+ tcc = lookup.fees.find(self.setup.exchange.lower(), self.setup.commissions)
153
+ if tcc is None:
154
+ raise SimulationConfigError(
155
+ f"Can't find transaction costs calculator for '{self.setup.exchange}' for specification '{self.setup.commissions}' !"
156
+ )
157
+
158
+ channel = SimulatedCtrlChannel("databus", sentinel=(None, None, None, None))
159
+ simulated_clock = SimulatedTimeProvider(np.datetime64(self.start, "ns"))
160
+
161
+ logger.debug(
162
+ f"[<y>simulator</y>] :: Preparing simulated trading on <g>{self.setup.exchange.upper()}</g> for {self.setup.capital} {self.setup.base_currency}..."
163
+ )
164
+
165
+ account = SimulatedAccountProcessor(
166
+ account_id=self.account_id,
167
+ channel=channel,
168
+ base_currency=self.setup.base_currency,
169
+ initial_capital=self.setup.capital,
170
+ time_provider=simulated_clock,
171
+ tcc=tcc,
172
+ accurate_stop_orders_execution=self.setup.accurate_stop_orders_execution,
173
+ )
174
+ scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
175
+ broker = SimulatedBroker(channel, account, self.setup.exchange)
176
+ data_provider = SimulatedDataProvider(
177
+ exchange_id=self.setup.exchange,
178
+ channel=channel,
179
+ scheduler=scheduler,
180
+ time_provider=simulated_clock,
181
+ account=account,
182
+ readers=self.data_config.data_providers,
183
+ open_close_time_indent_secs=self.data_config.adjusted_open_close_time_indent_secs,
184
+ )
185
+ # - get aux data provider
186
+ _aux_data = self.data_config.get_timeguarded_aux_reader(simulated_clock)
187
+ # - it will store simulation results into memory
188
+ logs_writer = InMemoryLogsWriter(self.account_id, self.setup.name, "0")
189
+
190
+ # - it will store simulation results into memory
191
+ strat: IStrategy | None = None
192
+
193
+ match self.setup.setup_type:
194
+ case SetupTypes.STRATEGY:
195
+ strat = self.setup.generator # type: ignore
196
+
197
+ case SetupTypes.STRATEGY_AND_TRACKER:
198
+ strat = self.setup.generator # type: ignore
199
+ strat.tracker = lambda ctx: self.setup.tracker # type: ignore
200
+
201
+ case SetupTypes.SIGNAL:
202
+ strat = SignalsProxy(timeframe=self.setup.signal_timeframe)
203
+ data_provider.set_generated_signals(self.setup.generator) # type: ignore
204
+
205
+ # - we don't need any unexpected triggerings
206
+ self._stop = min(self.setup.generator.index[-1], self.stop) # type: ignore
207
+
208
+ case SetupTypes.SIGNAL_AND_TRACKER:
209
+ strat = SignalsProxy(timeframe=self.setup.signal_timeframe)
210
+ strat.tracker = lambda ctx: self.setup.tracker
211
+ data_provider.set_generated_signals(self.setup.generator) # type: ignore
212
+
213
+ # - we don't need any unexpected triggerings
214
+ self._stop = min(self.setup.generator.index[-1], self.stop) # type: ignore
215
+
216
+ case _:
217
+ raise SimulationError(f"Unsupported setup type: {self.setup.setup_type} !")
218
+
219
+ if not isinstance(strat, IStrategy):
220
+ raise SimulationConfigError(f"Strategy should be an instance of IStrategy, but got {strat} !")
221
+
222
+ ctx = StrategyContext(
223
+ strategy=strat,
224
+ broker=broker,
225
+ data_provider=data_provider,
226
+ account=account,
227
+ scheduler=scheduler,
228
+ time_provider=simulated_clock,
229
+ instruments=self.setup.instruments,
230
+ logging=StrategyLogging(logs_writer, portfolio_log_freq=self.portfolio_log_freq),
231
+ aux_data_provider=_aux_data,
232
+ )
233
+
234
+ # - setup base subscription from spec
235
+ if ctx.get_base_subscription() == DataType.NONE:
236
+ logger.debug(
237
+ f"[<y>simulator</y>] :: Setting up default base subscription: {self.data_config.default_base_subscription}"
238
+ )
239
+ ctx.set_base_subscription(self.data_config.default_base_subscription)
240
+
241
+ # - set default on_event schedule if detected and strategy didn't set it's own schedule
242
+ if not ctx.get_event_schedule("time") and self.data_config.default_trigger_schedule:
243
+ logger.debug(f"[<y>simulator</y>] :: Setting default schedule: {self.data_config.default_trigger_schedule}")
244
+ ctx.set_event_schedule(self.data_config.default_trigger_schedule)
245
+
246
+ self.data_provider = data_provider
247
+ self.logs_writer = logs_writer
248
+ return ctx
@@ -9,6 +9,7 @@ from qubx.core.basics import DataType, Instrument, Timestamped
9
9
  from qubx.core.exceptions import SimulationError
10
10
  from qubx.data.readers import (
11
11
  AsDict,
12
+ AsOrderBook,
12
13
  AsQuotes,
13
14
  AsTrades,
14
15
  DataReader,
@@ -230,10 +231,16 @@ class DataFetcher:
230
231
  self._transformer = AsTrades()
231
232
 
232
233
  case DataType.QUOTE:
233
- self._requested_data_type = "orderbook"
234
+ # self._requested_data_type = "orderbook"
235
+ self._requested_data_type = "quote"
234
236
  self._producing_data_type = "quote" # ???
235
237
  self._transformer = AsQuotes()
236
238
 
239
+ case DataType.ORDERBOOK:
240
+ self._requested_data_type = "orderbook"
241
+ self._producing_data_type = "orderbook"
242
+ self._transformer = AsOrderBook()
243
+
237
244
  case _:
238
245
  self._requested_data_type = subtype
239
246
  self._producing_data_type = subtype
@@ -311,6 +318,12 @@ class DataFetcher:
311
318
  if self._timeframe:
312
319
  _args["timeframe"] = self._timeframe
313
320
 
321
+ # get arguments from self._reader.read
322
+ _reader_args = set(self._reader.read.__code__.co_varnames[: self._reader.read.__code__.co_argcount])
323
+ # match _args with self._params
324
+ _params = {k: v for k, v in self._params.items() if k in _reader_args}
325
+ _args = {**_args, **_params}
326
+
314
327
  try:
315
328
  _r_iters[self._fetcher_id + "." + _r] = self._reader.read(**_args) # type: ignore
316
329
  except Exception as e:
@@ -382,7 +395,7 @@ class IterableSimulationData(Iterator):
382
395
  case DataType.OHLC | DataType.OHLC_QUOTES:
383
396
  _timeframe = _params.get("timeframe", "1Min")
384
397
  _access_key = f"{_subtype}.{_timeframe}"
385
- case DataType.TRADE | DataType.QUOTE:
398
+ case DataType.TRADE | DataType.QUOTE | DataType.ORDERBOOK:
386
399
  _access_key = f"{_subtype}"
387
400
  case _:
388
401
  # - any arbitrary data type is passed as is