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

qubx/__init__.py CHANGED
@@ -65,7 +65,7 @@ class QubxLogConfig:
65
65
  QubxLogConfig.setup_logger(level)
66
66
 
67
67
  @staticmethod
68
- def setup_logger(level: str | None = None, custom_formatter: Callable | None = None):
68
+ def setup_logger(level: str | None = None, custom_formatter: Callable | None = None, colorize: bool = True):
69
69
  global logger
70
70
 
71
71
  config = {
@@ -82,13 +82,13 @@ class QubxLogConfig:
82
82
  logger.add(
83
83
  sys.stdout,
84
84
  format=custom_formatter or formatter,
85
- colorize=True,
85
+ colorize=colorize,
86
86
  level=level,
87
87
  enqueue=True,
88
88
  backtrace=True,
89
89
  diagnose=True,
90
90
  )
91
- logger = logger.opt(colors=True)
91
+ logger = logger.opt(colors=colorize)
92
92
 
93
93
 
94
94
  QubxLogConfig.setup_logger()
@@ -99,18 +99,7 @@ if runtime_env() in ["notebook", "shell"]:
99
99
  from IPython.core.getipython import get_ipython
100
100
  from IPython.core.magic import Magics, line_cell_magic, line_magic, magics_class
101
101
 
102
- from qubx.utils.charting.lookinglass import LookingGlass # noqa: F401
103
- from qubx.utils.charting.mpl_helpers import ( # noqa: F401
104
- ellips,
105
- fig,
106
- hline,
107
- ohlc_plot,
108
- plot_trends,
109
- sbp,
110
- set_mpl_theme,
111
- vline,
112
- )
113
- from qubx.utils.misc import install_pyx_recompiler_for_dev
102
+ from qubx.utils.charting.mpl_helpers import set_mpl_theme
114
103
 
115
104
  @magics_class
116
105
  class QubxMagics(Magics):
@@ -140,6 +129,8 @@ if runtime_env() in ["notebook", "shell"]:
140
129
 
141
130
  # setup cython dev hooks - only if 'dev' is passed as argument
142
131
  if line and "dev" in args:
132
+ from qubx.utils.misc import install_pyx_recompiler_for_dev
133
+
143
134
  install_pyx_recompiler_for_dev()
144
135
 
145
136
  tpl_path = os.path.join(os.path.dirname(__file__), "_nb_magic.py")
@@ -159,7 +150,7 @@ if runtime_env() in ["notebook", "shell"]:
159
150
  exec(_vscode_clr_trick, self.shell.user_ns)
160
151
 
161
152
  elif "light" in line.lower():
162
- sort: skip_mpl_theme("light")
153
+ set_mpl_theme("light")
163
154
 
164
155
  def _get_manager(self):
165
156
  if self.__manager is None:
qubx/backtester/ome.py CHANGED
@@ -9,12 +9,14 @@ from qubx.core.basics import (
9
9
  OPTION_FILL_AT_SIGNAL_PRICE,
10
10
  OPTION_SIGNAL_PRICE,
11
11
  OPTION_SKIP_PRICE_CROSS_CONTROL,
12
+ OPTION_AVOID_STOP_ORDER_PRICE_VALIDATION,
12
13
  Deal,
13
14
  Instrument,
14
15
  ITimeProvider,
15
16
  Order,
16
17
  OrderSide,
17
18
  OrderType,
19
+ OrderStatus,
18
20
  TransactionCostsCalculator,
19
21
  dt_64,
20
22
  )
@@ -37,14 +39,20 @@ class SimulatedExecutionReport:
37
39
  class OrdersManagementEngine:
38
40
  """
39
41
  Orders Management Engine (OME) is a simple implementation of a management of orders for simulation of a limit order book.
42
+
43
+ 2025-06-02: Added support for deferred execution reports (mainly for stop orders). This handles following cases:
44
+ - It's possible to send stop loss order (STOP_MARKET) in on_execution_report() from custom PositionsTracker class
45
+ - This order may be executed immediately that can lead to calling of on_execution_report() again (when we are still in on_execution_report())
46
+ - To avoid this, it emulate stop orders execution (when condition is met) and add deferred execution report to the list
47
+ - Deferred executions then would be sent on next process_market_data() call
40
48
  """
41
49
 
42
50
  instrument: Instrument
43
51
  time_service: ITimeProvider
44
52
  active_orders: dict[str, Order]
45
53
  stop_orders: dict[str, Order]
46
- asks: SortedDict[float, list[str]]
47
- bids: SortedDict[float, list[str]]
54
+ asks: SortedDict # [float, list[str]]
55
+ bids: SortedDict # [float, list[str]]
48
56
  bbo: Quote | None # - current best bid/ask order book
49
57
  __prev_bbo: Quote | None # - previous best bid/ask order book
50
58
  __order_id: int
@@ -53,6 +61,7 @@ class OrdersManagementEngine:
53
61
  _tick_size: float
54
62
  _last_update_time: dt_64
55
63
  _last_data_update_time_ns: int
64
+ _deferred_exec_reports: list[SimulatedExecutionReport]
56
65
 
57
66
  def __init__(
58
67
  self,
@@ -76,6 +85,7 @@ class OrdersManagementEngine:
76
85
  self._tick_size = instrument.tick_size
77
86
  self._last_update_time = np.datetime64(0, "ns")
78
87
  self._last_data_update_time_ns = 0
88
+ self._deferred_exec_reports = []
79
89
 
80
90
  if not debug:
81
91
  self._dbg = lambda message, **kwargs: None
@@ -94,6 +104,11 @@ class OrdersManagementEngine:
94
104
  def get_open_orders(self) -> list[Order]:
95
105
  return list(self.active_orders.values()) + list(self.stop_orders.values())
96
106
 
107
+ def __remove_pending_status(self, exec: SimulatedExecutionReport) -> SimulatedExecutionReport:
108
+ if exec.order.status == "PENDING":
109
+ exec.order.status = "CLOSED"
110
+ return exec
111
+
97
112
  def process_market_data(self, mdata: Quote | OrderBook | Trade | TradeArray) -> list[SimulatedExecutionReport]:
98
113
  """
99
114
  Processes the new market data (quote, trade or trades array) and simulates the execution of pending orders.
@@ -101,6 +116,11 @@ class OrdersManagementEngine:
101
116
  timestamp = self.time_service.time()
102
117
  _exec_report = []
103
118
 
119
+ # - process deferred exec reports: spit out deferred exec reports in first place
120
+ if self._deferred_exec_reports:
121
+ _exec_report = [self.__remove_pending_status(i) for i in self._deferred_exec_reports]
122
+ self._deferred_exec_reports.clear()
123
+
104
124
  # - pass through data if it's older than previous update
105
125
  if mdata.time < self._last_data_update_time_ns:
106
126
  return _exec_report
@@ -191,7 +211,7 @@ class OrdersManagementEngine:
191
211
  raise ExchangeError(f"Simulator is not ready for order management - no quote for {self.instrument.symbol}")
192
212
 
193
213
  # - validate order parameters
194
- self._validate_order(order_side, order_type, amount, price, time_in_force)
214
+ self._validate_order(order_side, order_type, amount, price, time_in_force, options)
195
215
 
196
216
  timestamp = self.time_service.time()
197
217
  order = Order(
@@ -262,7 +282,39 @@ class OrdersManagementEngine:
262
282
  case "STOP_MARKET":
263
283
  # - it processes stop orders separately without adding to orderbook (as on real exchanges)
264
284
  order.status = "OPEN"
265
- self.stop_orders[order.id] = order
285
+ _stp_order = order
286
+ _emulate_price_exec = self._fill_stops_at_price or _stp_order.options.get(
287
+ OPTION_FILL_AT_SIGNAL_PRICE, False
288
+ )
289
+
290
+ if _stp_order.side == "BUY" and _c_ask >= _stp_order.price:
291
+ # _exec_price = _c_ask if not _emulate_price_exec else so.price
292
+ self._deferred_exec_reports.append(
293
+ self._execute_order(
294
+ timestamp,
295
+ _c_ask if not _emulate_price_exec else _stp_order.price,
296
+ order,
297
+ True,
298
+ "BBO: " + str(self.bbo),
299
+ "PENDING",
300
+ )
301
+ )
302
+
303
+ elif _stp_order.side == "SELL" and _c_bid <= _stp_order.price:
304
+ # _exec_price = _c_bid if not _emulate_price_exec else so.price
305
+ self._deferred_exec_reports.append(
306
+ self._execute_order(
307
+ timestamp,
308
+ _c_bid if not _emulate_price_exec else _stp_order.price,
309
+ order,
310
+ True,
311
+ "BBO: " + str(self.bbo),
312
+ "PENDING",
313
+ )
314
+ )
315
+
316
+ else:
317
+ self.stop_orders[order.id] = order
266
318
 
267
319
  case "STOP_LIMIT":
268
320
  # TODO: (OME) check trigger conditions in options etc
@@ -289,11 +341,17 @@ class OrdersManagementEngine:
289
341
  return SimulatedExecutionReport(self.instrument, timestamp, order, None)
290
342
 
291
343
  def _execute_order(
292
- self, timestamp: dt_64, exec_price: float, order: Order, taker: bool, market_state: str
344
+ self,
345
+ timestamp: dt_64,
346
+ exec_price: float,
347
+ order: Order,
348
+ taker: bool,
349
+ market_state: str,
350
+ status: OrderStatus = "CLOSED",
293
351
  ) -> SimulatedExecutionReport:
294
- order.status = "CLOSED"
352
+ order.status = status
295
353
  self._dbg(
296
- f"<red>{order.id}</red> {order.type} {order.side} {order.quantity} executed at {exec_price} ::: {market_state}"
354
+ f"<red>{order.id}</red> {order.type} {order.side} {order.quantity} executed at {exec_price} ::: {market_state} [{status}]"
297
355
  )
298
356
  return SimulatedExecutionReport(
299
357
  self.instrument,
@@ -314,7 +372,7 @@ class OrdersManagementEngine:
314
372
  )
315
373
 
316
374
  def _validate_order(
317
- self, order_side: str, order_type: str, amount: float, price: float | None, time_in_force: str
375
+ self, order_side: str, order_type: str, amount: float, price: float | None, time_in_force: str, options: dict
318
376
  ) -> None:
319
377
  if order_side.upper() not in ["BUY", "SELL"]:
320
378
  raise InvalidOrder("Invalid order side. Only BUY or SELL is allowed.")
@@ -333,7 +391,12 @@ class OrdersManagementEngine:
333
391
  raise InvalidOrder("Invalid time in force. Only GTC, IOC, GTX are supported for now.")
334
392
 
335
393
  if _ot.startswith("STOP"):
336
- assert price is not None
394
+ # - if the option is set, we don't check the current market price against the stop price
395
+ if options.get(OPTION_AVOID_STOP_ORDER_PRICE_VALIDATION, False):
396
+ return
397
+
398
+ assert self.bbo
399
+ assert price
337
400
  c_ask, c_bid = self.bbo.ask, self.bbo.bid
338
401
  if (order_side == "BUY" and c_ask >= price) or (order_side == "SELL" and c_bid <= price):
339
402
  raise ExchangeError(
qubx/cli/commands.py CHANGED
@@ -68,7 +68,8 @@ def main(debug: bool, debug_port: int, log_level: str):
68
68
  @click.option(
69
69
  "--restore", "-r", is_flag=True, default=False, help="Restore strategy state from previous run.", show_default=True
70
70
  )
71
- def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool, restore: bool):
71
+ @click.option("--no-color", is_flag=True, default=False, help="Disable colored logging output.", show_default=True)
72
+ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool, restore: bool, no_color: bool):
72
73
  """
73
74
  Starts the strategy with the given configuration file. If paper mode is enabled, account is not required.
74
75
 
@@ -87,7 +88,7 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
87
88
  run_strategy_yaml_in_jupyter(config_file, account_file, paper, restore)
88
89
  else:
89
90
  logo()
90
- run_strategy_yaml(config_file, account_file, paper=paper, restore=restore, blocking=True)
91
+ run_strategy_yaml(config_file, account_file, paper=paper, restore=restore, blocking=True, no_color=no_color)
91
92
 
92
93
 
93
94
  @main.command()
@@ -249,5 +250,30 @@ def deploy(zip_file: str, output_dir: str | None, force: bool):
249
250
  deploy_strategy(zip_file, output_dir, force)
250
251
 
251
252
 
253
+ @main.command()
254
+ @click.argument(
255
+ "results-path",
256
+ type=click.Path(exists=True, resolve_path=True),
257
+ default="results",
258
+ callback=lambda ctx, param, value: os.path.abspath(os.path.expanduser(value)),
259
+ )
260
+ def browse(results_path: str):
261
+ """
262
+ Browse backtest results using an interactive TUI.
263
+
264
+ Opens a text-based user interface for exploring backtest results stored in ZIP files.
265
+ The browser provides:
266
+ - Tree view of results organized by strategy
267
+ - Table view with sortable metrics
268
+ - Equity chart view for comparing performance
269
+
270
+ Results are loaded from the specified directory containing .zip files
271
+ created by qubx simulate or result.to_file() methods.
272
+ """
273
+ from .tui import run_backtest_browser
274
+
275
+ run_backtest_browser(results_path)
276
+
277
+
252
278
  if __name__ == "__main__":
253
279
  main()