Qubx 0.2.2__cp311-cp311-manylinux_2_35_x86_64.whl → 0.2.5__cp311-cp311-manylinux_2_35_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.

qubx/backtester/ome.py CHANGED
@@ -90,6 +90,7 @@ class OrdersManagementEngine:
90
90
  price: float | None = None,
91
91
  client_id: str | None = None,
92
92
  time_in_force: str = "gtc",
93
+ fill_at_price: bool = False,
93
94
  ) -> OmeReport:
94
95
 
95
96
  if self.bbo is None:
@@ -114,12 +115,12 @@ class OrdersManagementEngine:
114
115
  client_id,
115
116
  )
116
117
 
117
- return self._process_order(timestamp, order)
118
+ return self._process_order(timestamp, order, fill_at_price=fill_at_price)
118
119
 
119
120
  def _dbg(self, message, **kwargs) -> None:
120
121
  logger.debug(f"[OMS] {self.instrument.symbol} - {message}", **kwargs)
121
122
 
122
- def _process_order(self, timestamp: dt_64, order: Order) -> OmeReport:
123
+ def _process_order(self, timestamp: dt_64, order: Order, fill_at_price: bool = False) -> OmeReport:
123
124
  if order.status in ["CLOSED", "CANCELED"]:
124
125
  raise InvalidOrder(f"Order {order.id} is already closed or canceled.")
125
126
 
@@ -129,7 +130,10 @@ class OrdersManagementEngine:
129
130
 
130
131
  # - check if order can be "executed" immediately
131
132
  exec_price = None
132
- if order.type == "MARKET":
133
+ if fill_at_price and order.price:
134
+ exec_price = order.price
135
+
136
+ elif order.type == "MARKET":
133
137
  exec_price = c_ask if buy_side else c_bid
134
138
 
135
139
  elif order.type == "LIMIT":
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, List, Sequence, Tuple
1
+ from typing import Any, Dict, List, Sequence, Tuple, Type
2
2
  import numpy as np
3
3
  import re
4
4
 
@@ -71,7 +71,7 @@ def permutate_params(
71
71
  case str():
72
72
  vals.append([v])
73
73
  case _:
74
- vals.append(list(v))
74
+ vals.append([v])
75
75
  # vals.append(v if isinstance(v, (List, Tuple)) else list(v) if isinstance(v, range) else [v])
76
76
  d = [dict(zip(args, p)) for p in product(*vals)]
77
77
  result = []
@@ -90,7 +90,7 @@ def permutate_params(
90
90
  return _wrap_single_list(result) if wrap_as_list else result
91
91
 
92
92
 
93
- def variate(clz, *args, conditions=None, **kwargs) -> Dict[str, Any]:
93
+ def variate(clz: Type[Any] | List[Type[Any]], *args, conditions=None, **kwargs) -> Dict[str, Any]:
94
94
  """
95
95
  Make variations of parameters for simulations (micro optimizer)
96
96
 
@@ -127,15 +127,29 @@ def variate(clz, *args, conditions=None, **kwargs) -> Dict[str, Any]:
127
127
  >>> variate(MomentumStrategy_Ex1_test, 10, lookback_period=[1,2,3], filter_type=['ema', 'sma'], skip_entries_flag=[True, False]),
128
128
  >>> data, capital, ["BINANCE.UM:BTCUSDT"], dict(type="ohlc", timeframe="5Min", nback=0), "5Min -1Sec", "vip0_usdt", "2024-01-01", "2024-01-02"
129
129
  >>> )
130
+
131
+ Also it's possible to pass a class with tracker:
132
+ >>> variate([MomentumStrategy_Ex1_test, AtrTracker(2, 1)], 10, lookback_period=[1,2,3], filter_type=['ema', 'sma'], skip_entries_flag=[True, False])
130
133
  """
131
134
 
132
135
  def _cmprss(xs: str):
133
136
  return "".join([x[0] for x in re.split("((?<!-)(?=[A-Z]))|_|(\d)", xs) if x])
134
137
 
135
- sfx = _cmprss(clz.__name__)
138
+ if isinstance(clz, type):
139
+ sfx = _cmprss(clz.__name__)
140
+ _mk = lambda k, *args, **kwargs: k(*args, **kwargs)
141
+ elif isinstance(clz, (list, tuple)) and clz and isinstance(clz[0], type):
142
+ sfx = _cmprss(clz[0].__name__)
143
+ _mk = lambda k, *args, **kwargs: [k[0](*args, **kwargs), *k[1:]]
144
+ else:
145
+ raise ValueError(
146
+ "Can't recognize data for variating: must be either a class type or a list where first element is class type"
147
+ )
148
+
136
149
  to_excl = [s for s, v in kwargs.items() if not isinstance(v, (list, set, tuple, range))]
137
150
  dic2str = lambda ds: [_cmprss(k) + "=" + str(v) for k, v in ds.items() if k not in to_excl]
138
151
 
139
152
  return {
140
- f"{sfx}_({ ','.join(dic2str(z)) })": clz(*args, **z) for z in permutate_params(kwargs, conditions=conditions)
153
+ f"{sfx}_({ ','.join(dic2str(z)) })": _mk(clz, *args, **z)
154
+ for z in permutate_params(kwargs, conditions=conditions)
141
155
  }
qubx/backtester/queue.py CHANGED
@@ -4,11 +4,13 @@ import heapq
4
4
  from dataclasses import dataclass
5
5
  from collections import defaultdict
6
6
  from typing import Any, Iterator, Iterable
7
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, Future
7
8
 
8
9
  from qubx import logger
9
10
  from qubx.core.basics import Instrument, dt_64, BatchEvent
10
11
  from qubx.data.readers import DataReader, DataTransformer
11
12
  from qubx.utils.misc import Stopwatch
13
+ from qubx.core.exceptions import SimulatorError
12
14
 
13
15
 
14
16
  _SW = Stopwatch()
@@ -133,17 +135,141 @@ class SimulatedDataQueue:
133
135
  return self
134
136
 
135
137
  def __iter__(self) -> Iterator:
136
- logger.info("Initializing chunks for each loader")
138
+ logger.debug("Initializing chunks for each loader")
139
+ assert self._start is not None
140
+ self._current_time = int(pd.Timestamp(self._start).timestamp() * 1e9)
141
+ self._index_to_chunk_size = {}
142
+ self._index_to_iterator = {}
143
+ self._event_heap = []
144
+ for loader_index in self._index_to_loader.keys():
145
+ self._add_chunk_to_heap(loader_index)
146
+ return self
147
+
148
+ @_SW.watch("DataQueue")
149
+ def __next__(self) -> tuple[str, str, Any]:
150
+ if not self._event_heap:
151
+ raise StopIteration
152
+
153
+ loader_index = None
154
+
155
+ # get the next event from the heap
156
+ # if the loader_index is in the removed_loader_indices, skip it (optimization to avoid unnecessary heap operations)
157
+ while self._event_heap and (loader_index is None or loader_index in self._removed_loader_indices):
158
+ dt, loader_index, chunk_index, event = heapq.heappop(self._event_heap)
159
+
160
+ if loader_index is None or loader_index in self._removed_loader_indices:
161
+ raise StopIteration
162
+
163
+ loader = self._index_to_loader[loader_index]
164
+ data_type = loader.data_type
165
+ if dt < self._current_time: # type: ignore
166
+ data_type = f"hist_{data_type}"
167
+ else:
168
+ # only update the current time if the event is not historical
169
+ self._current_time = dt
170
+
171
+ chunk_size = self._index_to_chunk_size[loader_index]
172
+ if chunk_index + 1 == chunk_size:
173
+ self._add_chunk_to_heap(loader_index)
174
+
175
+ return loader.symbol, data_type, event
176
+
177
+ @_SW.watch("DataQueue")
178
+ def _add_chunk_to_heap(self, loader_index: int):
179
+ chunk = self._next_chunk(loader_index)
180
+ self._index_to_chunk_size[loader_index] = len(chunk)
181
+ for chunk_index, event in enumerate(chunk):
182
+ dt = event.time # type: ignore
183
+ heapq.heappush(self._event_heap, (dt, loader_index, chunk_index, event))
184
+
185
+ @_SW.watch("DataQueue")
186
+ def _next_chunk(self, index: int) -> list[Any]:
187
+ if index not in self._index_to_iterator:
188
+ self._index_to_iterator[index] = self._index_to_loader[index].load(pd.Timestamp(self._current_time, unit="ns"), self._stop) # type: ignore
189
+ iterator = self._index_to_iterator[index]
190
+ try:
191
+ return next(iterator)
192
+ except StopIteration:
193
+ return []
194
+
195
+
196
+ class SimulatedDataQueueWithThreads(SimulatedDataQueue):
197
+ _loaders: dict[str, list[DataLoader]]
198
+
199
+ def __init__(self, workers: int = 4, prefetch_chunk_count: int = 1):
200
+ self._loaders = defaultdict(list)
201
+ self._start = None
202
+ self._stop = None
203
+ self._current_time = None
204
+ self._index_to_loader: dict[int, DataLoader] = {}
205
+ self._index_to_prefetch: dict[int, list[Future]] = defaultdict(list)
206
+ self._index_to_done: dict[int, bool] = defaultdict(bool)
207
+ self._loader_to_index = {}
208
+ self._index_to_chunk_size = {}
209
+ self._index_to_iterator = {}
210
+ self._latest_loader_index = -1
211
+ self._removed_loader_indices = set()
212
+ # TODO: potentially use ProcessPoolExecutor for better performance
213
+ self._pool = ThreadPoolExecutor(max_workers=workers)
214
+ self._prefetch_chunk_count = prefetch_chunk_count
215
+
216
+ @property
217
+ def is_running(self) -> bool:
218
+ return self._current_time is not None
219
+
220
+ def __add__(self, loader: DataLoader) -> "SimulatedDataQueueWithThreads":
221
+ self._latest_loader_index += 1
222
+ new_loader_index = self._latest_loader_index
223
+ self._loaders[loader.symbol].append(loader)
224
+ self._index_to_loader[new_loader_index] = loader
225
+ self._loader_to_index[loader] = new_loader_index
226
+ if self.is_running:
227
+ self._submit_chunk(new_loader_index)
228
+ self._add_chunk_to_heap(new_loader_index)
229
+ return self
230
+
231
+ def __sub__(self, loader: DataLoader) -> "SimulatedDataQueueWithThreads":
232
+ loader_index = self._loader_to_index[loader]
233
+ self._loaders[loader.symbol].remove(loader)
234
+ del self._index_to_loader[loader_index]
235
+ del self._loader_to_index[loader]
236
+ del self._index_to_chunk_size[loader_index]
237
+ del self._index_to_iterator[loader_index]
238
+ del self._index_to_done[loader_index]
239
+ for future in self._index_to_prefetch[loader_index]:
240
+ future.cancel()
241
+ del self._index_to_prefetch[loader_index]
242
+ self._removed_loader_indices.add(loader_index)
243
+ return self
244
+
245
+ def get_loader(self, symbol: str, data_type: str) -> DataLoader:
246
+ loaders = self._loaders[symbol]
247
+ for loader in loaders:
248
+ if loader.data_type == data_type:
249
+ return loader
250
+ raise ValueError(f"Loader for {symbol} and {data_type} not found")
251
+
252
+ def create_iterable(self, start: str | pd.Timestamp, stop: str | pd.Timestamp) -> Iterator:
253
+ self._start = start
254
+ self._stop = stop
255
+ self._current_time = None
256
+ return self
257
+
258
+ def __iter__(self) -> Iterator:
259
+ logger.debug("Initializing chunks for each loader")
137
260
  self._current_time = self._start
138
261
  self._index_to_chunk_size = {}
139
262
  self._index_to_iterator = {}
140
263
  self._event_heap = []
264
+ self._submit_chunk_prefetchers()
141
265
  for loader_index in self._index_to_loader.keys():
142
266
  self._add_chunk_to_heap(loader_index)
143
267
  return self
144
268
 
145
269
  @_SW.watch("DataQueue")
146
270
  def __next__(self) -> tuple[str, str, Any]:
271
+ self._submit_chunk_prefetchers()
272
+
147
273
  if not self._event_heap:
148
274
  raise StopIteration
149
275
 
@@ -167,13 +293,21 @@ class SimulatedDataQueue:
167
293
 
168
294
  @_SW.watch("DataQueue")
169
295
  def _add_chunk_to_heap(self, loader_index: int):
170
- chunk = self._next_chunk(loader_index)
296
+ futures = self._index_to_prefetch[loader_index]
297
+ if not futures and not self._index_to_done[loader_index]:
298
+ loader = self._index_to_loader[loader_index]
299
+ logger.error(f"Error state: No submitted tasks for loader {loader.symbol} {loader.data_type}")
300
+ raise SimulatorError("No submitted tasks for loader")
301
+ elif self._index_to_done[loader_index]:
302
+ return
303
+
304
+ # wait for future to finish if needed
305
+ chunk = futures.pop(0).result()
171
306
  self._index_to_chunk_size[loader_index] = len(chunk)
172
307
  for chunk_index, event in enumerate(chunk):
173
308
  dt = event.time # type: ignore
174
309
  heapq.heappush(self._event_heap, (dt, loader_index, chunk_index, event))
175
310
 
176
- @_SW.watch("DataQueue")
177
311
  def _next_chunk(self, index: int) -> list[Any]:
178
312
  if index not in self._index_to_iterator:
179
313
  self._index_to_iterator[index] = self._index_to_loader[index].load(self._current_time, self._stop) # type: ignore
@@ -183,6 +317,18 @@ class SimulatedDataQueue:
183
317
  except StopIteration:
184
318
  return []
185
319
 
320
+ def _submit_chunk_prefetchers(self):
321
+ for index in self._index_to_loader.keys():
322
+ if len(self._index_to_prefetch[index]) < self._prefetch_chunk_count:
323
+ self._submit_chunk(index)
324
+
325
+ def _submit_chunk(self, loader_index: int) -> None:
326
+ future = self._pool.submit(self._next_chunk, loader_index)
327
+ self._index_to_prefetch[loader_index].append(future)
328
+
329
+ def __del__(self):
330
+ self._pool.shutdown()
331
+
186
332
 
187
333
  class EventBatcher:
188
334
  _BATCH_SETTINGS = {
@@ -202,7 +348,8 @@ class EventBatcher:
202
348
  yield from _iter
203
349
  return
204
350
 
205
- last_symbol, last_data_type = None, None
351
+ last_symbol: str = None # type: ignore
352
+ last_data_type: str = None # type: ignore
206
353
  buffer = []
207
354
  for symbol, data_type, event in self.source_iterator:
208
355
  time: dt_64 = event.time # type: ignore
@@ -233,7 +380,7 @@ class EventBatcher:
233
380
  if pd.Timedelta(time - buffer[0].time) >= self._batch_settings[data_type]:
234
381
  yield symbol, data_type, self._batch_event(buffer)
235
382
  buffer = []
236
- last_symbol, last_data_type = None, None
383
+ last_symbol, last_data_type = None, None # type: ignore
237
384
 
238
385
  if buffer:
239
386
  yield last_symbol, last_data_type, self._batch_event(buffer)
@@ -187,13 +187,22 @@ class SimulatedTrading(ITradingServiceProvider):
187
187
  price: float | None = None,
188
188
  client_id: str | None = None,
189
189
  time_in_force: str = "gtc",
190
+ **optional,
190
191
  ) -> Order:
191
192
  ome = self._ome.get(instrument.symbol)
192
193
  if ome is None:
193
194
  raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument.symbol}'!")
194
195
 
195
196
  # - try to place order in OME
196
- report = ome.place_order(order_side.upper(), order_type.upper(), amount, price, client_id, time_in_force)
197
+ report = ome.place_order(
198
+ order_side.upper(),
199
+ order_type.upper(),
200
+ amount,
201
+ price,
202
+ client_id,
203
+ time_in_force,
204
+ fill_at_price=optional.get("fill_at_price", False),
205
+ )
197
206
  order = report.order
198
207
  self._order_to_symbol[order.id] = instrument.symbol
199
208
 
@@ -476,6 +485,9 @@ class SimulatedExchange(IBrokerServiceProvider):
476
485
  logger.info(f"SimulatedExchangeService :: run :: Simulation finished at {end}")
477
486
 
478
487
  def _run_generated_signals(self, symbol: str, data_type: str, data: Any) -> None:
488
+ is_hist = data_type.startswith("hist")
489
+ if is_hist:
490
+ raise ValueError("Historical data is not supported for pre-generated signals !")
479
491
  cc = self.get_communication_channel()
480
492
  t = data.time # type: ignore
481
493
  self._current_time = max(np.datetime64(t, "ns"), self._current_time)
@@ -496,18 +508,20 @@ class SimulatedExchange(IBrokerServiceProvider):
496
508
  t = data.time # type: ignore
497
509
  self._current_time = max(np.datetime64(t, "ns"), self._current_time)
498
510
  q = self.trading_service.emulate_quote_from_data(symbol, np.datetime64(t, "ns"), data)
499
- if q is not None:
511
+ is_hist = data_type.startswith("hist")
512
+ if not is_hist and q is not None:
500
513
  self._last_quotes[symbol] = q
501
514
  self.trading_service.update_position_price(symbol, self._current_time, q)
502
515
 
503
516
  cc.send((symbol, data_type, data))
504
517
 
505
- if q is not None:
506
- cc.send((symbol, "quote", q))
518
+ if not is_hist:
519
+ if q is not None and data_type != "quote":
520
+ cc.send((symbol, "quote", q))
507
521
 
508
- if self._scheduler.check_and_run_tasks():
509
- # - push nothing - it will force to process last event
510
- cc.send((None, "time", None))
522
+ if self._scheduler.check_and_run_tasks():
523
+ # - push nothing - it will force to process last event
524
+ cc.send((None, "time", None))
511
525
 
512
526
  def get_quote(self, symbol: str) -> Optional[Quote]:
513
527
  return self._last_quotes[symbol]
@@ -664,13 +678,14 @@ def simulate(
664
678
  commissions: str,
665
679
  start: str | pd.Timestamp,
666
680
  stop: str | pd.Timestamp | None = None,
681
+ fit: str | None = None,
667
682
  exchange: str | None = None, # in case if exchange is not specified in symbols list
668
683
  base_currency: str = "USDT",
669
684
  leverage: float = 1.0, # TODO: we need to add support for leverage
670
685
  n_jobs: int = 1,
671
686
  silent: bool = False,
672
687
  enable_event_batching: bool = True,
673
- ) -> TradingSessionResult | List[TradingSessionResult]:
688
+ ) -> list[TradingSessionResult]:
674
689
  # - recognize provided data
675
690
  if isinstance(data, dict):
676
691
  data_reader = InMemoryDataFrameReader(data)
@@ -723,6 +738,7 @@ def simulate(
723
738
  data_reader,
724
739
  subscription,
725
740
  trigger,
741
+ fit=fit,
726
742
  n_jobs=n_jobs,
727
743
  silent=silent,
728
744
  enable_event_batching=enable_event_batching,
@@ -785,6 +801,7 @@ def _run_setups(
785
801
  data_reader: DataReader,
786
802
  subscription: Dict[str, Any],
787
803
  trigger: str | list[str],
804
+ fit: str | None,
788
805
  n_jobs: int = -1,
789
806
  silent: bool = False,
790
807
  enable_event_batching: bool = True,
@@ -799,27 +816,31 @@ def _run_setups(
799
816
 
800
817
  reports = ProgressParallel(n_jobs=n_jobs, total=len(setups), silent=_main_loop_silent, backend="multiprocessing")(
801
818
  delayed(_run_setup)(
819
+ id,
802
820
  s,
803
821
  start,
804
822
  stop,
805
823
  data_reader,
806
824
  subscription,
807
825
  trigger,
826
+ fit=fit,
808
827
  silent=silent,
809
828
  enable_event_batching=enable_event_batching,
810
829
  )
811
- for s in setups
830
+ for id, s in enumerate(setups)
812
831
  )
813
832
  return reports # type: ignore
814
833
 
815
834
 
816
835
  def _run_setup(
836
+ setup_id: int,
817
837
  setup: SimulationSetup,
818
838
  start: str | pd.Timestamp,
819
839
  stop: str | pd.Timestamp,
820
840
  data_reader: DataReader,
821
841
  subscription: Dict[str, Any],
822
842
  trigger: str | list[str],
843
+ fit: str | None,
823
844
  silent: bool = False,
824
845
  enable_event_batching: bool = True,
825
846
  ) -> TradingSessionResult:
@@ -870,6 +891,7 @@ def _run_setup(
870
891
  instruments=setup.instruments,
871
892
  md_subscription=subscription,
872
893
  trigger_spec=_trigger,
894
+ fit_spec=fit,
873
895
  logs_writer=logs_writer,
874
896
  )
875
897
  ctx.start()
@@ -880,6 +902,7 @@ def _run_setup(
880
902
  logger.error("Simulated trading interrupted by user !")
881
903
 
882
904
  return TradingSessionResult(
905
+ setup_id,
883
906
  setup.name,
884
907
  start,
885
908
  stop,
qubx/core/basics.py CHANGED
@@ -19,6 +19,13 @@ td_64 = np.timedelta64
19
19
  class Signal:
20
20
  """
21
21
  Class for presenting signals generated by strategy
22
+
23
+ Attributes:
24
+ reference_price: float - exact price when signal was generated
25
+
26
+ Options:
27
+ - fill_at_signal_price: bool - if True, then fill order at signal price (only used in backtesting)
28
+ - allow_override: bool - if True, and there is another signal for the same instrument, then override current.
22
29
  """
23
30
 
24
31
  instrument: "Instrument"
@@ -26,15 +33,20 @@ class Signal:
26
33
  price: float | None = None
27
34
  stop: float | None = None
28
35
  take: float | None = None
36
+ reference_price: float | None = None
29
37
  group: str = ""
30
38
  comment: str = ""
39
+ options: dict[str, Any] = field(default_factory=dict)
31
40
 
32
41
  def __str__(self) -> str:
33
42
  _p = f" @ { self.price }" if self.price is not None else ""
34
43
  _s = f" stop: { self.stop }" if self.stop is not None else ""
35
44
  _t = f" take: { self.take }" if self.take is not None else ""
45
+ _r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
36
46
  _c = f" [{self.comment}]" if self.take is not None else ""
37
- return f"{self.group} {self.signal:+f} {self.instrument.symbol}{_p}{_s}{_t} on {self.instrument.exchange}{_c}"
47
+ return (
48
+ f"{self.group}{_r} {self.signal:+f} {self.instrument.symbol}{_p}{_s}{_t} on {self.instrument.exchange}{_c}"
49
+ )
38
50
 
39
51
 
40
52
  @dataclass
@@ -43,9 +55,18 @@ class TargetPosition:
43
55
  Class for presenting target position calculated from signal
44
56
  """
45
57
 
58
+ time: dt_64 # time when position was set
46
59
  signal: Signal # original signal
47
60
  target_position_size: float # actual position size after processing in sizer
48
61
 
62
+ @staticmethod
63
+ def create(ctx: "ITimeProvider", signal: Signal, target_size: float) -> "TargetPosition":
64
+ return TargetPosition(ctx.time(), signal, target_size)
65
+
66
+ @staticmethod
67
+ def zero(ctx: "ITimeProvider", signal: Signal) -> "TargetPosition":
68
+ return TargetPosition(ctx.time(), signal, 0.0)
69
+
49
70
  @property
50
71
  def instrument(self) -> "Instrument":
51
72
  return self.signal.instrument
@@ -63,7 +84,7 @@ class TargetPosition:
63
84
  return self.signal.take
64
85
 
65
86
  def __str__(self) -> str:
66
- return f"Target for {self.signal} -> {self.target_position_size}"
87
+ return f"Target for {self.signal} -> {self.target_position_size} at {self.time}"
67
88
 
68
89
 
69
90
  @dataclass
@@ -125,14 +146,26 @@ class Instrument:
125
146
  take: float | None = None,
126
147
  group: str = "",
127
148
  comment: str = "",
149
+ options: dict[str, Any] = None,
128
150
  ) -> Signal:
129
- return Signal(self, signal, price, stop, take, group, comment)
151
+ return Signal(
152
+ self,
153
+ signal=signal,
154
+ price=price,
155
+ stop=stop,
156
+ take=take,
157
+ group=group,
158
+ comment=comment,
159
+ options=options or {},
160
+ )
130
161
 
131
162
  def __hash__(self) -> int:
132
163
  return hash((self.symbol, self.exchange, self.market_type))
133
164
 
134
165
  def __eq__(self, other: Any) -> bool:
135
- if not isinstance(other, "Instrument"):
166
+ if other is None:
167
+ return False
168
+ if type(other) != type(self):
136
169
  return False
137
170
  return self.symbol == other.symbol and self.exchange == other.exchange and self.market_type == other.market_type
138
171
 
@@ -502,7 +535,8 @@ class ITimeProvider:
502
535
 
503
536
 
504
537
  class TradingSessionResult:
505
- trading_id: str
538
+ id: int
539
+ name: str
506
540
  start: str | pd.Timestamp
507
541
  stop: str | pd.Timestamp
508
542
  exchange: str
@@ -518,7 +552,8 @@ class TradingSessionResult:
518
552
 
519
553
  def __init__(
520
554
  self,
521
- trading_id: str,
555
+ id: int,
556
+ name: str,
522
557
  start: str | pd.Timestamp,
523
558
  stop: str | pd.Timestamp,
524
559
  exchange: str,
@@ -532,7 +567,8 @@ class TradingSessionResult:
532
567
  signals_log: pd.DataFrame,
533
568
  is_simulation=True,
534
569
  ):
535
- self.trading_id = trading_id
570
+ self.id = id
571
+ self.name = name
536
572
  self.start = start
537
573
  self.stop = stop
538
574
  self.exchange = exchange