Qubx 0.6.21__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.23__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.

@@ -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,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)
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
qubx/backtester/runner.py CHANGED
@@ -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
+ )
qubx/core/context.py CHANGED
@@ -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:
qubx/core/interfaces.py CHANGED
@@ -14,7 +14,7 @@ This module includes:
14
14
 
15
15
  import traceback
16
16
  from dataclasses import dataclass
17
- from typing import Any, Callable, Dict, List, Literal, Protocol, Set, Tuple
17
+ from typing import Any, Callable, Literal, Protocol
18
18
 
19
19
  import numpy as np
20
20
  import pandas as pd
@@ -49,24 +49,24 @@ RemovalPolicy = Literal["close", "wait_for_close", "wait_for_change"]
49
49
  class ITradeDataExport:
50
50
  """Interface for exporting trading data to external systems."""
51
51
 
52
- def export_signals(self, time: dt_64, signals: List[Signal], account: "IAccountViewer") -> None:
52
+ def export_signals(self, time: dt_64, signals: list[Signal], account: "IAccountViewer") -> None:
53
53
  """
54
54
  Export signals to an external system.
55
55
 
56
56
  Args:
57
57
  time: Timestamp when the signals were generated
58
- signals: List of signals to export
58
+ signals: list of signals to export
59
59
  account: Account viewer to get account information like total capital, leverage, etc.
60
60
  """
61
61
  pass
62
62
 
63
- def export_target_positions(self, time: dt_64, targets: List[TargetPosition], account: "IAccountViewer") -> None:
63
+ def export_target_positions(self, time: dt_64, targets: list[TargetPosition], account: "IAccountViewer") -> None:
64
64
  """
65
65
  Export target positions to an external system.
66
66
 
67
67
  Args:
68
68
  time: Timestamp when the target positions were generated
69
- targets: List of target positions to export
69
+ targets: list of target positions to export
70
70
  account: Account viewer to get account information like total capital, leverage, etc.
71
71
  """
72
72
  pass
@@ -377,7 +377,7 @@ class IDataProvider:
377
377
  def subscribe(
378
378
  self,
379
379
  subscription_type: str,
380
- instruments: Set[Instrument],
380
+ instruments: set[Instrument],
381
381
  reset: bool = False,
382
382
  ) -> None:
383
383
  """
@@ -390,7 +390,7 @@ class IDataProvider:
390
390
  """
391
391
  ...
392
392
 
393
- def unsubscribe(self, subscription_type: str | None, instruments: Set[Instrument]) -> None:
393
+ def unsubscribe(self, subscription_type: str | None, instruments: set[Instrument]) -> None:
394
394
  """
395
395
  Unsubscribe from market data for a list of instruments.
396
396
 
@@ -413,7 +413,7 @@ class IDataProvider:
413
413
  """
414
414
  ...
415
415
 
416
- def get_subscriptions(self, instrument: Instrument | None = None) -> List[str]:
416
+ def get_subscriptions(self, instrument: Instrument | None = None) -> list[str]:
417
417
  """
418
418
  Get all subscriptions for an instrument.
419
419
 
@@ -421,11 +421,11 @@ class IDataProvider:
421
421
  instrument (optional): Instrument to get subscriptions for. If None, all subscriptions are returned.
422
422
 
423
423
  Returns:
424
- List[str]: List of subscriptions
424
+ list[str]: list of subscriptions
425
425
  """
426
426
  ...
427
427
 
428
- def get_subscribed_instruments(self, subscription_type: str | None = None) -> List[Instrument]:
428
+ def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
429
429
  """
430
430
  Get a list of instruments that are subscribed to a specific subscription type.
431
431
 
@@ -433,11 +433,11 @@ class IDataProvider:
433
433
  subscription_type: Type of subscription to filter by (optional)
434
434
 
435
435
  Returns:
436
- List[Instrument]: List of subscribed instruments
436
+ list[Instrument]: list of subscribed instruments
437
437
  """
438
438
  ...
439
439
 
440
- def warmup(self, configs: Dict[Tuple[str, Instrument], str]) -> None:
440
+ def warmup(self, configs: dict[tuple[str, Instrument], str]) -> None:
441
441
  """
442
442
  Run warmup for subscriptions.
443
443
 
@@ -522,7 +522,7 @@ class IMarketManager(ITimeProvider):
522
522
  sub_type: The subscription type of data to get
523
523
 
524
524
  Returns:
525
- List[Any]: The data
525
+ list[Any]: The data
526
526
  """
527
527
  ...
528
528
 
@@ -542,7 +542,7 @@ class IMarketManager(ITimeProvider):
542
542
  """Get list of subscribed instruments.
543
543
 
544
544
  Returns:
545
- list[Instrument]: List of subscribed instruments
545
+ list[Instrument]: list of subscribed instruments
546
546
  """
547
547
  ...
548
548
 
@@ -683,9 +683,9 @@ class IUniverseManager:
683
683
  """Set the trading universe.
684
684
 
685
685
  Args:
686
- instruments: List of instruments in the universe
687
- skip_callback: Skip callback to the strategy
688
- if_has_position_then: What to do if the instrument has a position
686
+ instruments: list of instruments in the universe
687
+ skip_callback: skip callback to the strategy
688
+ if_has_position_then: what to do if the instrument has a position
689
689
  - "close" (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
690
690
  - "wait_for_close" - keep instrument and it's position until it's closed from strategy (or risk management), then remove instrument from strategy
691
691
  - "wait_for_change" - keep instrument and position until strategy would try to change it - then close position and remove instrument
@@ -752,7 +752,7 @@ class IUniverseManager:
752
752
  class ISubscriptionManager:
753
753
  """Manages subscriptions."""
754
754
 
755
- def subscribe(self, subscription_type: str, instruments: List[Instrument] | Instrument | None = None) -> None:
755
+ def subscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
756
756
  """Subscribe to market data for an instrument.
757
757
 
758
758
  Args:
@@ -761,7 +761,7 @@ class ISubscriptionManager:
761
761
  """
762
762
  ...
763
763
 
764
- def unsubscribe(self, subscription_type: str, instruments: List[Instrument] | Instrument | None = None) -> None:
764
+ def unsubscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
765
765
  """Unsubscribe from market data for an instrument.
766
766
 
767
767
  Args:
@@ -799,7 +799,7 @@ class ISubscriptionManager:
799
799
  """
800
800
  ...
801
801
 
802
- def get_subscriptions(self, instrument: Instrument | None = None) -> List[str]:
802
+ def get_subscriptions(self, instrument: Instrument | None = None) -> list[str]:
803
803
  """
804
804
  Get all subscriptions for an instrument.
805
805
 
@@ -807,11 +807,11 @@ class ISubscriptionManager:
807
807
  instrument: Instrument to get subscriptions for (optional)
808
808
 
809
809
  Returns:
810
- List[str]: List of subscriptions
810
+ list[str]: list of subscriptions
811
811
  """
812
812
  ...
813
813
 
814
- def get_subscribed_instruments(self, subscription_type: str | None = None) -> List[Instrument]:
814
+ def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
815
815
  """
816
816
  Get a list of instruments that are subscribed to a specific subscription type.
817
817
 
@@ -819,7 +819,7 @@ class ISubscriptionManager:
819
819
  subscription_type: Type of subscription to filter by (optional)
820
820
 
821
821
  Returns:
822
- List[Instrument]: List of subscribed instruments
822
+ list[Instrument]: list of subscribed instruments
823
823
  """
824
824
  ...
825
825
 
@@ -963,7 +963,7 @@ class IAccountProcessor(IAccountViewer):
963
963
  """
964
964
  ...
965
965
 
966
- def add_active_orders(self, orders: Dict[str, Order]) -> None:
966
+ def add_active_orders(self, orders: dict[str, Order]) -> None:
967
967
  """Add active orders to the account.
968
968
 
969
969
  Warning only use in the beginning for state restoration because it does not update locked balances.
@@ -1116,8 +1116,8 @@ class IPositionGathering:
1116
1116
  def alter_position_size(self, ctx: IStrategyContext, target: TargetPosition) -> float: ...
1117
1117
 
1118
1118
  def alter_positions(
1119
- self, ctx: IStrategyContext, targets: List[TargetPosition] | TargetPosition
1120
- ) -> Dict[Instrument, float]:
1119
+ self, ctx: IStrategyContext, targets: list[TargetPosition] | TargetPosition
1120
+ ) -> dict[Instrument, float]:
1121
1121
  if not isinstance(targets, list):
1122
1122
  targets = [targets]
1123
1123
 
@@ -1195,7 +1195,7 @@ class PositionsTracker:
1195
1195
 
1196
1196
  def update(
1197
1197
  self, ctx: IStrategyContext, instrument: Instrument, update: Timestamped
1198
- ) -> List[TargetPosition] | TargetPosition:
1198
+ ) -> list[TargetPosition] | TargetPosition:
1199
1199
  """
1200
1200
  Tracker is being updated by new market data.
1201
1201
  It may require to change position size or create new position because of interior tracker's logic (risk management for example).
@@ -1730,7 +1730,7 @@ class IStrategy(metaclass=Mixable):
1730
1730
  """
1731
1731
  return None
1732
1732
 
1733
- def on_event(self, ctx: IStrategyContext, event: TriggerEvent) -> List[Signal] | Signal | None:
1733
+ def on_event(self, ctx: IStrategyContext, event: TriggerEvent) -> list[Signal] | Signal | None:
1734
1734
  """Called on strategy events.
1735
1735
 
1736
1736
  Args:
@@ -1742,7 +1742,7 @@ class IStrategy(metaclass=Mixable):
1742
1742
  """
1743
1743
  return None
1744
1744
 
1745
- def on_market_data(self, ctx: IStrategyContext, data: MarketEvent) -> List[Signal] | Signal | None:
1745
+ def on_market_data(self, ctx: IStrategyContext, data: MarketEvent) -> list[Signal] | Signal | None:
1746
1746
  """
1747
1747
  Called when new market data is received.
1748
1748
 
qubx/ta/indicators.pxd CHANGED
@@ -59,7 +59,7 @@ cdef class Lowest(Indicator):
59
59
 
60
60
  cdef class Std(Indicator):
61
61
  cdef int period
62
- cdef RollingSum rolling_sum, variance_sum
62
+ cdef RollingSum rolling_sum, rolling_sum_sq
63
63
  cpdef double calculate(self, long long time, double value, short new_item_started)
64
64
 
65
65
 
qubx/ta/indicators.pyx CHANGED
@@ -226,29 +226,26 @@ cdef class Std(Indicator):
226
226
  def __init__(self, str name, TimeSeries series, int period):
227
227
  self.period = period
228
228
  self.rolling_sum = RollingSum(period)
229
- self.variance_sum = RollingSum(period)
229
+ self.rolling_sum_sq = RollingSum(period)
230
230
  super().__init__(name, series)
231
231
 
232
232
  cpdef double calculate(self, long long time, double value, short new_item_started):
233
233
  # Update the rolling sum with the new value
234
234
  cdef double _sum = self.rolling_sum.update(value, new_item_started)
235
+ cdef double _sum_sq = self.rolling_sum_sq.update(value * value, new_item_started)
235
236
 
236
237
  # If we're still in the initialization stage, return NaN
237
- if self.rolling_sum.is_init_stage:
238
+ if self.rolling_sum.is_init_stage or self.rolling_sum_sq.is_init_stage:
238
239
  return np.nan
239
240
 
240
241
  # Calculate the mean from the rolling sum
241
242
  cdef double _mean = _sum / self.period
242
243
 
243
244
  # Update the variance sum with the squared deviation from the mean
244
- cdef double _var_sum = self.variance_sum.update((value - _mean) ** 2, new_item_started)
245
-
246
- # If the variance sum is still in the initialization stage, return NaN
247
- if self.variance_sum.is_init_stage:
248
- return np.nan
245
+ cdef double _variance = _sum_sq / self.period - _mean * _mean
249
246
 
250
247
  # Return the square root of the variance (standard deviation)
251
- return np.sqrt(_var_sum / self.period)
248
+ return np.sqrt(_variance)
252
249
 
253
250
 
254
251
  def std(series: TimeSeries, period: int, mean=0):
@@ -13,6 +13,7 @@ from qubx.backtester.account import SimulatedAccountProcessor
13
13
  from qubx.backtester.broker import SimulatedBroker
14
14
  from qubx.backtester.optimization import variate
15
15
  from qubx.backtester.runner import SimulationRunner
16
+ from qubx.backtester.simulated_exchange import get_simulated_exchange
16
17
  from qubx.backtester.utils import (
17
18
  SetupTypes,
18
19
  SimulatedLogFormatter,
@@ -452,12 +453,15 @@ def _create_account_processor(
452
453
  ) -> IAccountProcessor:
453
454
  if paper:
454
455
  settings = account_manager.get_exchange_settings(exchange_name)
456
+
457
+ # - TODO: here we can create different types of simulated exchanges based on it's name etc
458
+ simulated_exchange = get_simulated_exchange(exchange_name, time_provider, tcc)
459
+
455
460
  return SimulatedAccountProcessor(
456
461
  account_id=exchange_name,
462
+ exchange=simulated_exchange,
457
463
  channel=channel,
458
464
  base_currency=settings.base_currency,
459
- time_provider=time_provider,
460
- tcc=tcc,
461
465
  initial_capital=settings.initial_capital,
462
466
  restored_state=restored_state,
463
467
  )
@@ -493,7 +497,7 @@ def _create_broker(
493
497
  ) -> IBroker:
494
498
  if paper:
495
499
  assert isinstance(account, SimulatedAccountProcessor)
496
- return SimulatedBroker(channel=channel, account=account, exchange_id=exchange_name)
500
+ return SimulatedBroker(channel=channel, account=account, simulated_exchange=account._exchange)
497
501
 
498
502
  creds = account_manager.get_exchange_credentials(exchange_name)
499
503
 
@@ -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
@@ -1,14 +1,15 @@
1
1
  qubx/__init__.py,sha256=GBvbyDpm2yCMJVmGW66Jo0giLOUsKKldDGcVA_r9Ohc,8294
2
2
  qubx/_nb_magic.py,sha256=kcYn8qNb8O223ZRPpq30_n5e__lD5GSVcd0U_jhfnbM,3019
3
3
  qubx/backtester/__init__.py,sha256=OhXhLmj2x6sp6k16wm5IPATvv-E2qRZVIcvttxqPgcg,176
4
- qubx/backtester/account.py,sha256=BFbQYpdeQ81yMw2ghLr2TqWke6NWn1e_FFjHAGtiYkw,6672
5
- qubx/backtester/broker.py,sha256=yyS12b7e-7Vs_czRLXu_-pl7P47vN6tfEs6r3W0cba4,3221
6
- qubx/backtester/data.py,sha256=DEEtVlpiLNcvIDOMZcgmIkwvb-GSD2YP2qgxWxUyFxg,11966
4
+ qubx/backtester/account.py,sha256=0yvE06icSeK2ymovvaKkuftY8Ou3Z7Y2JrDa6VtkINw,3048
5
+ qubx/backtester/broker.py,sha256=JMasxycLqCT99NxN50uyQ1uxtpHYL0wpp4sJ3hB6v2M,2688
6
+ qubx/backtester/data.py,sha256=vxpZCoFZsxI7QKDfragGlb0-tCplMJH99xVZWKcvEm0,11981
7
7
  qubx/backtester/management.py,sha256=HuyzFsBPgR7j-ei78Ngcx34CeSn65c9atmaii1aTsYg,14900
8
- qubx/backtester/ome.py,sha256=8AwOKxpL_sM2Tk9EUbfmrE5z9Lg-YnHSCtLxeSoCaFE,14745
8
+ qubx/backtester/ome.py,sha256=hfqwvaMBccDpy6eMe04QupEA6BjfjAodbm707fbQUb0,14984
9
9
  qubx/backtester/optimization.py,sha256=HHUIYA6Y66rcOXoePWFOuOVX9iaHGKV0bGt_4d5e6FM,7619
10
- qubx/backtester/runner.py,sha256=WlGUP-b8RxYzBKgNJQIi0dQ379hV0lVp4rOZ60lKIDg,11092
10
+ qubx/backtester/runner.py,sha256=VM0qiq27lccBDNjenYrjIJD3acMtTNfs3T9HJCBcnLk,11507
11
11
  qubx/backtester/simulated_data.py,sha256=niujaMRj__jf4IyzCZrSBR5ZoH1VUbvsZHSewHftdmI,17240
12
+ qubx/backtester/simulated_exchange.py,sha256=ATGcJXnKdD47kUwgbc5tvPVL0tq4_-6jpgsTTAMxW3c,8124
12
13
  qubx/backtester/simulator.py,sha256=JkGm5xP8q0p4BuR_PEoDm66Uaqc0u-NX-vyjkia_0Vc,9410
13
14
  qubx/backtester/utils.py,sha256=6WKTlw6nArnZgGrJatDl8ExqvTHH0SyqEbDbMgiJbIk,33539
14
15
  qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -30,13 +31,13 @@ qubx/connectors/ccxt/utils.py,sha256=oMvL0Zh-Tt9pKPTCcZMTo293mScvTiZlE9XKw2-qDgg
30
31
  qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
32
  qubx/core/account.py,sha256=b_tKnXQxpQCoJB-kVvrFzzTgDz2lLcebQdtP99x9N8I,10912
32
33
  qubx/core/basics.py,sha256=fynmFt6n8dc-9g9IuaXxixB8x52VQ1yFQDDK_FH3LmU,29463
33
- qubx/core/context.py,sha256=TgjJP0nMfVikKY7uIphcsG6xhxpS1NpSiNNkdU1ycmo,21874
34
+ qubx/core/context.py,sha256=PmO0x5VWGXY6zMF7H2rrTgPwNhDhq-f5Y8diUoZmNE0,21875
34
35
  qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
35
36
  qubx/core/errors.py,sha256=kWCK6o0-mm87VUhhlGKqwTpvdDXAza7YRRjeyz-vwfI,609
36
37
  qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
37
38
  qubx/core/helpers.py,sha256=9nl9L_ZzT1HsMC9VthMqXfmuRS_37crB-9bVfIRHeOs,19631
38
39
  qubx/core/initializer.py,sha256=PUiD_cIjvGpuPjYyRpUjpwm3xNQ2Kipa8bAhbtxCQRo,3935
39
- qubx/core/interfaces.py,sha256=HHe4t5AhHmdLUpe2200fA_MNloNU_iiLvhKhXr6qOaI,57468
40
+ qubx/core/interfaces.py,sha256=sUs1oIcLI3ZZL78Cl9IewTpEtAERfuAiZ57xrP4HNrs,57444
40
41
  qubx/core/loggers.py,sha256=eYhJANHYwz1heeFMa5V7jYCL196wkTSvj6c-8lkPj1Y,19567
41
42
  qubx/core/lookups.py,sha256=KBE0ab4eheA6C5C-RIND_svYhE7Glb4CSlLRjMhPNRc,15906
42
43
  qubx/core/metrics.py,sha256=2AocZUYwoZE8OvScN3ULa-wyVregzlCn722QIZUe7Q8,57784
@@ -46,11 +47,11 @@ qubx/core/mixins/processing.py,sha256=dqehukrfqcLy5BeILKnkpHCvva4SbLKj1ZbQdnByu1
46
47
  qubx/core/mixins/subscription.py,sha256=J_SX0CNw2bPy4bhxe0vswvDXY4LCkwXSaj_1PepKRLY,8540
47
48
  qubx/core/mixins/trading.py,sha256=wqG-o7PxUZLd_yUAg0MUbq2LfnXmTcBLETrGv8vs9jU,6819
48
49
  qubx/core/mixins/universe.py,sha256=1oqOadbVB9nlvQQp2jDRhUvsSQkyxdoNgswxnxHTcs4,10653
49
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=GMsr0FgX5481EP68ZAwkLzKog85KnDCSJfND0h9_ZMc,978280
50
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=GK9DRBxZYO3ReW8861L8LC39bjk54BKNAAlCrr1KiOU,978280
50
51
  qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
51
52
  qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
52
53
  qubx/core/series.pyx,sha256=7DTYRj5D6O_O5MoDBj_FBjQh2zdxofi3912AnlZ7BiY,46477
53
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=8cd5oHCkH5TGcXblIHiBIz5H8NBhnvMyb98uGtF1Hfg,86568
54
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=Wgpzupp_ZIIRrY4m_GRoIpFXfb8LQxlOXsUIvcSjLeU,86568
54
55
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
55
56
  qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
56
57
  qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
@@ -111,10 +112,10 @@ qubx/restorers/signal.py,sha256=DBLqA7vDhoMTAzUC4N9UerrO0GbjeHdTeMoCz7U7iI8,6621
111
112
  qubx/restorers/state.py,sha256=ePmh604fp2kRYzMNXL-TWvZOxmtTGAaKYfHJcnKResY,4042
112
113
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
113
114
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=55CCENeW3hatx-i8u2AhMcfofHrfR9gNauS1dd52tDk,654440
115
- qubx/ta/indicators.pxd,sha256=eCJ9paOxtxbDFx4U5CUhcgB1jjCQAfVqMF2FnbJ03Lo,4222
115
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=qmqUiyajgSh8E1aWvOY32KMbVxOhHDhkcFSUjtx5Evo,654440
116
+ qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
116
117
  qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
117
- qubx/ta/indicators.pyx,sha256=FVkv5ld04TpZMT3a_kR1MU3IUuWfijzjJnh_lG78JxM,26029
118
+ qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
118
119
  qubx/trackers/__init__.py,sha256=ThIP1jXaACse5hG3lZqQSlWSKYl6APxFmBHaRcVpPdU,1002
119
120
  qubx/trackers/advanced.py,sha256=WiYqihr5ZIY74DjiCzyMCywB7F1wTt71qrXqlzzIbZQ,12764
120
121
  qubx/trackers/composite.py,sha256=W-n1vd4l-RZEoojj6lICqvJ8EgTV2kE6JUUmZUkZ1cI,6339
@@ -144,11 +145,11 @@ qubx/utils/runner/_jupyter_runner.pyt,sha256=41dLQeI2EL4wJjBDht2qKbgISgS5DtmJ6Xt
144
145
  qubx/utils/runner/accounts.py,sha256=brf_RP22-nFlpyLg-eES-4pCikqev-I7OHa2bnLDK6o,3304
145
146
  qubx/utils/runner/configs.py,sha256=Sr4h-1d84Im29_nNtHu_UCQh1-MJGudU9MvIZmh2sWA,3462
146
147
  qubx/utils/runner/factory.py,sha256=vQ2dBTbrQE9YH__-TvuFzGF-E1li-vt_qQum9GHa11g,11666
147
- qubx/utils/runner/runner.py,sha256=hZZHIFTjSSUA1ZHjE-4e4vkQMzMI25CbJ_70B7blEE0,27967
148
+ qubx/utils/runner/runner.py,sha256=03w1pZdbiXuGdcUbPy3_HLUoCk20IzmAL58FSXCYMNE,28216
148
149
  qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
149
150
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
150
- qubx-0.6.21.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
151
- qubx-0.6.21.dist-info/METADATA,sha256=ZBSDCYBQaxLVX-liMdUciuy5SYmMaVcDHo1B3Ts1YQ8,4444
152
- qubx-0.6.21.dist-info/WHEEL,sha256=h1DdjcD2ZFnKGsDLjEycQhNNPJ5l-R8qdFdDSXHrAGY,110
153
- qubx-0.6.21.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
154
- qubx-0.6.21.dist-info/RECORD,,
151
+ qubx-0.6.23.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
152
+ qubx-0.6.23.dist-info/METADATA,sha256=w1vDSRGZxYqvAKCoJUlT3605G_03Y4IHsMjbPafcRyk,4444
153
+ qubx-0.6.23.dist-info/WHEEL,sha256=XjdW4AGUgFDhpG9b3b2KPhtR_JLZvHyfemLgJJwcqOI,110
154
+ qubx-0.6.23.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
155
+ qubx-0.6.23.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 2.1.2
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp312-cp312-manylinux_2_39_x86_64
File without changes