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

@@ -265,12 +265,13 @@ def _run_setup(
265
265
  stop,
266
266
  setup.exchanges,
267
267
  setup.instruments,
268
- setup.capital,
269
- setup.base_currency,
270
- commissions_for_result,
271
- runner.logs_writer.get_portfolio(as_plain_dataframe=True),
272
- runner.logs_writer.get_executions(),
273
- runner.logs_writer.get_signals(),
268
+ capital=setup.capital,
269
+ base_currency=setup.base_currency,
270
+ commissions=commissions_for_result,
271
+ portfolio_log=runner.logs_writer.get_portfolio(as_plain_dataframe=True),
272
+ executions_log=runner.logs_writer.get_executions(),
273
+ signals_log=runner.logs_writer.get_signals(),
274
+ targets_log=runner.logs_writer.get_targets(),
274
275
  strategy_class=runner.strategy_class,
275
276
  parameters=runner.strategy_params,
276
277
  is_simulation=True,
qubx/backtester/utils.py CHANGED
@@ -209,7 +209,7 @@ class SignalsProxy(IStrategy):
209
209
  signal = event.data.get("order")
210
210
  # - TODO: also need to think about how to pass stop/take here
211
211
  if signal is not None and event.instrument:
212
- return [event.instrument.signal(signal)]
212
+ return [event.instrument.signal(ctx, signal)]
213
213
  return None
214
214
 
215
215
 
@@ -44,10 +44,12 @@ class CcxtDataProvider(IDataProvider):
44
44
 
45
45
  # - subscriptions
46
46
  _subscriptions: Dict[str, Set[Instrument]]
47
+ _pending_subscriptions: Dict[str, Set[Instrument]] # Track subscriptions being established
47
48
  _sub_to_coro: Dict[str, concurrent.futures.Future]
48
49
  _sub_to_name: Dict[str, str]
49
50
  _sub_to_unsubscribe: Dict[str, Callable[[], Awaitable[None]]]
50
51
  _is_sub_name_enabled: Dict[str, bool]
52
+ _sub_connection_ready: Dict[str, bool] # Track if connection is actually ready
51
53
 
52
54
  _sub_instr_to_time: Dict[Tuple[str, Instrument], dt_64]
53
55
  _last_quotes: Dict[Instrument, Optional[Quote]]
@@ -80,10 +82,12 @@ class CcxtDataProvider(IDataProvider):
80
82
 
81
83
  self._last_quotes = defaultdict(lambda: None)
82
84
  self._subscriptions = defaultdict(set)
85
+ self._pending_subscriptions = defaultdict(set)
83
86
  self._sub_to_coro = {}
84
87
  self._sub_to_name = {}
85
88
  self._sub_to_unsubscribe = {}
86
89
  self._is_sub_name_enabled = defaultdict(lambda: False)
90
+ self._sub_connection_ready = defaultdict(lambda: False)
87
91
  self._symbol_to_instrument = {}
88
92
  self._subscribers = {
89
93
  n.split("_subscribe_")[1]: f
@@ -135,11 +139,33 @@ class CcxtDataProvider(IDataProvider):
135
139
  def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
136
140
  if not subscription_type:
137
141
  return list(self.subscribed_instruments)
138
- return list(self._subscriptions[subscription_type]) if subscription_type in self._subscriptions else []
142
+
143
+ # Return active subscriptions, fallback to pending if no active ones
144
+ _sub_type, _ = DataType.from_str(subscription_type)
145
+ if _sub_type in self._subscriptions:
146
+ return list(self._subscriptions[_sub_type])
147
+ elif _sub_type in self._pending_subscriptions:
148
+ return list(self._pending_subscriptions[_sub_type])
149
+ else:
150
+ return []
139
151
 
140
152
  def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
141
153
  sub_type, _ = DataType.from_str(subscription_type)
142
- return sub_type in self._subscriptions and instrument in self._subscriptions[sub_type]
154
+ # Only return True if subscription is actually active (not just pending)
155
+ return (
156
+ sub_type in self._subscriptions
157
+ and instrument in self._subscriptions[sub_type]
158
+ and self._sub_connection_ready.get(sub_type, False)
159
+ )
160
+
161
+ def has_pending_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
162
+ """Check if a subscription is pending (connection being established)."""
163
+ sub_type, _ = DataType.from_str(subscription_type)
164
+ return (
165
+ sub_type in self._pending_subscriptions
166
+ and instrument in self._pending_subscriptions[sub_type]
167
+ and not self._sub_connection_ready.get(subscription_type, False)
168
+ )
143
169
 
144
170
  def warmup(self, warmups: Dict[Tuple[str, Instrument], str]) -> None:
145
171
  _coros = []
@@ -207,9 +233,9 @@ class CcxtDataProvider(IDataProvider):
207
233
 
208
234
  @property
209
235
  def subscribed_instruments(self) -> Set[Instrument]:
210
- if not self._subscriptions:
211
- return set()
212
- return set.union(*self._subscriptions.values())
236
+ active = set.union(*self._subscriptions.values()) if self._subscriptions else set()
237
+ pending = set.union(*self._pending_subscriptions.values()) if self._pending_subscriptions else set()
238
+ return active.union(pending)
213
239
 
214
240
  @property
215
241
  def is_read_only(self) -> bool:
@@ -226,19 +252,29 @@ class CcxtDataProvider(IDataProvider):
226
252
  if _subscriber is None:
227
253
  raise ValueError(f"{self._exchange_id}: Subscription type {sub_type} is not supported")
228
254
 
255
+ # Save old subscription state before starting cleanup
256
+ old_sub_info = None
229
257
  if sub_type in self._sub_to_coro:
258
+ old_sub_info = {"name": self._sub_to_name[sub_type], "coro": self._sub_to_coro[sub_type]}
230
259
  logger.debug(
231
- f"<yellow>{self._exchange_id}</yellow> Canceling existing {sub_type} subscription for {self._subscriptions[_sub_type]}"
260
+ f"<yellow>{self._exchange_id}</yellow> Canceling existing {sub_type} subscription for {self._subscriptions.get(_sub_type, set())}"
232
261
  )
233
- # - wait for the subscriber to stop
234
- self._loop.submit(self._stop_subscriber(sub_type, self._sub_to_name[sub_type])).result()
262
+
263
+ # Clear state immediately to prevent interference with new subscription
235
264
  del self._sub_to_coro[sub_type]
236
265
  del self._sub_to_name[sub_type]
237
- del self._subscriptions[_sub_type]
266
+ # Clean up both active and pending subscriptions
267
+ self._subscriptions.pop(_sub_type, None)
268
+ self._pending_subscriptions.pop(_sub_type, None)
269
+ self._sub_connection_ready.pop(sub_type, None)
238
270
 
239
271
  if instruments is not None and len(instruments) == 0:
240
272
  return
241
273
 
274
+ # Mark subscription as pending (not active yet)
275
+ self._pending_subscriptions[_sub_type] = instruments
276
+ self._sub_connection_ready[sub_type] = False
277
+
242
278
  kwargs = {"instruments": instruments, **_params}
243
279
  _subscriber = self._subscribers[_sub_type]
244
280
  _subscriber_params = set(_subscriber.__code__.co_varnames[: _subscriber.__code__.co_argcount])
@@ -247,7 +283,12 @@ class CcxtDataProvider(IDataProvider):
247
283
  self._sub_to_name[sub_type] = (name := self._get_subscription_name(_sub_type, **kwargs))
248
284
  self._sub_to_coro[sub_type] = self._loop.submit(_subscriber(self, name, _sub_type, self.channel, **kwargs))
249
285
 
250
- self._subscriptions[_sub_type] = instruments
286
+ # Now stop the old subscriber after new one is started (to avoid interference)
287
+ if old_sub_info is not None:
288
+ # Stop old subscriber in background to avoid blocking
289
+ self._loop.submit(self._stop_old_subscriber(old_sub_info["name"], old_sub_info["coro"]))
290
+
291
+ # Don't set _subscriptions here - it will be set when connection is established
251
292
 
252
293
  def _time_msec_nbars_back(self, timeframe: str, nbarsback: int = 1) -> int:
253
294
  return (self.time_provider.time() - nbarsback * pd.Timedelta(timeframe)).asm8.item() // 1000000
@@ -278,6 +319,14 @@ class CcxtDataProvider(IDataProvider):
278
319
  _name += f" ({kwargs_str})"
279
320
  return _name
280
321
 
322
+ def _mark_subscription_active(self, sub_type: str) -> None:
323
+ """Mark a subscription as active once the WebSocket connection is established."""
324
+ _sub_type, _ = DataType.from_str(sub_type)
325
+ if _sub_type in self._pending_subscriptions:
326
+ self._subscriptions[_sub_type] = self._pending_subscriptions[_sub_type]
327
+ self._sub_connection_ready[sub_type] = True
328
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Subscription {sub_type} is now active")
329
+
281
330
  async def _stop_subscriber(self, sub_type: str, sub_name: str) -> None:
282
331
  try:
283
332
  self._is_sub_name_enabled[sub_name] = False # stop the subscriber
@@ -303,11 +352,57 @@ class CcxtDataProvider(IDataProvider):
303
352
  del self._sub_to_unsubscribe[sub_name]
304
353
 
305
354
  del self._is_sub_name_enabled[sub_name]
355
+
356
+ # Clean up connection state for this subscription
357
+ for sub_type, stream_name in list(self._sub_to_name.items()):
358
+ if stream_name == sub_name:
359
+ self._sub_connection_ready.pop(sub_type, None)
360
+ break
361
+
306
362
  logger.debug(f"<yellow>{self._exchange_id}</yellow> Unsubscribed from {sub_name}")
307
363
  except Exception as e:
308
364
  logger.error(f"<yellow>{self._exchange_id}</yellow> Error stopping {sub_name}")
309
365
  logger.exception(e)
310
366
 
367
+ async def _stop_old_subscriber(self, old_name: str, old_coro: concurrent.futures.Future) -> None:
368
+ """Stop an old subscriber safely without interfering with new subscriptions."""
369
+ try:
370
+ # Disable the old stream by name
371
+ self._is_sub_name_enabled[old_name] = False
372
+
373
+ # Wait for the old coroutine to finish
374
+ total_sleep_time = 0.0
375
+ while old_coro.running():
376
+ await asyncio.sleep(1.0)
377
+ total_sleep_time += 1.0
378
+ if total_sleep_time >= 20.0:
379
+ break
380
+
381
+ if old_coro.running():
382
+ logger.warning(
383
+ f"<yellow>{self._exchange_id}</yellow> Old subscriber {old_name} is still running. Cancelling it."
384
+ )
385
+ old_coro.cancel()
386
+ else:
387
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Old subscriber {old_name} has been stopped")
388
+
389
+ # Clean up old unsubscriber if it exists
390
+ if old_name in self._sub_to_unsubscribe:
391
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Calling old unsubscriber for {old_name}")
392
+ await self._sub_to_unsubscribe[old_name]()
393
+ # Use pop to safely remove, in case it was already removed
394
+ self._sub_to_unsubscribe.pop(old_name, None)
395
+
396
+ # Clean up old stream state
397
+ if old_name in self._is_sub_name_enabled:
398
+ del self._is_sub_name_enabled[old_name]
399
+
400
+ logger.debug(f"<yellow>{self._exchange_id}</yellow> Old subscription {old_name} cleaned up")
401
+
402
+ except Exception as e:
403
+ logger.error(f"<yellow>{self._exchange_id}</yellow> Error stopping old subscriber {old_name}")
404
+ logger.exception(e)
405
+
311
406
  async def _listen_to_stream(
312
407
  self,
313
408
  subscriber: Callable[[], Awaitable[None]],
@@ -322,10 +417,22 @@ class CcxtDataProvider(IDataProvider):
322
417
 
323
418
  self._is_sub_name_enabled[name] = True
324
419
  n_retry = 0
420
+ connection_established = False
421
+
325
422
  while channel.control.is_set() and self._is_sub_name_enabled[name]:
326
423
  try:
327
424
  await subscriber()
328
425
  n_retry = 0
426
+
427
+ # Mark subscription as active on first successful data reception
428
+ if not connection_established:
429
+ # Find the subscription type for this stream name
430
+ for sub_type, stream_name in self._sub_to_name.items():
431
+ if stream_name == name:
432
+ self._mark_subscription_active(sub_type)
433
+ connection_established = True
434
+ break
435
+
329
436
  if not self._is_sub_name_enabled[name]:
330
437
  break
331
438
  except CcxtSymbolNotRecognized:
@@ -616,7 +723,8 @@ class CcxtDataProvider(IDataProvider):
616
723
  for exch_symbol, ccxt_ticker in ccxt_tickers.items(): # type: ignore
617
724
  instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
618
725
  quote = ccxt_convert_ticker(ccxt_ticker)
619
- if self._last_quotes[instrument] is None or quote.time > self._last_quotes[instrument].time:
726
+ last_quote = self._last_quotes[instrument]
727
+ if last_quote is None or quote.time > last_quote.time:
620
728
  self._health_monitor.record_data_arrival(sub_type, dt_64(quote.time, "ns"))
621
729
  self._last_quotes[instrument] = quote
622
730
  channel.send((instrument, sub_type, quote, False))
qubx/core/basics.py CHANGED
@@ -56,22 +56,73 @@ class TimestampedDict:
56
56
  data: dict[str, Any]
57
57
 
58
58
 
59
+ class ITimeProvider:
60
+ """
61
+ Generic interface for providing current time
62
+ """
63
+
64
+ def time(self) -> dt_64:
65
+ """
66
+ Returns current time
67
+ """
68
+ ...
69
+
70
+
59
71
  # Alias for timestamped data types used in Qubx
60
72
  Timestamped: TypeAlias = Quote | Trade | Bar | OrderBook | TimestampedDict | FundingRate | Liquidation
61
73
 
62
74
 
75
+ @dataclass
76
+ class TargetPosition:
77
+ """
78
+ Class for presenting target position calculated from signal
79
+ """
80
+
81
+ time: dt_64 | str # time when position was created
82
+ instrument: "Instrument"
83
+ target_position_size: float # actual position size after processing in sizer
84
+ entry_price: float | None = None
85
+ stop_price: float | None = None
86
+ take_price: float | None = None
87
+ options: dict[str, Any] = field(default_factory=dict)
88
+
89
+ @property
90
+ def price(self) -> float | None:
91
+ return self.entry_price
92
+
93
+ @property
94
+ def stop(self) -> float | None:
95
+ return self.stop_price
96
+
97
+ @property
98
+ def take(self) -> float | None:
99
+ return self.take_price
100
+
101
+ def __str__(self) -> str:
102
+ _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
103
+ _p = f" @ {self.entry_price}" if self.entry_price is not None else ""
104
+ _s = f" stop: {self.stop_price}" if self.stop_price is not None else ""
105
+ _t = f" take: {self.take_price}" if self.take_price is not None else ""
106
+ return f"[{_d}] TARGET {self.target_position_size:+f} {self.instrument.base}{_p}{_s}{_t} for {self.instrument}"
107
+
108
+
63
109
  @dataclass
64
110
  class Signal:
65
111
  """
66
112
  Class for presenting signals generated by strategy
67
113
 
68
114
  Attributes:
69
- reference_price: float - exact price when signal was generated
115
+ reference_price: float - aux market price when signal was generated
116
+ is_service: bool - when we need this signal only for informative purposes (post-factum risk management etc)
70
117
 
71
118
  Options:
72
- - allow_override: bool - if True, and there is another signal for the same instrument, then override current.
119
+ - allow_override: bool - if True, and there is another signal for the same instrument, then override current.
120
+ - group: str - group name for signal
121
+ - comment: str - comment for signal
122
+ - options: dict[str, Any] - additional options for signal
73
123
  """
74
124
 
125
+ time: dt_64 | str # time when signal was generated
75
126
  instrument: "Instrument"
76
127
  signal: float
77
128
  price: float | None = None
@@ -81,20 +132,37 @@ class Signal:
81
132
  group: str = ""
82
133
  comment: str = ""
83
134
  options: dict[str, Any] = field(default_factory=dict)
135
+ is_service: bool = False # when we need this signal only for informative purposes (post-factum risk management etc)
136
+
137
+ def target_for_amount(self, amount: float, **kwargs) -> TargetPosition:
138
+ assert not self.is_service, "Service signals can't be converted to target positions !"
139
+ return self.instrument.target(
140
+ self.time,
141
+ self.instrument.round_size_down(amount),
142
+ entry_price=self.price,
143
+ stop_price=self.stop,
144
+ take_price=self.take,
145
+ options=self.options,
146
+ **kwargs,
147
+ )
84
148
 
85
149
  def __str__(self) -> str:
150
+ _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
86
151
  _p = f" @ {self.price}" if self.price is not None else ""
87
152
  _s = f" stop: {self.stop}" if self.stop is not None else ""
88
153
  _t = f" take: {self.take}" if self.take is not None else ""
89
154
  _r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
90
155
  _c = f" ({self.comment})" if self.comment else ""
91
- return f"{self.group}{_r} {self.signal:+f} {self.instrument}{_p}{_s}{_t}{_c}"
156
+ _i = "SERVICE ::" if self.is_service else ""
157
+
158
+ return f"[{_d}] {_i}{self.group}{_r} {self.signal:+.2f} {self.instrument}{_p}{_s}{_t}{_c}"
92
159
 
93
160
  def copy(self) -> "Signal":
94
161
  """
95
162
  Return a copy of the original signal
96
163
  """
97
164
  return Signal(
165
+ self.time,
98
166
  self.instrument,
99
167
  self.signal,
100
168
  self.price,
@@ -104,60 +172,27 @@ class Signal:
104
172
  self.group,
105
173
  self.comment,
106
174
  dict(self.options),
175
+ self.is_service,
107
176
  )
108
177
 
109
178
 
110
179
  @dataclass
111
- class TargetPosition:
180
+ class InitializingSignal(Signal):
112
181
  """
113
- Class for presenting target position calculated from signal
182
+ Special signal type for post-warmup initialization
114
183
  """
115
184
 
116
- time: dt_64 # time when position was set
117
- signal: Signal # original signal
118
- target_position_size: float # actual position size after processing in sizer
119
- _is_service: bool = False
120
-
121
- @staticmethod
122
- def create(ctx: "ITimeProvider", signal: Signal, target_size: float) -> "TargetPosition":
123
- return TargetPosition(ctx.time(), signal, signal.instrument.round_size_down(target_size))
124
-
125
- @staticmethod
126
- def zero(ctx: "ITimeProvider", signal: Signal) -> "TargetPosition":
127
- return TargetPosition(ctx.time(), signal, 0.0)
128
-
129
- @staticmethod
130
- def service(ctx: "ITimeProvider", signal: Signal, size: float | None = None) -> "TargetPosition":
131
- """
132
- Generate just service position target (for logging purposes)
133
- """
134
- return TargetPosition(ctx.time(), signal, size if size else signal.signal, _is_service=True)
135
-
136
- @property
137
- def instrument(self) -> "Instrument":
138
- return self.signal.instrument
139
-
140
- @property
141
- def price(self) -> float | None:
142
- return self.signal.price
143
-
144
- @property
145
- def stop(self) -> float | None:
146
- return self.signal.stop
147
-
148
- @property
149
- def take(self) -> float | None:
150
- return self.signal.take
151
-
152
- @property
153
- def is_service(self) -> bool:
154
- """
155
- Some target may be used just for informative purposes (post-factum risk management etc)
156
- """
157
- return self._is_service
185
+ use_limit_order: bool = False # if True, then use limit order for post-warmup initialization
158
186
 
159
187
  def __str__(self) -> str:
160
- return f"{'::: INFORMATIVE ::: ' if self.is_service else ''}Target {self.target_position_size:+f} for {self.signal}"
188
+ _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
189
+ _p = f" @ {self.price}" if self.price is not None else ""
190
+ _s = f" stop: {self.stop}" if self.stop is not None else ""
191
+ _t = f" take: {self.take}" if self.take is not None else ""
192
+ _r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
193
+ _c = f" ({self.comment})" if self.comment else ""
194
+
195
+ return f"[{_d}] POST-WARMUP-INIT ::{self.group}{_r} {self.signal:+.2f} {self.instrument}{_p}{_s}{_t}{_c}"
161
196
 
162
197
 
163
198
  class AssetType(StrEnum):
@@ -261,8 +296,26 @@ class Instrument:
261
296
  """
262
297
  return prec_ceil(price, self.price_precision)
263
298
 
299
+ def service_signal(
300
+ self,
301
+ time: dt_64 | str | ITimeProvider,
302
+ signal: float,
303
+ price: float | None = None,
304
+ stop: float | None = None,
305
+ take: float | None = None,
306
+ group: str = "",
307
+ comment: str = "",
308
+ options: dict[str, Any] | None = None,
309
+ **kwargs,
310
+ ) -> Signal:
311
+ """
312
+ Create service signal for the instrument
313
+ """
314
+ return self.signal(time, signal, price, stop, take, group, comment, options, is_service=True, **kwargs)
315
+
264
316
  def signal(
265
317
  self,
318
+ time: dt_64 | str | ITimeProvider,
266
319
  signal: float,
267
320
  price: float | None = None,
268
321
  stop: float | None = None,
@@ -270,9 +323,14 @@ class Instrument:
270
323
  group: str = "",
271
324
  comment: str = "",
272
325
  options: dict[str, Any] | None = None,
326
+ is_service: bool = False,
273
327
  **kwargs,
274
328
  ) -> Signal:
329
+ """
330
+ Create signal for the instrument
331
+ """
275
332
  return Signal(
333
+ time=time.time() if isinstance(time, ITimeProvider) else time,
276
334
  instrument=self,
277
335
  signal=signal,
278
336
  price=price,
@@ -281,6 +339,30 @@ class Instrument:
281
339
  group=group,
282
340
  comment=comment,
283
341
  options=(options or {}) | kwargs,
342
+ is_service=is_service,
343
+ )
344
+
345
+ def target(
346
+ self,
347
+ time: dt_64 | str | ITimeProvider,
348
+ amount: float,
349
+ entry_price: float | None = None,
350
+ stop_price: float | None = None,
351
+ take_price: float | None = None,
352
+ options: dict[str, Any] | None = None,
353
+ **kwargs,
354
+ ) -> TargetPosition:
355
+ """
356
+ Create target position for the instrument
357
+ """
358
+ return TargetPosition(
359
+ time=time.time() if isinstance(time, ITimeProvider) else time,
360
+ instrument=self,
361
+ target_position_size=self.round_size_down(amount),
362
+ entry_price=entry_price,
363
+ stop_price=stop_price,
364
+ take_price=take_price,
365
+ options=(options or {}) | kwargs,
284
366
  )
285
367
 
286
368
  def __hash__(self) -> int:
@@ -736,18 +818,6 @@ class CtrlChannel:
736
818
  raise QueueTimeout(f"Timeout waiting for data on {self.name} channel")
737
819
 
738
820
 
739
- class ITimeProvider:
740
- """
741
- Generic interface for providing current time
742
- """
743
-
744
- def time(self) -> dt_64:
745
- """
746
- Returns current time
747
- """
748
- ...
749
-
750
-
751
821
  class DataType(StrEnum):
752
822
  """
753
823
  Data type constants. Used for specifying the type of data and can be used for subscription to.
@@ -884,6 +954,7 @@ class RestoredState:
884
954
 
885
955
  time: np.datetime64
886
956
  balances: dict[str, AssetBalance]
957
+ instrument_to_signal_positions: dict[Instrument, list[Signal]]
887
958
  instrument_to_target_positions: dict[Instrument, list[TargetPosition]]
888
959
  positions: dict[Instrument, Position]
889
960
 
qubx/core/context.py CHANGED
@@ -15,6 +15,8 @@ from qubx.core.basics import (
15
15
  Order,
16
16
  OrderRequest,
17
17
  Position,
18
+ Signal,
19
+ TargetPosition,
18
20
  Timestamped,
19
21
  dt_64,
20
22
  )
@@ -87,6 +89,7 @@ class StrategyContext(IStrategyContext):
87
89
 
88
90
  _warmup_positions: dict[Instrument, Position] | None = None
89
91
  _warmup_orders: dict[Instrument, list[Order]] | None = None
92
+ _warmup_active_targets: dict[Instrument, list[TargetPosition]] | None = None
90
93
 
91
94
  def __init__(
92
95
  self,
@@ -420,6 +423,8 @@ class StrategyContext(IStrategyContext):
420
423
 
421
424
  # ITradingManager delegation
422
425
  def trade(self, instrument: Instrument, amount: float, price: float | None = None, time_in_force="gtc", **options):
426
+ # TODO: we need to generate target position and apply it in the processing manager
427
+ # - one of the options is to have multiple entry levels in TargetPosition class
423
428
  return self._trading_manager.trade(instrument, amount, price, time_in_force, **options)
424
429
 
425
430
  def trade_async(
@@ -522,16 +527,28 @@ class StrategyContext(IStrategyContext):
522
527
  def is_fitted(self) -> bool:
523
528
  return self._processing_manager.is_fitted()
524
529
 
530
+ def get_active_targets(self) -> dict[Instrument, list[TargetPosition]]:
531
+ return self._processing_manager.get_active_targets()
532
+
533
+ def emit_signal(self, signal: Signal) -> None:
534
+ return self._processing_manager.emit_signal(signal)
535
+
525
536
  # IWarmupStateSaver delegation
526
537
  def set_warmup_positions(self, positions: dict[Instrument, Position]) -> None:
527
538
  self._warmup_positions = positions
528
539
 
540
+ def set_warmup_active_targets(self, active_targets: dict[Instrument, list[TargetPosition]]) -> None:
541
+ self._warmup_active_targets = active_targets
542
+
529
543
  def set_warmup_orders(self, orders: dict[Instrument, list[Order]]) -> None:
530
544
  self._warmup_orders = orders
531
545
 
532
546
  def get_warmup_positions(self) -> dict[Instrument, Position]:
533
547
  return self._warmup_positions if self._warmup_positions is not None else {}
534
548
 
549
+ def get_warmup_active_targets(self) -> dict[Instrument, list[TargetPosition]]:
550
+ return self._warmup_active_targets if self._warmup_active_targets is not None else {}
551
+
535
552
  def get_warmup_orders(self) -> dict[Instrument, list[Order]]:
536
553
  return self._warmup_orders if self._warmup_orders is not None else {}
537
554