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

@@ -154,7 +154,6 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
154
154
  return
155
155
  for r in ome.process_market_data(data):
156
156
  if r.exec is not None:
157
- # TODO: why do we pop here if it is popped later after send
158
157
  if r.order.id in self.order_to_instrument:
159
158
  self.order_to_instrument.pop(r.order.id)
160
159
  # - process methods will be called from stg context
qubx/backtester/runner.py CHANGED
@@ -8,7 +8,8 @@ from qubx.core.basics import SW, DataType
8
8
  from qubx.core.context import StrategyContext
9
9
  from qubx.core.exceptions import SimulationConfigError, SimulationError
10
10
  from qubx.core.helpers import extract_parameters_from_object, full_qualified_class_name
11
- from qubx.core.interfaces import IMetricEmitter, IStrategy, IStrategyContext
11
+ from qubx.core.initializer import BasicStrategyInitializer
12
+ from qubx.core.interfaces import IMetricEmitter, IStrategy, IStrategyContext, StrategyState
12
13
  from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
13
14
  from qubx.core.lookups import lookup
14
15
  from qubx.pandaz.utils import _frame_to_str
@@ -58,6 +59,8 @@ class SimulationRunner:
58
59
  account_id: str = "SimulatedAccount",
59
60
  portfolio_log_freq: str = "5Min",
60
61
  emitter: IMetricEmitter | None = None,
62
+ strategy_state: StrategyState | None = None,
63
+ initializer: BasicStrategyInitializer | None = None,
61
64
  ):
62
65
  """
63
66
  Initialize the BacktestContextRunner with a strategy context.
@@ -78,6 +81,8 @@ class SimulationRunner:
78
81
  self.account_id = account_id
79
82
  self.portfolio_log_freq = portfolio_log_freq
80
83
  self.emitter = emitter
84
+ self.strategy_state = strategy_state if strategy_state is not None else StrategyState()
85
+ self.initializer = initializer
81
86
  self.ctx = self._create_backtest_context()
82
87
 
83
88
  # - get strategy parameters BEFORE simulation start
@@ -239,6 +244,8 @@ class SimulationRunner:
239
244
  logging=StrategyLogging(logs_writer, portfolio_log_freq=self.portfolio_log_freq),
240
245
  aux_data_provider=_aux_data,
241
246
  emitter=self.emitter,
247
+ strategy_state=self.strategy_state,
248
+ initializer=self.initializer,
242
249
  )
243
250
 
244
251
  # - setup base subscription from spec
@@ -11,6 +11,7 @@ from qubx.core.basics import (
11
11
  Order,
12
12
  Position,
13
13
  )
14
+ from qubx.core.exceptions import InvalidOrderParameters
14
15
  from qubx.core.interfaces import (
15
16
  IAccountProcessor,
16
17
  IBroker,
@@ -54,13 +55,20 @@ class CcxtBroker(IBroker):
54
55
  price: float | None = None,
55
56
  client_id: str | None = None,
56
57
  time_in_force: str = "gtc",
58
+ **options,
57
59
  ) -> Order:
58
60
  params = {}
61
+ _is_trigger_order = order_type.startswith("stop_")
59
62
 
60
- if order_type == "limit":
63
+ if order_type == "limit" or _is_trigger_order:
61
64
  params["timeInForce"] = time_in_force.upper()
62
65
  if price is None:
63
- raise ValueError("Price must be specified for limit order")
66
+ raise InvalidOrderParameters(f"Price must be specified for '{order_type}' order")
67
+
68
+ # - handle trigger (stop) orders
69
+ if _is_trigger_order:
70
+ params["triggerPrice"] = price
71
+ order_type = order_type.split("_")[1]
64
72
 
65
73
  if client_id:
66
74
  params["newClientOrderId"] = client_id
qubx/core/context.py CHANGED
@@ -100,6 +100,7 @@ class StrategyContext(IStrategyContext):
100
100
  lifecycle_notifier: IStrategyLifecycleNotifier | None = None,
101
101
  initializer: BasicStrategyInitializer | None = None,
102
102
  strategy_name: str | None = None,
103
+ strategy_state: StrategyState | None = None,
103
104
  ) -> None:
104
105
  self.account = account
105
106
  self.strategy = self.__instantiate_strategy(strategy, config)
@@ -122,7 +123,7 @@ class StrategyContext(IStrategyContext):
122
123
  self._cache = CachedMarketDataHolder()
123
124
  self._exporter = exporter
124
125
  self._lifecycle_notifier = lifecycle_notifier
125
- self._strategy_state = StrategyState()
126
+ self._strategy_state = strategy_state if strategy_state is not None else StrategyState()
126
127
  self._strategy_name = strategy_name if strategy_name is not None else strategy.__class__.__name__
127
128
 
128
129
  __position_tracker = self.strategy.tracker(self)
@@ -182,7 +183,9 @@ class StrategyContext(IStrategyContext):
182
183
  self.__post_init__()
183
184
 
184
185
  def __post_init__(self) -> None:
185
- self.strategy.on_init(self.initializer)
186
+ if not self._strategy_state.is_on_init_called:
187
+ self.strategy.on_init(self.initializer)
188
+ self._strategy_state.is_on_init_called = True
186
189
 
187
190
  if base_sub := self.initializer.get_base_subscription():
188
191
  self.set_base_subscription(base_sub)
@@ -196,6 +199,14 @@ class StrategyContext(IStrategyContext):
196
199
  if event_schedule := self.initializer.get_event_schedule():
197
200
  self.set_event_schedule(event_schedule)
198
201
 
202
+ if pending_global_subscriptions := self.initializer.get_pending_global_subscriptions():
203
+ for sub_type in pending_global_subscriptions:
204
+ self.subscribe(sub_type)
205
+
206
+ if pending_instrument_subscriptions := self.initializer.get_pending_instrument_subscriptions():
207
+ for sub_type, instruments in pending_instrument_subscriptions.items():
208
+ self.subscribe(sub_type, list(instruments))
209
+
199
210
  # - update cache default timeframe
200
211
  sub_type = self.get_base_subscription()
201
212
  _, params = DataType.from_str(sub_type)
@@ -208,7 +219,7 @@ class StrategyContext(IStrategyContext):
208
219
 
209
220
  # Update initial instruments if strategy set them after warmup
210
221
  if self.get_warmup_positions():
211
- self._initial_instruments = list(self.get_warmup_positions().keys())
222
+ self._initial_instruments = list(set(self.get_warmup_positions().keys()) | set(self._initial_instruments))
212
223
 
213
224
  # Notify strategy start
214
225
  if self._lifecycle_notifier:
@@ -263,7 +274,8 @@ class StrategyContext(IStrategyContext):
263
274
 
264
275
  # - invoke strategy's stop code
265
276
  try:
266
- self.strategy.on_stop(self)
277
+ if not self._strategy_state.is_warmup_in_progress:
278
+ self.strategy.on_stop(self)
267
279
  except Exception as strat_error:
268
280
  logger.error(
269
281
  f"[<y>StrategyContext</y>] :: Strategy {self._strategy_name} raised an exception in on_stop: {strat_error}"
qubx/core/exceptions.py CHANGED
@@ -14,6 +14,10 @@ class InvalidOrder(ExchangeError):
14
14
  pass
15
15
 
16
16
 
17
+ class InvalidOrderParameters(ExchangeError):
18
+ pass
19
+
20
+
17
21
  class OrderNotFound(InvalidOrder):
18
22
  pass
19
23
 
qubx/core/helpers.py CHANGED
@@ -109,13 +109,19 @@ class CachedMarketDataHolder:
109
109
  return list(self._instr_to_sub_to_buffer[instrument][event_type])
110
110
 
111
111
  def update(
112
- self, instrument: Instrument, event_type: str, data: Any, update_ohlc: bool = False, is_historical: bool = False
112
+ self,
113
+ instrument: Instrument,
114
+ event_type: str,
115
+ data: Any,
116
+ update_ohlc: bool = False,
117
+ is_historical: bool = False,
118
+ is_base_data: bool = True,
113
119
  ) -> None:
114
120
  # - store data in buffer if it's not OHLC
115
121
  if event_type != DataType.OHLC:
116
122
  self._instr_to_sub_to_buffer[instrument][event_type].append(data)
117
123
 
118
- if not is_historical:
124
+ if not is_historical and is_base_data:
119
125
  self._ready_instruments.add(instrument)
120
126
 
121
127
  if not update_ohlc:
qubx/core/initializer.py CHANGED
@@ -8,7 +8,7 @@ schedules, warmup periods, and position mismatch resolution.
8
8
  from dataclasses import dataclass, field
9
9
  from typing import Any, Dict, Optional
10
10
 
11
- from qubx.core.basics import td_64
11
+ from qubx.core.basics import Instrument, td_64
12
12
  from qubx.core.interfaces import IStrategyInitializer, StartTimeFinderProtocol, StateResolverProtocol
13
13
  from qubx.core.utils import recognize_timeframe
14
14
 
@@ -35,6 +35,9 @@ class BasicStrategyInitializer(IStrategyInitializer):
35
35
  # Additional configuration that might be needed
36
36
  config: Dict[str, Any] = field(default_factory=dict)
37
37
 
38
+ _pending_global_subscriptions: set[str] = field(default_factory=set)
39
+ _pending_instrument_subscriptions: dict[str, set[Instrument]] = field(default_factory=dict)
40
+
38
41
  def set_base_subscription(self, subscription_type: str) -> None:
39
42
  self.base_subscription = subscription_type
40
43
 
@@ -87,3 +90,19 @@ class BasicStrategyInitializer(IStrategyInitializer):
87
90
  @property
88
91
  def is_simulation(self) -> bool | None:
89
92
  return self.simulation
93
+
94
+ def subscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
95
+ if instruments is None:
96
+ self._pending_global_subscriptions.add(subscription_type)
97
+ return
98
+
99
+ if isinstance(instruments, Instrument):
100
+ instruments = [instruments]
101
+
102
+ self._pending_instrument_subscriptions[subscription_type].update(instruments)
103
+
104
+ def get_pending_global_subscriptions(self) -> set[str]:
105
+ return self._pending_global_subscriptions
106
+
107
+ def get_pending_instrument_subscriptions(self) -> dict[str, set[Instrument]]:
108
+ return self._pending_instrument_subscriptions
qubx/core/interfaces.py CHANGED
@@ -978,14 +978,18 @@ class IWarmupStateSaver:
978
978
 
979
979
  @dataclass
980
980
  class StrategyState:
981
+ is_on_init_called: bool = False
981
982
  is_on_start_called: bool = False
982
983
  is_on_warmup_finished_called: bool = False
983
984
  is_on_fit_called: bool = False
985
+ is_warmup_in_progress: bool = False
984
986
 
985
987
  def reset_from_state(self, state: "StrategyState"):
988
+ self.is_on_init_called = state.is_on_init_called
986
989
  self.is_on_start_called = state.is_on_start_called
987
990
  self.is_on_warmup_finished_called = state.is_on_warmup_finished_called
988
991
  self.is_on_fit_called = state.is_on_fit_called
992
+ self.is_warmup_in_progress = state.is_warmup_in_progress
989
993
 
990
994
 
991
995
  class IStrategyContext(
@@ -1380,6 +1384,27 @@ class IStrategyInitializer:
1380
1384
  """
1381
1385
  ...
1382
1386
 
1387
+ def subscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
1388
+ """Subscribe to market data for an instrument.
1389
+
1390
+ Args:
1391
+ subscription_type: Type of subscription. If None, the base subscription type is used.
1392
+ instruments: A list of instrument of instrument to subscribe to
1393
+ """
1394
+ ...
1395
+
1396
+ def get_pending_global_subscriptions(self) -> set[str]:
1397
+ """
1398
+ Get the pending global subscriptions.
1399
+ """
1400
+ ...
1401
+
1402
+ def get_pending_instrument_subscriptions(self) -> dict[str, set[Instrument]]:
1403
+ """
1404
+ Get the pending instrument subscriptions.
1405
+ """
1406
+ ...
1407
+
1383
1408
 
1384
1409
  class IStrategy(metaclass=Mixable):
1385
1410
  """Base class for trading strategies."""
qubx/core/lookups.py CHANGED
@@ -380,7 +380,7 @@ class FeesLookup:
380
380
 
381
381
  # - check if spec is of type maker=...,taker=...
382
382
  # Check if spec is in the format maker=X,taker=Y
383
- maker_taker_pattern = re.compile(r"maker=(-?[0-9.]+),taker=(-?[0-9.]+)")
383
+ maker_taker_pattern = re.compile(r"maker=(-?[0-9.]+)[,\ ]taker=(-?[0-9.]+)")
384
384
  match = maker_taker_pattern.match(spec)
385
385
  if match:
386
386
  maker_rate, taker_rate = float(match.group(1)), float(match.group(2))
@@ -346,7 +346,14 @@ class ProcessingManager(IProcessingManager):
346
346
 
347
347
  # update cached ohlc is this is base subscription
348
348
  _update_ohlc = is_base_data
349
- self._cache.update(instrument, event_type, _update, update_ohlc=_update_ohlc, is_historical=is_historical)
349
+ self._cache.update(
350
+ instrument,
351
+ event_type,
352
+ _update,
353
+ update_ohlc=_update_ohlc,
354
+ is_historical=is_historical,
355
+ is_base_data=is_base_data,
356
+ )
350
357
 
351
358
  # update trackers, gatherers on base data
352
359
  if not is_historical: